mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Compare commits
899 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
68547190c7 | ||
![]() |
94e55dfc6c | ||
![]() |
bb0c71549a | ||
![]() |
2a648e6ea5 | ||
![]() |
3a58afef10 | ||
![]() |
84dbcb5f11 | ||
![]() |
cd8ad77515 | ||
![]() |
050461528c | ||
![]() |
073cd4f0ba | ||
![]() |
42e0c015e8 | ||
![]() |
4888717c09 | ||
![]() |
449ebe4b54 | ||
![]() |
3cca1fdbf3 | ||
![]() |
50c184a9a2 | ||
![]() |
e2e15e3f0c | ||
![]() |
0048bf7326 | ||
![]() |
4fd894308d | ||
![]() |
496b50f6b1 | ||
![]() |
729a58a860 | ||
![]() |
2d96b7191d | ||
![]() |
dca5e363d1 | ||
![]() |
e670b6d924 | ||
![]() |
5c1cbcfcb1 | ||
![]() |
a11d3102ce | ||
![]() |
f184154aaa | ||
![]() |
f93870ea31 | ||
![]() |
941469df29 | ||
![]() |
ef95c3fe72 | ||
![]() |
572100b1a3 | ||
![]() |
200193262c | ||
![]() |
74e97347bb | ||
![]() |
b742dbc0fc | ||
![]() |
fcca9b72d3 | ||
![]() |
5edab3f795 | ||
![]() |
a2b99c3c92 | ||
![]() |
fde2c8c0c7 | ||
![]() |
48ea20c9e4 | ||
![]() |
906b5f7f2c | ||
![]() |
bbc1afada5 | ||
![]() |
efa32646ed | ||
![]() |
f1e05043bc | ||
![]() |
eedfb8b0a2 | ||
![]() |
ef28ba3038 | ||
![]() |
dd52e8b89e | ||
![]() |
e74bf8ae2a | ||
![]() |
9b7edd1c59 | ||
![]() |
93926d2700 | ||
![]() |
41b3a404dc | ||
![]() |
c922886f8a | ||
![]() |
2bd5f564a3 | ||
![]() |
f4c969fd00 | ||
![]() |
c3daefa6e9 | ||
![]() |
4c5813463f | ||
![]() |
6f318071db | ||
![]() |
b3da438a00 | ||
![]() |
945604ea79 | ||
![]() |
f914e1b7bf | ||
![]() |
4cb99c3fd5 | ||
![]() |
715719238a | ||
![]() |
7b38c21631 | ||
![]() |
958aab240d | ||
![]() |
4719cf26f4 | ||
![]() |
a6266dc4bf | ||
![]() |
12c9825658 | ||
![]() |
448bc823ef | ||
![]() |
78eb54464c | ||
![]() |
75fbdbf70e | ||
![]() |
326bf8d85f | ||
![]() |
38e04969df | ||
![]() |
a1bf55c7ae | ||
![]() |
707e1e9b98 | ||
![]() |
7f9f1feed5 | ||
![]() |
52a94815c0 | ||
![]() |
6594ac1262 | ||
![]() |
6e34fadcc2 | ||
![]() |
a16955136d | ||
![]() |
d50c038e07 | ||
![]() |
b59bded648 | ||
![]() |
3d825c56bf | ||
![]() |
756b5646f6 | ||
![]() |
640be407cf | ||
![]() |
d7948ad0bc | ||
![]() |
b02cbfce7f | ||
![]() |
59d5f28ed6 | ||
![]() |
24a0d7fd00 | ||
![]() |
91afa1004e | ||
![]() |
0882d96624 | ||
![]() |
3ea32ba891 | ||
![]() |
12b4893239 | ||
![]() |
aea6983cc4 | ||
![]() |
65f5bd1c37 | ||
![]() |
c5ec42f587 | ||
![]() |
741c8f62e0 | ||
![]() |
5b1ac25794 | ||
![]() |
eedcca7879 | ||
![]() |
7f70d7f3f2 | ||
![]() |
8447769447 | ||
![]() |
4d45ca6f09 | ||
![]() |
77c3a279f7 | ||
![]() |
fde057171d | ||
![]() |
6c3d7501e2 | ||
![]() |
1b283c8694 | ||
![]() |
401e5c0b07 | ||
![]() |
7e1701d9d1 | ||
![]() |
06e5f534ba | ||
![]() |
3ee605b433 | ||
![]() |
bc0132239e | ||
![]() |
796229ac34 | ||
![]() |
3d75a7288f | ||
![]() |
42bf34c29a | ||
![]() |
d0e810d29a | ||
![]() |
b7aa685009 | ||
![]() |
cfe49b37ec | ||
![]() |
f383fe101a | ||
![]() |
7db2817877 | ||
![]() |
24dbbbad64 | ||
![]() |
5894c362d6 | ||
![]() |
57667734c6 | ||
![]() |
8f852e3153 | ||
![]() |
aaae2910e7 | ||
![]() |
507cedca1c | ||
![]() |
a94128a9ce | ||
![]() |
d477aa0df6 | ||
![]() |
198192aeb4 | ||
![]() |
d1b9a88297 | ||
![]() |
2ebd9a74f5 | ||
![]() |
c2fc1cbbd9 | ||
![]() |
423633bc2b | ||
![]() |
045409fa35 | ||
![]() |
74bb0e58c0 | ||
![]() |
bf1c1379a2 | ||
![]() |
c9eb2e2289 | ||
![]() |
d090795d0c | ||
![]() |
6822bcfcb3 | ||
![]() |
9ae1a0177c | ||
![]() |
c26c2c6007 | ||
![]() |
2955824366 | ||
![]() |
fac0f0bc1d | ||
![]() |
33aaa7f1d1 | ||
![]() |
2453a82856 | ||
![]() |
0967ee953c | ||
![]() |
a1a6515524 | ||
![]() |
0dd0198693 | ||
![]() |
6dcdc680e0 | ||
![]() |
8f0389ab8c | ||
![]() |
035846578f | ||
![]() |
c858dd9982 | ||
![]() |
789696d22f | ||
![]() |
0bb20d7e19 | ||
![]() |
e3f0beb6cb | ||
![]() |
a383baa812 | ||
![]() |
65f89db861 | ||
![]() |
b591f42914 | ||
![]() |
3ca2fec672 | ||
![]() |
263cd2e3d1 | ||
![]() |
d7ae2daed1 | ||
![]() |
667dc856ce | ||
![]() |
00b70b84ab | ||
![]() |
748b940dc4 | ||
![]() |
6126aed19e | ||
![]() |
82b92b497f | ||
![]() |
67f495bdac | ||
![]() |
51ecd4c2b0 | ||
![]() |
3561f25806 | ||
![]() |
05c4e1c43d | ||
![]() |
0c5f88a541 | ||
![]() |
e631b1d32e | ||
![]() |
f6c77d1383 | ||
![]() |
3656df036f | ||
![]() |
bc2b2a6c19 | ||
![]() |
54f572fbd0 | ||
![]() |
98ccaec295 | ||
![]() |
ce64ee9c5c | ||
![]() |
4222dd6686 | ||
![]() |
d7ffde7299 | ||
![]() |
bcb012977c | ||
![]() |
18d05d3a40 | ||
![]() |
5a87e008e5 | ||
![]() |
c5f854164c | ||
![]() |
d301a9f3ae | ||
![]() |
fd0fc9a5f9 | ||
![]() |
264558e2ca | ||
![]() |
92a4123f25 | ||
![]() |
5926787861 | ||
![]() |
8d262d01b2 | ||
![]() |
ae9a802403 | ||
![]() |
4fd2cdb390 | ||
![]() |
3edb4b75ee | ||
![]() |
59adaaad69 | ||
![]() |
82815624b4 | ||
![]() |
1496961c53 | ||
![]() |
18b22257a8 | ||
![]() |
bc1a7d0873 | ||
![]() |
bd6eb388f8 | ||
![]() |
71c832f6b2 | ||
![]() |
f3f77f596e | ||
![]() |
ef690bc792 | ||
![]() |
ad41b6c06e | ||
![]() |
e10ed4f5ed | ||
![]() |
15867684f5 | ||
![]() |
57a8e18022 | ||
![]() |
19ec133abb | ||
![]() |
f5d6097980 | ||
![]() |
93f5a7d789 | ||
![]() |
afc1f0e6b0 | ||
![]() |
adfa6b43ad | ||
![]() |
06fd09b93a | ||
![]() |
9d9607feff | ||
![]() |
d1a35e8421 | ||
![]() |
da23ad1d09 | ||
![]() |
b4df49b87f | ||
![]() |
6fce7c808c | ||
![]() |
89360a4ef0 | ||
![]() |
acaabaace1 | ||
![]() |
e763ad3036 | ||
![]() |
b44dfef3f9 | ||
![]() |
9619c2ea7d | ||
![]() |
83f74169da | ||
![]() |
fc986076c9 | ||
![]() |
8c9b4abe45 | ||
![]() |
a5c087d3d4 | ||
![]() |
04245f9dc1 | ||
![]() |
9af56c26bc | ||
![]() |
1967546cab | ||
![]() |
bd337df442 | ||
![]() |
57caf25611 | ||
![]() |
a53ed039b8 | ||
![]() |
1289d645d8 | ||
![]() |
f19954414f | ||
![]() |
1c5101a22b | ||
![]() |
94802f3175 | ||
![]() |
3e1cdb9fa2 | ||
![]() |
309068ae1d | ||
![]() |
17247f205f | ||
![]() |
cdb861a26a | ||
![]() |
37e13bbcd2 | ||
![]() |
c9905ecd3a | ||
![]() |
37da276f9c | ||
![]() |
e0eebb1c7e | ||
![]() |
06d9c7a13d | ||
![]() |
121d9029b9 | ||
![]() |
13bceb5f40 | ||
![]() |
29a88c0dde | ||
![]() |
445866967f | ||
![]() |
2612112c63 | ||
![]() |
d46ca9b53a | ||
![]() |
924b2cb614 | ||
![]() |
3f3bb74f99 | ||
![]() |
8cb0062bb7 | ||
![]() |
54ef9b308f | ||
![]() |
29506af46b | ||
![]() |
dcf3eb95a5 | ||
![]() |
7a7f7b5dab | ||
![]() |
1638b6c22d | ||
![]() |
aa94ea8dd9 | ||
![]() |
06b9252b64 | ||
![]() |
d82b737133 | ||
![]() |
07e3ce416a | ||
![]() |
7586210cb8 | ||
![]() |
cb736664b8 | ||
![]() |
459e40762a | ||
![]() |
78bc2482fd | ||
![]() |
57ebcba34c | ||
![]() |
382ecf4d73 | ||
![]() |
84968a3c02 | ||
![]() |
165d956fb5 | ||
![]() |
09596e3b56 | ||
![]() |
89eed6ca11 | ||
![]() |
222e0fc635 | ||
![]() |
4cf831573a | ||
![]() |
c2365a945e | ||
![]() |
cebbf9173d | ||
![]() |
8fc5a1aba5 | ||
![]() |
fe3dbf5ec9 | ||
![]() |
3744ca473f | ||
![]() |
d6e4dac032 | ||
![]() |
b45fbf4337 | ||
![]() |
eadbf4e001 | ||
![]() |
3fd439839d | ||
![]() |
4109f157c0 | ||
![]() |
189d846ab8 | ||
![]() |
d8368b86e6 | ||
![]() |
e9af88b332 | ||
![]() |
32fbe20b13 | ||
![]() |
2790ec5aaa | ||
![]() |
8f4ba03550 | ||
![]() |
a02704a7a4 | ||
![]() |
cd8573d79e | ||
![]() |
e399515941 | ||
![]() |
a60eeeff60 | ||
![]() |
3a93b3154c | ||
![]() |
aa5687ca77 | ||
![]() |
c6ffcb7ce4 | ||
![]() |
b4d7bd49e0 | ||
![]() |
4233b20451 | ||
![]() |
c52f4b4d48 | ||
![]() |
db7c5d8a0e | ||
![]() |
b4d3b175dc | ||
![]() |
76aa084fd5 | ||
![]() |
51d5a523ea | ||
![]() |
17eb4dc74f | ||
![]() |
622babd39e | ||
![]() |
b6cd911bc2 | ||
![]() |
e8a1d94ac2 | ||
![]() |
4df35a65e2 | ||
![]() |
0689fc7d87 | ||
![]() |
ce73fefa84 | ||
![]() |
d462d354f9 | ||
![]() |
5c73deed31 | ||
![]() |
fe413000f4 | ||
![]() |
d1b15d4f3c | ||
![]() |
eeaec6d9a2 | ||
![]() |
4a44c774d8 | ||
![]() |
10f2497c82 | ||
![]() |
3f032d24af | ||
![]() |
015a324fad | ||
![]() |
7501394036 | ||
![]() |
02b1d137ba | ||
![]() |
45dbdb13c8 | ||
![]() |
d8de2f3ff9 | ||
![]() |
ee96cf3a19 | ||
![]() |
f2556d80e3 | ||
![]() |
1a9ef4ceaa | ||
![]() |
d3863d3a9a | ||
![]() |
037197a7e9 | ||
![]() |
56b8bff325 | ||
![]() |
3f4c267ec9 | ||
![]() |
faeacaec68 | ||
![]() |
27f1976929 | ||
![]() |
2011ea37e6 | ||
![]() |
91002ac042 | ||
![]() |
c958f6271f | ||
![]() |
d2064d873b | ||
![]() |
b0d5a6776b | ||
![]() |
49da883f7e | ||
![]() |
e1b543bfa0 | ||
![]() |
65ae21436a | ||
![]() |
81f04543e5 | ||
![]() |
1dfc3a3b74 | ||
![]() |
242e9ad983 | ||
![]() |
754b60cfe1 | ||
![]() |
175f065811 | ||
![]() |
62d58a86fa | ||
![]() |
50a66c6843 | ||
![]() |
81b8706524 | ||
![]() |
4739e367a6 | ||
![]() |
fe1448fa19 | ||
![]() |
11eea4731e | ||
![]() |
d2a5d4d25d | ||
![]() |
7fcdae56ac | ||
![]() |
66bcbaf21c | ||
![]() |
57472ed255 | ||
![]() |
30656ae18f | ||
![]() |
0adafa0fc0 | ||
![]() |
546bd42240 | ||
![]() |
532020e2af | ||
![]() |
70010fac73 | ||
![]() |
ed8f96354f | ||
![]() |
4323ffbb4e | ||
![]() |
ff400224c2 | ||
![]() |
1bdd0a8299 | ||
![]() |
94046baaf0 | ||
![]() |
6c85bbf852 | ||
![]() |
53472daa07 | ||
![]() |
a02aec578c | ||
![]() |
2266bcabb9 | ||
![]() |
684fb57019 | ||
![]() |
013d413841 | ||
![]() |
379387f56f | ||
![]() |
de884c6721 | ||
![]() |
3fbaae8ac2 | ||
![]() |
86e857e969 | ||
![]() |
9373f571be | ||
![]() |
bbe4910247 | ||
![]() |
5b887a77ae | ||
![]() |
6b0ae9f082 | ||
![]() |
83c1c5943e | ||
![]() |
208b29799a | ||
![]() |
c0f4de6077 | ||
![]() |
f5fd593976 | ||
![]() |
031b61c466 | ||
![]() |
ce28c64750 | ||
![]() |
614d906ca6 | ||
![]() |
0c7a89a70a | ||
![]() |
24b59a2560 | ||
![]() |
e9bb222b6c | ||
![]() |
eb11e5793f | ||
![]() |
a6b89bde2b | ||
![]() |
03425e10d3 | ||
![]() |
069f5d019b | ||
![]() |
cbce8580d2 | ||
![]() |
d964b71e93 | ||
![]() |
e0b7cf6592 | ||
![]() |
5bceb37150 | ||
![]() |
a840060b19 | ||
![]() |
1d6bdd1bab | ||
![]() |
a2a93b2c0f | ||
![]() |
1d2b3ed4a2 | ||
![]() |
73416ebb42 | ||
![]() |
a7759d627c | ||
![]() |
9d3f7aadbb | ||
![]() |
854c779ab1 | ||
![]() |
7090edbd4e | ||
![]() |
5a2d571ed1 | ||
![]() |
fe5ae394c6 | ||
![]() |
a5dcfaa36c | ||
![]() |
872f7bb370 | ||
![]() |
9ca0d0739b | ||
![]() |
faa69acafe | ||
![]() |
ccbd59713d | ||
![]() |
1651498e0b | ||
![]() |
7b62d5e8a9 | ||
![]() |
9c3e8cd681 | ||
![]() |
83b84fdb50 | ||
![]() |
66eb97d6a4 | ||
![]() |
11f769eade | ||
![]() |
aba91228de | ||
![]() |
a8df4ccbbe | ||
![]() |
9fbfb76985 | ||
![]() |
4ab22c7187 | ||
![]() |
f19d34aa63 | ||
![]() |
abf625fc96 | ||
![]() |
1973e08342 | ||
![]() |
14cdf72944 | ||
![]() |
afe730f945 | ||
![]() |
7a4efa23a4 | ||
![]() |
03e46572a3 | ||
![]() |
c43ff33483 | ||
![]() |
181582060b | ||
![]() |
d83931c145 | ||
![]() |
e90d194c6e | ||
![]() |
ef437b3874 | ||
![]() |
3791f73977 | ||
![]() |
c0c83f334c | ||
![]() |
6e8a473c5f | ||
![]() |
9f4ea1cf1d | ||
![]() |
bcf6c5ec19 | ||
![]() |
72ffbcc8ea | ||
![]() |
692f3e0b2d | ||
![]() |
e403af6e99 | ||
![]() |
069af38f02 | ||
![]() |
c03998f827 | ||
![]() |
7e52aeaddc | ||
![]() |
1d2b783b93 | ||
![]() |
e69cb0507d | ||
![]() |
39c48888ca | ||
![]() |
944240e442 | ||
![]() |
d1bb28374b | ||
![]() |
f85e1a57d4 | ||
![]() |
af859056da | ||
![]() |
f07071ea7b | ||
![]() |
200c5eb463 | ||
![]() |
c817c2839d | ||
![]() |
288b0f43a1 | ||
![]() |
00b2940315 | ||
![]() |
571334eb02 | ||
![]() |
8d9d0fccb4 | ||
![]() |
85caaf2fb3 | ||
![]() |
d84c65e90e | ||
![]() |
0afdeb80d3 | ||
![]() |
3a16f2da63 | ||
![]() |
b5ce236a20 | ||
![]() |
a3ddc7ab4b | ||
![]() |
51d10c47f9 | ||
![]() |
dca19b755d | ||
![]() |
3c2bb28823 | ||
![]() |
5a2e8fc0aa | ||
![]() |
c6387ffad1 | ||
![]() |
96a9b5cc90 | ||
![]() |
fbb6fbb56f | ||
![]() |
5bb9cc67dd | ||
![]() |
9e6630cfef | ||
![]() |
ef5c3f0c4f | ||
![]() |
4e4dcbaf72 | ||
![]() |
ed0a30afc7 | ||
![]() |
69182dc52c | ||
![]() |
b788266f3e | ||
![]() |
c3fb738013 | ||
![]() |
d4e55740b1 | ||
![]() |
d0898df596 | ||
![]() |
689296bf78 | ||
![]() |
4739b117e8 | ||
![]() |
fb9d678f7f | ||
![]() |
2e824a29cf | ||
![]() |
056a94a44d | ||
![]() |
ca2c6139ef | ||
![]() |
1570d57c79 | ||
![]() |
0c1db92a23 | ||
![]() |
d2706f6711 | ||
![]() |
db2dcbdc06 | ||
![]() |
63058d434b | ||
![]() |
db2d194150 | ||
![]() |
e8f3247efd | ||
![]() |
8f46c6d23c | ||
![]() |
9c94625fad | ||
![]() |
b3a7514066 | ||
![]() |
1a6ad8b633 | ||
![]() |
d014001f7b | ||
![]() |
f7f3a54e00 | ||
![]() |
a66750da25 | ||
![]() |
042edbc792 | ||
![]() |
845c49ac04 | ||
![]() |
b3c9bdb2e7 | ||
![]() |
e7d546d129 | ||
![]() |
23ae3d9a3e | ||
![]() |
e37ac1a2c1 | ||
![]() |
0d9944bcf0 | ||
![]() |
e26f4b7c5c | ||
![]() |
429a160943 | ||
![]() |
6a718036b9 | ||
![]() |
e0917b5ce5 | ||
![]() |
d77d20e297 | ||
![]() |
cec2a2f10b | ||
![]() |
6fa2cacc0c | ||
![]() |
a1095f74d9 | ||
![]() |
2dabd41230 | ||
![]() |
2f1f5e0080 | ||
![]() |
8a2d458cbf | ||
![]() |
8170f530f6 | ||
![]() |
d8d3a96dff | ||
![]() |
03a9466a90 | ||
![]() |
3292bbe55f | ||
![]() |
8114c38c2b | ||
![]() |
d56aaf5c99 | ||
![]() |
b19754f259 | ||
![]() |
feb178e171 | ||
![]() |
b78a1bec9c | ||
![]() |
c7dc293960 | ||
![]() |
281ad3351b | ||
![]() |
766b3f237d | ||
![]() |
6f3b827d6c | ||
![]() |
7b06f37b22 | ||
![]() |
bd33fbb4ec | ||
![]() |
25c5507a03 | ||
![]() |
49a6211f25 | ||
![]() |
b84f203459 | ||
![]() |
51aa83406e | ||
![]() |
389c432c5c | ||
![]() |
5ef31b0f47 | ||
![]() |
f50f25b236 | ||
![]() |
1c41936ab2 | ||
![]() |
9cbbffe100 | ||
![]() |
4b41715bc0 | ||
![]() |
d924dd3019 | ||
![]() |
f60ca3db99 | ||
![]() |
caff9a2d95 | ||
![]() |
01801d6bd0 | ||
![]() |
fd2292c723 | ||
![]() |
1aa9ff8aa4 | ||
![]() |
a9069d0d0b | ||
![]() |
6e40f1f5a0 | ||
![]() |
a29bf7619d | ||
![]() |
106acd8509 | ||
![]() |
f739c41722 | ||
![]() |
920e84dd4f | ||
![]() |
720f436654 | ||
![]() |
b4f5fdaf0c | ||
![]() |
1d0fdb9864 | ||
![]() |
dc05b37d17 | ||
![]() |
9f180a36c4 | ||
![]() |
3cf8adb6d1 | ||
![]() |
92e4492c48 | ||
![]() |
401dcafe47 | ||
![]() |
1d35888ca8 | ||
![]() |
5158eb0d9e | ||
![]() |
3f75361db6 | ||
![]() |
81044ac1cb | ||
![]() |
bba1ade360 | ||
![]() |
ae3f4a367d | ||
![]() |
a604032a11 | ||
![]() |
50de7f74ed | ||
![]() |
861718d276 | ||
![]() |
d9f9e9b29a | ||
![]() |
1365086b5a | ||
![]() |
222918cb09 | ||
![]() |
c0d86312a9 | ||
![]() |
ac6c54eb71 | ||
![]() |
6bba1244f9 | ||
![]() |
f83b02d788 | ||
![]() |
f07112109b | ||
![]() |
8d3bc24c4b | ||
![]() |
5138e28ff8 | ||
![]() |
54c8280a3b | ||
![]() |
64a4017a09 | ||
![]() |
e1a7d0ef8a | ||
![]() |
034e1bf328 | ||
![]() |
bad8ea2c2e | ||
![]() |
52a4813fbb | ||
![]() |
718107d861 | ||
![]() |
9aa557649e | ||
![]() |
1ee4d6f875 | ||
![]() |
42ef506081 | ||
![]() |
828a67aed8 | ||
![]() |
1681a664db | ||
![]() |
b9f6d021a3 | ||
![]() |
516e95597e | ||
![]() |
ccdf8ad4cf | ||
![]() |
0e8293becb | ||
![]() |
ba8810c411 | ||
![]() |
9cd5efd1c3 | ||
![]() |
b57f51145e | ||
![]() |
0f2364d08e | ||
![]() |
cf72ddf445 | ||
![]() |
7df9a6c089 | ||
![]() |
3b5f1f9f94 | ||
![]() |
d81dafc6a8 | ||
![]() |
04b8af8b39 | ||
![]() |
e8aca0072f | ||
![]() |
6f8344613c | ||
![]() |
d0680b5ba3 | ||
![]() |
9510b61356 | ||
![]() |
e97ef2d3eb | ||
![]() |
030398ca08 | ||
![]() |
6b65003085 | ||
![]() |
cbabe1a49e | ||
![]() |
95794e6604 | ||
![]() |
41c409252f | ||
![]() |
b65b180937 | ||
![]() |
53db855814 | ||
![]() |
de2a2997fd | ||
![]() |
56785c8e71 | ||
![]() |
df2459145f | ||
![]() |
85fc4cd7ca | ||
![]() |
0545e54215 | ||
![]() |
167475789c | ||
![]() |
a23d540597 | ||
![]() |
90b6988696 | ||
![]() |
638dcd3356 | ||
![]() |
dc7463d681 | ||
![]() |
abf0cf247f | ||
![]() |
98e4e19e20 | ||
![]() |
082ba268da | ||
![]() |
18d11094ce | ||
![]() |
b7ac3b2848 | ||
![]() |
548a635c9b | ||
![]() |
bb8a398933 | ||
![]() |
fd23e98d4c | ||
![]() |
204705452e | ||
![]() |
2c07a6c584 | ||
![]() |
302a57228c | ||
![]() |
ed51b887ef | ||
![]() |
a4bf3518e4 | ||
![]() |
be4b02c25d | ||
![]() |
9b6e0e1c4f | ||
![]() |
40fe866787 | ||
![]() |
b5e942256d | ||
![]() |
c95e7d3aa6 | ||
![]() |
61d493cd62 | ||
![]() |
6f7dd4a767 | ||
![]() |
f40eea3413 | ||
![]() |
03de725ffa | ||
![]() |
db7f832b1d | ||
![]() |
43a0228c2c | ||
![]() |
e8b70ccdff | ||
![]() |
140b153ff3 | ||
![]() |
499d4f970a | ||
![]() |
f3ac4d6a7a | ||
![]() |
6965754df1 | ||
![]() |
23ba5ad53e | ||
![]() |
5f1bedef44 | ||
![]() |
da8be31418 | ||
![]() |
226d4618e2 | ||
![]() |
0c525bb4fe | ||
![]() |
0354f8ab51 | ||
![]() |
809eaac6ce | ||
![]() |
3963a7d01b | ||
![]() |
dc4cf14b9a | ||
![]() |
245b941563 | ||
![]() |
ee0b4cfd3d | ||
![]() |
2751dbef6f | ||
![]() |
d5c4fa74e4 | ||
![]() |
1c31fe29cc | ||
![]() |
7d3e366f00 | ||
![]() |
5d3b6f6be9 | ||
![]() |
dd4027a10f | ||
![]() |
fac6b15ada | ||
![]() |
a2a8ba7af5 | ||
![]() |
1bf655fcba | ||
![]() |
e24716c571 | ||
![]() |
8c3797ced5 | ||
![]() |
5877b3749c | ||
![]() |
fbe794f9a4 | ||
![]() |
15057edf95 | ||
![]() |
0dbc7b9b8c | ||
![]() |
181c2e1e46 | ||
![]() |
cfb01e20c4 | ||
![]() |
967cb2a010 | ||
![]() |
f4a098749c | ||
![]() |
f226272a14 | ||
![]() |
2f7bdd664e | ||
![]() |
5925047fb7 | ||
![]() |
fac343ac28 | ||
![]() |
f915fab8d0 | ||
![]() |
2fffd9379b | ||
![]() |
079f499bc5 | ||
![]() |
bb27398151 | ||
![]() |
86b216cf86 | ||
![]() |
7b06bcc279 | ||
![]() |
a558e8b2d2 | ||
![]() |
53a93e279d | ||
![]() |
361daf8433 | ||
![]() |
09b14e656e | ||
![]() |
ae496af13e | ||
![]() |
73fa352cca | ||
![]() |
04a1c1aecc | ||
![]() |
00d4387a85 | ||
![]() |
b06fbace89 | ||
![]() |
6737fdf49a | ||
![]() |
f18ac54e5b | ||
![]() |
d49ecdf440 | ||
![]() |
d174362881 | ||
![]() |
1d0470561e | ||
![]() |
17626d7b3f | ||
![]() |
63d30864f4 | ||
![]() |
741a58b7c2 | ||
![]() |
ead5de75d4 | ||
![]() |
29dfb87133 | ||
![]() |
2e99b2dd75 | ||
![]() |
2735be4b47 | ||
![]() |
cbfa839804 | ||
![]() |
75c6fd9417 | ||
![]() |
89c0f36a53 | ||
![]() |
f8db7406cf | ||
![]() |
7ecfb807ad | ||
![]() |
d31993b714 | ||
![]() |
8f72e470f4 | ||
![]() |
8de04c14e9 | ||
![]() |
04333e817f | ||
![]() |
c1ced4209e | ||
![]() |
acaa6810df | ||
![]() |
97b846b806 | ||
![]() |
745c051bc3 | ||
![]() |
b7a974e448 | ||
![]() |
e498a80638 | ||
![]() |
b991534827 | ||
![]() |
efdbd77d5d | ||
![]() |
66b27aafac | ||
![]() |
75089d7dc2 | ||
![]() |
9480204f0a | ||
![]() |
ad21027598 | ||
![]() |
986e71a1f7 | ||
![]() |
e19ee1ebc9 | ||
![]() |
1efa315d55 | ||
![]() |
d6f7b471de | ||
![]() |
9f70fecb42 | ||
![]() |
98e3648a71 | ||
![]() |
933be18036 | ||
![]() |
5e71dec766 | ||
![]() |
3cce7ec1a0 | ||
![]() |
6a9c378021 | ||
![]() |
45da32cdd7 | ||
![]() |
8f87a6cd9f | ||
![]() |
d0babc0012 | ||
![]() |
a2812e40d9 | ||
![]() |
1797f7e548 | ||
![]() |
b78a3f2e89 | ||
![]() |
297e789cf8 | ||
![]() |
9a021cfdc0 | ||
![]() |
a3ab7af885 | ||
![]() |
10eacdecf2 | ||
![]() |
0bd2abc5e8 | ||
![]() |
a6f2e92c35 | ||
![]() |
9b05a4b49e | ||
![]() |
d0e2ecd82c | ||
![]() |
7cec8fd98c | ||
![]() |
7d99a6b857 | ||
![]() |
e7753c1b62 | ||
![]() |
6f68db1be9 | ||
![]() |
ade57193ac | ||
![]() |
a496da3780 | ||
![]() |
334ad174a9 | ||
![]() |
a1279d7eb5 | ||
![]() |
bd452215ae | ||
![]() |
85eb64fb0e | ||
![]() |
0341f17e69 | ||
![]() |
006fc59c88 | ||
![]() |
4474f95aa4 | ||
![]() |
d0240a1372 | ||
![]() |
b4dbe854b5 | ||
![]() |
f72762e5ee | ||
![]() |
ccb3fd4ab7 | ||
![]() |
6e44e7e29a | ||
![]() |
9719251d02 | ||
![]() |
b9c3a4837e | ||
![]() |
d60983bea5 | ||
![]() |
94deeb0a8f | ||
![]() |
f54d9e58f3 | ||
![]() |
b77555124d | ||
![]() |
8b6c0048b1 | ||
![]() |
3793a666a6 | ||
![]() |
d3176ad9ac | ||
![]() |
e222f78334 | ||
![]() |
0921485b55 | ||
![]() |
d7af541133 | ||
![]() |
75f34a9571 | ||
![]() |
edb113aa32 | ||
![]() |
8270cdbddf | ||
![]() |
672cbb6adc | ||
![]() |
9c0d014906 | ||
![]() |
517cd44534 | ||
![]() |
ff66a55759 | ||
![]() |
7fca6321dd | ||
![]() |
67d37707f9 | ||
![]() |
fcb63bc116 | ||
![]() |
a7be820abc | ||
![]() |
473cd4f7ef | ||
![]() |
69c851c8e6 | ||
![]() |
96380859ef | ||
![]() |
a256b54371 | ||
![]() |
5722674e99 | ||
![]() |
082c9e5b50 | ||
![]() |
ea55c052fc | ||
![]() |
3c5b3b07dc | ||
![]() |
9e53e4c861 | ||
![]() |
71744313f0 | ||
![]() |
0fc3f91d83 | ||
![]() |
76226d8568 | ||
![]() |
fd6b6b5931 | ||
![]() |
ccafb6c6eb | ||
![]() |
60db4392e0 | ||
![]() |
af8237dbff | ||
![]() |
211ff8457e | ||
![]() |
aa89718329 | ||
![]() |
b7f42a187e | ||
![]() |
6e57570283 | ||
![]() |
72fc4adfe8 | ||
![]() |
fb49e82524 | ||
![]() |
287f9adbc9 | ||
![]() |
cfc5cfd06b | ||
![]() |
07bc2bdac4 | ||
![]() |
301e19c4b4 | ||
![]() |
0ee00337f3 | ||
![]() |
a913427eaf | ||
![]() |
25a9f37ded | ||
![]() |
75d7c2a9dc | ||
![]() |
e9f887323a | ||
![]() |
8426746bf1 | ||
![]() |
6e296350e4 | ||
![]() |
b295dd5820 | ||
![]() |
f0f44e1704 | ||
![]() |
5ce0b0f65d | ||
![]() |
8f7dde01d5 | ||
![]() |
034b3eb220 | ||
![]() |
5bcf97598a | ||
![]() |
066ccbe409 | ||
![]() |
afaf15c5e9 | ||
![]() |
ff615d3bba | ||
![]() |
a4e27b5cdf | ||
![]() |
e37543250e | ||
![]() |
f1ac0f24b4 | ||
![]() |
0b387b7b82 | ||
![]() |
9c992530c5 | ||
![]() |
3c7883fb6d | ||
![]() |
2b80cfe651 | ||
![]() |
2675506c2e | ||
![]() |
f10534a774 | ||
![]() |
09db4b11d9 | ||
![]() |
095a5a315f | ||
![]() |
d2700679bb | ||
![]() |
0c82731912 | ||
![]() |
415100262e | ||
![]() |
05ece9c115 | ||
![]() |
f81a4a95fe | ||
![]() |
3019284fec | ||
![]() |
87e5eff41c | ||
![]() |
813673f4a4 | ||
![]() |
2faa99ed04 | ||
![]() |
e0f1d93a24 | ||
![]() |
8229f88aff | ||
![]() |
523fb867ee | ||
![]() |
ac2d85f7d7 | ||
![]() |
8810d63c40 | ||
![]() |
260a6e5ec4 | ||
![]() |
dff2e95369 | ||
![]() |
b35127e172 | ||
![]() |
09d5fcabd6 | ||
![]() |
1a5c4ff11d | ||
![]() |
3e69a6ce19 | ||
![]() |
e3a850cdef | ||
![]() |
33856ffa22 | ||
![]() |
1fe60b9406 | ||
![]() |
c345f683d6 | ||
![]() |
70871e4d4f | ||
![]() |
b466e5671c | ||
![]() |
c627e6d834 | ||
![]() |
5efbbcbeb1 | ||
![]() |
4572d49a0e | ||
![]() |
84abb552a5 | ||
![]() |
626aa929cf | ||
![]() |
61951b9b6f | ||
![]() |
8c30e166c3 | ||
![]() |
56a28cb95e | ||
![]() |
ad2eab26b1 | ||
![]() |
9f512fa1fd | ||
![]() |
a21955318b | ||
![]() |
288dffcde0 | ||
![]() |
19b8cfaa4f | ||
![]() |
df2d584b1b | ||
![]() |
4c99c2399c |
1839 changed files with 1021744 additions and 470489 deletions
17
.dprint.json
17
.dprint.json
|
@ -61,11 +61,22 @@
|
|||
"exportDeclaration.forceMultiLine": "never",
|
||||
"importDeclaration.forceMultiLine": "never",
|
||||
"arrayExpression.spaceAround": true,
|
||||
"arrayPattern.spaceAround": true
|
||||
"arrayPattern.spaceAround": true,
|
||||
"importDeclaration.preferSingleLine": true
|
||||
},
|
||||
"json": {},
|
||||
"markdown": {},
|
||||
"toml": {},
|
||||
"malva": {
|
||||
"quotes": "preferDouble",
|
||||
"printWidth": 140,
|
||||
"hexCase": "ignore",
|
||||
"blockSelectorLinebreak": "always"
|
||||
},
|
||||
"markup": {
|
||||
"printWidth": 160,
|
||||
"preferAttrsSingleLine": true
|
||||
},
|
||||
"excludes": [
|
||||
"**/node_modules",
|
||||
"**/*-lock.json",
|
||||
|
@ -75,6 +86,8 @@
|
|||
"https://plugins.dprint.dev/typescript-0.91.1.wasm",
|
||||
"https://plugins.dprint.dev/json-0.19.3.wasm",
|
||||
"https://plugins.dprint.dev/markdown-0.17.1.wasm",
|
||||
"https://plugins.dprint.dev/toml-0.6.2.wasm"
|
||||
"https://plugins.dprint.dev/toml-0.6.2.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/malva-v0.12.0.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.23.3.wasm"
|
||||
]
|
||||
}
|
||||
|
|
151
.eslintrc.json
151
.eslintrc.json
|
@ -1,151 +0,0 @@
|
|||
{
|
||||
"extends": "standard-with-typescript",
|
||||
"root": true,
|
||||
"rules": {
|
||||
"eol-last": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"indent": "off",
|
||||
"no-lone-blocks": "off",
|
||||
"no-mixed-operators": "off",
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 140
|
||||
}
|
||||
],
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"quote-props": [
|
||||
"error",
|
||||
"consistent-as-needed"
|
||||
],
|
||||
"padded-blocks": "off",
|
||||
"prefer-regex-literals": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"dot-notation": "off",
|
||||
"promise/param-names": "off",
|
||||
"import/first": "off",
|
||||
"operator-linebreak": [
|
||||
"error",
|
||||
"after",
|
||||
{
|
||||
"overrides": {
|
||||
"?": "before",
|
||||
":": "before"
|
||||
}
|
||||
}
|
||||
],
|
||||
"quotes": "off",
|
||||
|
||||
"no-constant-binary-expression": "error",
|
||||
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"MemberExpression": "off",
|
||||
// https://github.com/eslint/eslint/issues/15299
|
||||
"ignoredNodes": ["PropertyDefinition", "TSTypeParameterInstantiation", "TSConditionalType *"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/consistent-type-assertions": [
|
||||
"error",
|
||||
{
|
||||
"assertionStyle": "as"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/array-type": [
|
||||
"error",
|
||||
{
|
||||
"default": "array"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/restrict-template-expressions": [
|
||||
"off",
|
||||
{
|
||||
"allowNumber": "true"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-this-alias": [
|
||||
"error",
|
||||
{
|
||||
"allowDestructuring": true, // Allow `const { props, state } = this`; false by default
|
||||
"allowedNames": ["self"] // Allow `const self = this`; `[]` by default
|
||||
}
|
||||
],
|
||||
|
||||
"@typescript-eslint/return-await": "off",
|
||||
"@typescript-eslint/dot-notation": "off",
|
||||
"@typescript-eslint/method-signature-style": "off",
|
||||
"@typescript-eslint/no-base-to-string": "off",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/promise-function-async": "off",
|
||||
"@typescript-eslint/no-dynamic-delete": "off",
|
||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-extraneous-class": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
|
||||
"require-await": "off",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
|
||||
// bugged but useful
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
|
||||
// Requires strictNullChecks
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "off",
|
||||
"@typescript-eslint/consistent-indexed-object-style": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
"@typescript-eslint/consistent-type-exports": "off",
|
||||
"@typescript-eslint/key-spacing": "off",
|
||||
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
|
||||
"@typescript-eslint/ban-types": [
|
||||
"error",
|
||||
{
|
||||
"types": {
|
||||
"{}": false,
|
||||
"Function": false
|
||||
},
|
||||
"extendDefaults": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"packages/tests/fixtures",
|
||||
"apps/**/dist",
|
||||
"packages/**/dist",
|
||||
"server/dist",
|
||||
"packages/types-generator/tests",
|
||||
"*.js",
|
||||
"/client",
|
||||
"/dist"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.eslint.json"
|
||||
],
|
||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true
|
||||
}
|
||||
}
|
17
.github/CONTRIBUTING.md
vendored
17
.github/CONTRIBUTING.md
vendored
|
@ -79,7 +79,7 @@ First, you should use a server or PC with at least 4GB of RAM. Less RAM may lead
|
|||
git clone https://github.com/Chocobozzz/PeerTube
|
||||
cd PeerTube
|
||||
git remote add me git@github.com:YOUR_GITHUB_USERNAME/PeerTube.git
|
||||
yarn install --pure-lockfile
|
||||
npm run install-node-dependencies
|
||||
```
|
||||
|
||||
Note that development is done on the `develop` branch. If you want to hack on
|
||||
|
@ -223,8 +223,19 @@ Instance configurations are in `config/test-{1,2,3}.yaml`.
|
|||
|
||||
To test emails with PeerTube:
|
||||
|
||||
* Run [mailslurper](http://mailslurper.com/)
|
||||
* Run PeerTube using mailslurper SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||
* Run [MailDev](https://github.com/maildev/maildev) using Docker
|
||||
* Run PeerTube using MailDev SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||
|
||||
To test all emails without having to run actions manually on the web interface, you can run notification unit tests with environment variables to relay emails to your MailDev instance. For example:
|
||||
|
||||
```sh
|
||||
MAILDEV_RELAY_HOST=localhost MAILDEV_RELAY_PORT=2500 mocha --exit --bail packages/tests/src/api/notifications/comments-notifications.ts
|
||||
```
|
||||
|
||||
You can then go to the MailDev web interface and see how emails look like.
|
||||
|
||||
The admin web interface also have a button to send some email templates to a specific email address.
|
||||
|
||||
|
||||
### Environment variables
|
||||
|
||||
|
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,11 +1,11 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🤷💻🤦 Question/Forum
|
||||
- name: 📱 Mobile Application bug/feature request
|
||||
url: https://framagit.org/framasoft/peertube/mobile-application/-/issues
|
||||
about: Use the mobile application repository to report bugs or request new features
|
||||
- name: 🤷 Question/Forum
|
||||
url: https://framacolibri.org/c/peertube
|
||||
about: You can ask and answer other questions here
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#peertube:matrix.org
|
||||
about: Chat with us via Matrix for quick Q/A here
|
||||
- name: 💬 IRC
|
||||
url: https://web.libera.chat/#peertube
|
||||
about: Chat with us via IRC for quick Q/A here
|
||||
|
|
|
@ -11,32 +11,20 @@ runs:
|
|||
using: "composite"
|
||||
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Install peertube runner dependencies
|
||||
shell: bash
|
||||
run: cd apps/peertube-runner && yarn install --frozen-lockfile
|
||||
|
||||
- name: Install peertube CLI dependencies
|
||||
shell: bash
|
||||
run: cd apps/peertube-cli && yarn install --frozen-lockfile
|
||||
run: npm run install-node-dependencies
|
||||
|
||||
- name: Display PeerTube dependencies
|
||||
shell: bash
|
||||
|
|
222
.github/copilot-instructions.md
vendored
Normal file
222
.github/copilot-instructions.md
vendored
Normal file
|
@ -0,0 +1,222 @@
|
|||
# PeerTube Copilot Instructions
|
||||
|
||||
## Repository Overview
|
||||
|
||||
PeerTube is an open-source, ActivityPub-federated video streaming platform using P2P technology directly in web browsers. It's developed by Framasoft and provides a decentralized alternative to centralized video platforms like YouTube.
|
||||
|
||||
**Repository Stats:**
|
||||
- **Size**: Large monorepo (~350MB, ~15k files)
|
||||
- **Type**: Full-stack web application
|
||||
- **Languages**: TypeScript (backend), Angular (frontend), Shell scripts
|
||||
- **Target Runtime**: Node.js >=20.x, PostgreSQL >=10.x, Redis >=6.x
|
||||
- **Package Manager**: Yarn 1.x (NOT >=2.x)
|
||||
- **Architecture**: Express.js API server + Angular SPA client + P2P video delivery
|
||||
|
||||
## Critical: Client Directory Exclusion
|
||||
|
||||
**🚫 ALWAYS IGNORE `client/` directory** - it contains a separate Angular frontend project with its own build system, dependencies, and development workflow. Focus only on the server-side backend code.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
### Prerequisites (Required)
|
||||
1. **Dependencies**: Node.js >=20.x, Yarn 1.x, PostgreSQL >=10.x, Redis >=6.x, FFmpeg >=4.3, Python >=3.8
|
||||
2. **PostgreSQL Setup**:
|
||||
```bash
|
||||
sudo -u postgres createuser -P peertube
|
||||
sudo -u postgres createdb -O peertube peertube_dev
|
||||
sudo -u postgres psql -c "CREATE EXTENSION pg_trgm;" peertube_dev
|
||||
sudo -u postgres psql -c "CREATE EXTENSION unaccent;" peertube_dev
|
||||
```
|
||||
3. **Services**: Start PostgreSQL and Redis before development
|
||||
|
||||
### Installation & Build (Execute in Order)
|
||||
```bash
|
||||
# 1. ALWAYS install dependencies first (takes ~2-3 minutes)
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
# 2. Build server (required for most operations, takes ~3-5 minutes)
|
||||
npm run build:server
|
||||
|
||||
# 3. Optional: Build full application (takes ~10-15 minutes)
|
||||
npm run build
|
||||
```
|
||||
|
||||
**⚠️ Critical Notes:**
|
||||
- Always run `yarn install --frozen-lockfile` before any build operation
|
||||
- Server build is prerequisite for testing and development
|
||||
- Never use `npm install` - always use `yarn`
|
||||
- Build failures often indicate missing PostgreSQL extensions or wrong Node.js version
|
||||
|
||||
### Development Commands
|
||||
```bash
|
||||
# Server-only development (recommended for backend work)
|
||||
npm run dev:server # Starts server on localhost:9000 with hot reload
|
||||
|
||||
# Full stack development (NOT recommended if only working on server)
|
||||
npm run dev # Starts both server (9000) and client (3000)
|
||||
|
||||
# Development credentials:
|
||||
# Username: root
|
||||
# Password: test
|
||||
```
|
||||
|
||||
### Testing Commands (Execute in Order)
|
||||
```bash
|
||||
# 1. Prepare test environment (required before first test run)
|
||||
sudo -u postgres createuser $(whoami) --createdb --superuser
|
||||
npm run clean:server:test
|
||||
|
||||
# 2. Build (required before testing)
|
||||
npm run build
|
||||
|
||||
# 3. Run specific test suites (recommended over full test)
|
||||
npm run ci -- api-1 # API tests part 1
|
||||
npm run ci -- api-2 # API tests part 2
|
||||
npm run ci -- lint # Linting only
|
||||
npm run ci -- client # Client tests
|
||||
|
||||
# 4. Run single test file
|
||||
npm run mocha -- --exit --bail packages/tests/src/api/videos/single-server.ts
|
||||
|
||||
# 5. Full test suite (takes ~45-60 minutes, avoid unless necessary)
|
||||
npm run test
|
||||
```
|
||||
|
||||
**⚠️ Test Environment Notes:**
|
||||
- Tests require PostgreSQL user with createdb/superuser privileges
|
||||
- Some tests need Docker containers for S3/LDAP simulation
|
||||
- Test failures often indicate missing system dependencies or DB permissions
|
||||
- Set `DISABLE_HTTP_IMPORT_TESTS=true` to skip flaky import tests
|
||||
|
||||
### Validation Commands
|
||||
```bash
|
||||
# Lint code (runs ESLint + OpenAPI validation)
|
||||
npm run lint
|
||||
|
||||
# Validate OpenAPI spec
|
||||
npm run swagger-cli -- validate support/doc/api/openapi.yaml
|
||||
|
||||
# Build server
|
||||
npm run build:server
|
||||
```
|
||||
|
||||
## Project Architecture & Layout
|
||||
|
||||
### Server-Side Structure (Primary Focus)
|
||||
```
|
||||
server/core/
|
||||
├── controllers/api/ # Express route handlers (add new endpoints here)
|
||||
│ ├── index.ts # Main API router registration
|
||||
│ ├── videos/ # Video-related endpoints
|
||||
│ └── users/ # User-related endpoints
|
||||
├── models/ # Sequelize database models
|
||||
│ ├── video/ # Video, channel, playlist models
|
||||
│ └── user/ # User, account models
|
||||
├── lib/ # Business logic services
|
||||
│ ├── job-queue/ # Background job processing
|
||||
│ └── emailer.ts # Email service
|
||||
├── middlewares/ # Express middleware
|
||||
│ ├── validators/ # Input validation (always required)
|
||||
│ └── auth.ts # Authentication middleware
|
||||
├── helpers/ # Utility functions
|
||||
└── initializers/ # App startup and constants
|
||||
```
|
||||
|
||||
### Key Configuration Files
|
||||
- `package.json` - Main dependencies and scripts
|
||||
- `server/package.json` - Server-specific config
|
||||
- `eslint.config.mjs` - Linting rules
|
||||
- `tsconfig.base.json` - TypeScript base config
|
||||
- `config/default.yaml` - Default app configuration
|
||||
- `.mocharc.cjs` - Test runner configuration
|
||||
|
||||
### Shared Packages (`packages/`)
|
||||
```
|
||||
packages/
|
||||
├── models/ # Shared TypeScript interfaces (modify for API changes)
|
||||
├── core-utils/ # Common utilities
|
||||
├── ffmpeg/ # Video processing
|
||||
├── server-commands/ # Test helpers
|
||||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
### Scripts Directory (`scripts/`)
|
||||
- `scripts/build/` - Build automation
|
||||
- `scripts/dev/` - Development helpers
|
||||
- `scripts/ci.sh` - Continuous integration runner
|
||||
- `scripts/test.sh` - Test runner
|
||||
|
||||
## Continuous Integration Pipeline
|
||||
|
||||
**GitHub Actions** (`.github/workflows/test.yml`):
|
||||
1. **Matrix Strategy**: Tests run in parallel across different suites
|
||||
2. **Required Services**: PostgreSQL, Redis, LDAP, S3, Keycloak containers
|
||||
3. **Test Suites**: `types-package`, `client`, `api-1` through `api-5`, `transcription`, `cli-plugin`, `lint`, `external-plugins`
|
||||
4. **Environment**: Ubuntu 22.04, Node.js 20.x
|
||||
5. **Typical Runtime**: 15-30 minutes per suite
|
||||
|
||||
**Pre-commit Checks**: ESLint, TypeScript compilation, OpenAPI validation
|
||||
|
||||
## Making Code Changes
|
||||
|
||||
### Adding New API Endpoint
|
||||
1. Create controller in `server/core/controllers/api/`
|
||||
2. Add validation middleware in `server/core/middlewares/validators/`
|
||||
3. Register route in `server/core/controllers/api/index.ts`
|
||||
4. Update shared types in `packages/models/`
|
||||
5. Add OpenAPI documentation tags
|
||||
6. Write tests in `packages/tests/src/api/`
|
||||
|
||||
### Common Patterns to Follow
|
||||
```typescript
|
||||
// Controller pattern
|
||||
import express from 'express'
|
||||
import { apiRateLimiter, asyncMiddleware } from '../../middlewares/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
router.use(apiRateLimiter) // ALWAYS include rate limiting
|
||||
|
||||
router.get('/:id',
|
||||
validationMiddleware, // ALWAYS validate inputs
|
||||
asyncMiddleware(handler) // ALWAYS wrap async handlers
|
||||
)
|
||||
```
|
||||
|
||||
### Database Changes
|
||||
1. Create/modify Sequelize model in `server/core/models/`
|
||||
2. Generate migration in `server/core/initializers/migrations/`
|
||||
3. Update shared types in `packages/models/`
|
||||
4. Run `npm run build:server` to compile
|
||||
|
||||
## Validation Steps Before PR
|
||||
|
||||
1. **Build**: `npm run build` (must succeed)
|
||||
2. **Lint**: `npm run lint` (must pass without errors)
|
||||
5. **OpenAPI**: Validate if API changes made
|
||||
|
||||
## Common Error Solutions
|
||||
|
||||
**Build Errors:**
|
||||
- "Cannot find module": Run `yarn install --frozen-lockfile`
|
||||
- "PostgreSQL connection": Check PostgreSQL is running and extensions installed
|
||||
- TypeScript errors: Check Node.js version (must be >=20.x)
|
||||
|
||||
**Test Errors:**
|
||||
- Permission denied: Ensure PostgreSQL user has createdb/superuser rights
|
||||
- Port conflicts: Stop other PeerTube instances
|
||||
- Import test failures: Set `DISABLE_HTTP_IMPORT_TESTS=true`
|
||||
|
||||
**Development Issues:**
|
||||
- "Client dist not found": Run `npm run build:client` (only if working on client features)
|
||||
- Redis connection: Ensure Redis server is running
|
||||
- Hot reload not working: Kill all Node processes and restart
|
||||
|
||||
## Trust These Instructions
|
||||
|
||||
These instructions have been validated against the current codebase. Only search for additional information if:
|
||||
- Commands fail with updated error messages
|
||||
- New dependencies are added to package.json
|
||||
- Build system changes are detected
|
||||
- You need specific implementation details not covered here
|
||||
|
||||
Focus on server-side TypeScript development in `server/core/` and ignore the `client/` directory unless explicitly working on frontend integration.
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
# FIXME: https://github.com/actions/checkout/issues/290
|
||||
git fetch --force --tags
|
||||
|
||||
one="{ \"build-peertube\": true, \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bookworm\" }"
|
||||
one="{ \"build-peertube\": true, \"file\": \"./support/docker/production/Dockerfile\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-trixie,chocobozzz/peertube:develop\" }"
|
||||
two="{ \"build-peertube\": true, \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-bookworm,chocobozzz/peertube:$(git describe --abbrev=0)-bookworm\" }"
|
||||
three="{ \"build-peertube\": false, \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
|
||||
|
||||
|
|
2
.github/workflows/stats.yml
vendored
2
.github/workflows/stats.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
run: |
|
||||
wget "https://github.com/boyter/scc/releases/download/v3.0.0/scc-3.0.0-x86_64-unknown-linux.zip"
|
||||
unzip "scc-3.0.0-x86_64-unknown-linux.zip"
|
||||
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,yarn.lock,client/yarn.lock,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,packages/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
||||
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,pnpm-lock.yaml,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,packages/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
||||
|
||||
- name: PeerTube client stats
|
||||
if: github.event_name != 'pull_request'
|
||||
|
|
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
@ -38,6 +38,14 @@ jobs:
|
|||
ports:
|
||||
- 9444:9000
|
||||
|
||||
keycloak:
|
||||
image: chocobozzz/peertube-tests-keycloak
|
||||
ports:
|
||||
- 8082:8080
|
||||
env:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
@ -111,7 +119,7 @@ jobs:
|
|||
) || \
|
||||
echo "parse-log.js script does not exist, skipping."
|
||||
|
||||
- name: Upload logs
|
||||
- name: Upload logs and database
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,9 +1,7 @@
|
|||
# NPM instalation
|
||||
node_modules
|
||||
*npm-debug.log
|
||||
yarn-error.log
|
||||
*-ci.log
|
||||
.yarn
|
||||
|
||||
# Testing
|
||||
/test1/
|
||||
|
|
260
CHANGELOG.md
260
CHANGELOG.md
|
@ -1,13 +1,269 @@
|
|||
# Changelog
|
||||
|
||||
## v7.3.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Minimum supported NodeJS version is `20.19`
|
||||
|
||||
### NGINX
|
||||
|
||||
* Disable request buffering on upload endpoints to fix HTTP request timeouts: https://github.com/Chocobozzz/PeerTube/commit/d1a35e8421195088e2754b787c4af1e765b9eaa9
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* **Breaking change** Plugin and themes must use `:root` CSS selector instead of `body` to inject CSS variables
|
||||
* Add server API (https://docs.joinpeertube.org/api/plugins):
|
||||
* Support `externalRedirectUri` for `registerExternalAuth` so PeerTube redirects users on another URL set by the plugin
|
||||
* If your plugin uses `filter:email.template-path.result` server hook: emails now use Handlebars template engine instead of Pug template engine
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: Emails can now be translated :tada: Check the [translation documentation](https://docs.joinpeertube.org/support/doc/translation) to help us translate emails in your language!
|
||||
* :tada: Introduce a web configuration wizard to help administrators to configure their instance automatically :tada:
|
||||
* The wizard appears once the administrators have logged in following the installation of the PeerTube instance
|
||||
* Admins can also run the wizard via a button in the web admin config
|
||||
* The main instance information (e.g. name, short description, logo, primary colour) can be entered using the wizard.
|
||||
* It also helps the admin to apply a configuration depending on the instance type (community-based, institutional, private)
|
||||
* :tada: Redesign the admin config to use a lateral menu for navigating between subsections :tada:
|
||||
* Add a new *Customization* page to easily change the main colors and shape of the client interface
|
||||
* Add a new *Logo* page where admins can upload logos/favicon and social media images for their instances
|
||||
* Add an option to set the default licence, privacy and comments policy when publishing videos
|
||||
* The email prefix and body can now be changed in the web admin config. These configurations also support the `{{instanceName}}` template variable, which is replaced by the instance name
|
||||
* Improve admin federation control:
|
||||
* Add the ability for admins to completely disable remote subscriptions to local channels
|
||||
* Admins can also set up automatic rejection of video comments from remote instances
|
||||
* Add 2FA column information in admin users overview table
|
||||
* Display remote runner version in admin
|
||||
* Add ability for users to set the planned date of a live. These lives are displayed when browsing videos [#7144](https://github.com/Chocobozzz/PeerTube/pull/7144)
|
||||
* Improve data tables UX/UI
|
||||
* Improve account/channel playlists management:
|
||||
* Use a data table to manage account and channel playlists
|
||||
* Allow to manually set the order of the public playlists displayed in a channel
|
||||
* Improve sensitive content warning in embed player
|
||||
* Improve audio transcoding quality, especially with FLAC input
|
||||
* Support Creole French languages in video language metadata
|
||||
* Add ability for users to list and revoke token sessions
|
||||
* Support *Free of known copyright restrictions* and *Copyrighted - All Rights Reserved* video licence metadata
|
||||
* Play/pause the video player using `k` key
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix ActivityPub audience for unlisted videos
|
||||
* Use an array of URL in `attributedTo` ActivityPub field
|
||||
* Prefer `og:image` instead of `og:image:url`
|
||||
* Better thumbnail blur for sensitive content [#7105](https://github.com/Chocobozzz/PeerTube/pull/7105)
|
||||
* Prefer `allow="fullscreen"` for video embed `iframe` [#7043](https://github.com/Chocobozzz/PeerTube/pull/7043)
|
||||
* Respect the sensitive content policy, even for videos owned by the user
|
||||
* Fix the issue of the scroll position not being restored when pages load slowly [#7143](https://github.com/Chocobozzz/PeerTube/pull/7143)
|
||||
* Fix remote actor follow counter after a local subscription
|
||||
* Fix reloading videos in *Browser videos* when the link only changes query parameters
|
||||
* Add stall job check for remote studio and transcription runner jobs
|
||||
* Prevent metric warning for redundancy gauge
|
||||
* Fix disabling *Wait transcoding* checkbox
|
||||
* Correctly import new elements of a playlist in channel synchronization
|
||||
* Fix overflow in discover page
|
||||
* Fix restoring scroll position when going back in the web browser on the homepage set by the admin
|
||||
* Fill video support on channel sync
|
||||
* Respect instance default privacy setting when publishing imports and lives
|
||||
* Remove useless help for live transcoding
|
||||
* Fix RTL margins on some components
|
||||
|
||||
|
||||
## v7.2.3
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Upgrade `multer` dependency to prevent Denial of Service with a malformed request
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix channel synchronization that duplicates video imports
|
||||
|
||||
|
||||
## v7.2.2
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Prevent ReDOS from `useragent` package by removing deprecated Do Not Track feature. Thanks to Patrick Bohn Matthiesen and [Leonora](https://github.com/herover) from IT University of Copenhagen for reporting this vulnerability!
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Correctly display bulk actions button in "My videos"
|
||||
* Keep playlist name original casing in "My videos"
|
||||
* Fix PIP button z-index on Firefox
|
||||
* More robust S3 upload and ACL error handler
|
||||
* Fix broken video state on S3 move failure
|
||||
* Reset filters when loading query params in "Browse videos"
|
||||
* Fix upload tab title when the file is uploaded
|
||||
* Fix follow card overflow in about page
|
||||
* Convert to full UUID request param `id` in `filter:html.embed.video.allowed.result` and `filter:html.embed.video-playlist.allowed.result` plugin hooks
|
||||
* Fix HLS playback issue on Chrome 138
|
||||
* Fix selecting frame on Safari
|
||||
* Fix input search with multiple prefix tokens
|
||||
* Fix channel sync duplicate after video deletion
|
||||
* Fix caption raw edition when editing segment
|
||||
* Fix accessibility issues:
|
||||
* Fix embed title/avatar accessibility
|
||||
* Add player P2P up/down info aria label
|
||||
* Support escape key in the player settings menu
|
||||
* Support arrow left/right navigation in the settings menu
|
||||
* Fix entry focus when navigating in the settings menu
|
||||
* Add aria controls attribute to settings button
|
||||
* Thanks to [Woebin](https://github.com/Woebin) from [Access Lab](https://axesslab.com/) and [HowlRound Theatre Commons](https://howlround.com/) for conducting the player accessibility audit!
|
||||
|
||||
|
||||
## v7.2.1
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix federation of sensitive videos with previous PeerTube versions
|
||||
* Do not uppercase video tags to prevent accessibility issues
|
||||
* Fix support field not automatically filled from channel data when publishing a video
|
||||
* Fix "Add new playlist" broken style
|
||||
* Fix browse videos page title on web browser "History Back"
|
||||
* Fix parent menu highlighting in *About Platform* pages
|
||||
* Don't display description/terms titles if these blocks are empty
|
||||
* Correctly load count and rows per page when listing *My videos*
|
||||
|
||||
|
||||
## v7.2.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* **Important** You need to manually execute a migration script after your upgrade while PeerTube is running and the database migration is complete (`Migrations finished. New migration version schema: xxx` in PeerTube startup logs):
|
||||
* Classic installation: `cd /var/www/peertube/peertube-latest && sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production node dist/scripts/migrations/peertube-7.2.js`
|
||||
* Docker installation: `cd /var/www/peertube-docker && docker compose exec -u peertube peertube node dist/scripts/migrations/peertube-7.2.js`
|
||||
|
||||
### SECURITY
|
||||
|
||||
* If you installed PeerTube using the [official documentation](https://docs.joinpeertube.org/install/any-os#installation), we highly recommend setting the default user shell to `nologin`. For example on GNU/Linux: `chsh -s /usr/sbin/nologin peertube`
|
||||
* If you installed PeerTube runners using the [official Systemd service documentation](https://docs.joinpeertube.org/maintain/tools#as-a-systemd-service), we highly recommend setting the default user shell to `nologin`. For example on GNU/Linux: `chsh -s /usr/sbin/nologin prunner`
|
||||
|
||||
### Configuration
|
||||
|
||||
* Prefer to not store lives in object storage by default: `object_storage.streaming_playlists.store_live_streams` is now `false` in the config template
|
||||
* Use `hot` trending algorithm by default: `trending.videos.default` is now `hot` in the config template
|
||||
* Add global rate limit to video download that can be changed by `download_generate_video.max_parallel_downloads`
|
||||
|
||||
### Docker
|
||||
|
||||
* Add missing docker env options to configure live settings [#6948](https://github.com/Chocobozzz/PeerTube/pull/6948)
|
||||
* Expose NGINX logs folder in `docker-compose.yml` [#6963](https://github.com/Chocobozzz/PeerTube/pull/6963)
|
||||
* Add exec to NGINX process to ensure is PID 1 and then ensure a graceful shutdown[#7041](https://github.com/Chocobozzz/PeerTube/pull/7041)
|
||||
|
||||
### NGINX
|
||||
|
||||
* Fix max body size inconsistency with PeerTube backend: https://github.com/Chocobozzz/PeerTube/commit/a2812e40d90619528a6b2a4c491640a9737f8f3c
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* **Breaking change** Theme CSS must include `--is-dark: 0` or `--is-dark: 1` CSS variable for the `body` so PeerTube understands if it's a dark or a light theme
|
||||
* Add server plugin hooks (https://docs.joinpeertube.org/api/plugins):
|
||||
* `filter:email.subject.result` & `filter:email.template-path.result` [#6876](https://github.com/Chocobozzz/PeerTube/pull/6876)
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: Redesign *Manage my videos* page :tada:
|
||||
* Redesign the page to list more videos for a clearer overview
|
||||
* Add sort, pagination and column display settings
|
||||
* Add channel buttons to quickly filter videos
|
||||
* Improve video search & filters
|
||||
* Add ability to display video comments count [#6635](https://github.com/Chocobozzz/PeerTube/pull/6635)
|
||||
* :tada: Redesign video management/publication pages :tada:
|
||||
* Migrate the video update page to a *Manage video* tool, that includes *Studio* and *Stats* features
|
||||
* Video publication privacy choice is moved in the second step
|
||||
* Use a lateral menu to navigate between *Manage video* pages
|
||||
* Add information related to the video state (transcoding, etc.) and clearly display unavailable features
|
||||
* Add user agent stats to video stats [#6871](https://github.com/Chocobozzz/PeerTube/pull/6871)
|
||||
* Support drag-and-drop to replace the video file [#6970](https://github.com/Chocobozzz/PeerTube/pull/6970)
|
||||
* :tada: Improve NSFW/sensitive content system :tada:
|
||||
* Support content warning so video authors can describe why the video is considered sensitive
|
||||
* Change the *Blur* sensitive content policy for viewers where the miniature name is not blurred anymore
|
||||
* Add an additional *Warn* sensitive content policy for viewers where the thumbnail is not blurred
|
||||
* *Blur* and *Warn* policies add a *Sensitive* icon below the thumbnail. A warning is also displayed in the player
|
||||
* The player embed now displays the sensitive content warning
|
||||
* Add ability to set predefined sensitive flags to videos so video authors help to identify specific sensitive content
|
||||
* If enabled by administrators, users can override their default sensitive content policy for specific flags.
|
||||
For example, they can hide all sensitive content but display with a warning content flagged as *Violent* by video authors
|
||||
* Add sensitive content filter in admin videos overview
|
||||
* Allow users to resend the email verification link when changing their current email
|
||||
* Inject subtitle links in HLS playlists so it's easier for external video players that use the `master.m3u8` playlist to display subtitles
|
||||
* Disable log coloration when TTY does not support it [#6988](https://github.com/Chocobozzz/PeerTube/pull/6988)
|
||||
* Support more embed parameters in custom markup (`<peertube-video-embed>` and `<peertube-playlist-embed>`) [#6989](https://github.com/Chocobozzz/PeerTube/pull/6989)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* More robust theme CSS variables injection
|
||||
* Fix podcast feed URL in subscribe button
|
||||
* Fix podcast feed download extension when the file is a video
|
||||
* Fix broken downloaded audio file
|
||||
* Fix crash on download stream error
|
||||
* Fix local posts counter in NodeInfo
|
||||
* Run transcription after file replacement
|
||||
* Better video chapters parsing from the description
|
||||
* Correctly handle `generateTranscription` body param on upload/import
|
||||
* Fix federation compatibility with GoToSocial
|
||||
* Fix PeerTube account client redirection
|
||||
* Prevent plugins to log exceptions
|
||||
* Fix broken replay on live privacy change
|
||||
* Fix iOS/Android deep link with URL that contains query params in watch page
|
||||
* Fix ownership changes count
|
||||
* Always specify object storage content type
|
||||
* Fix broken live title in Chinese
|
||||
* Fix theme crash in embed
|
||||
* Fix broken video state on move on object storage failure
|
||||
* Fix CORS issue with object storage providers
|
||||
* Correctly display images in support modal
|
||||
|
||||
|
||||
## v7.1.1
|
||||
|
||||
### SECURITY
|
||||
|
||||
This release fixes important vulnerabilities discovered by Ori Hollander of the JFrog Vulnerability Research team. Many thanks to them!
|
||||
|
||||
* Fix DoS and blind SSRF on ActivityPub playlist creation [CVE-2025-32948](https://research.jfrog.com/vulnerabilities/peertube-activitypub-playlist-creation-blind-ssrf-dos/)
|
||||
* Prevent infinite loop DoS when crawling ActivityPub data [CVE-2025-32947](https://research.jfrog.com/vulnerabilities/peertube-activitypub-crawl-dos/)
|
||||
* Prevent an attacker from adding playlists to a another user's channel using the ActivityPub [CVE-2025-32946](https://research.jfrog.com/vulnerabilities/peertube-arbitrary-playlist-creation-activitypub/)
|
||||
* Prevent an attacker from adding playlists to a another user's channel using the REST API [CVE-2025-32945](https://research.jfrog.com/vulnerabilities/peertube-arbitrary-playlist-creation-rest/)
|
||||
* Add protection against [ZIP bomb](https://en.wikipedia.org/wiki/Zip_bomb) on user import [CVE-2025-32949](https://research.jfrog.com/vulnerabilities/peertube-archive-resource-exhaustion/)
|
||||
* Prevent crash on user import with a ZIP containg an illegal filename [CVE-2025-32944](https://research.jfrog.com/vulnerabilities/peertube-archive-persistent-dos/)
|
||||
* Do not leak private HLS playlists (`.m3u8` files) [CVE-2025-32943](https://research.jfrog.com/vulnerabilities/peertube-hls-path-traversal/)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix playlist page margins
|
||||
* Fix danger button border
|
||||
* Fix unsubscribe button label for channels
|
||||
* Fix remote subscribe on iOS
|
||||
* Add Podcast feed to subscribe button
|
||||
* Always display technical information tab in *About* page
|
||||
* Fix menu button auto font-size to prevent overflow in some locales
|
||||
* Correctly inject multiple `rel="me"` links with supported markdown fields
|
||||
* Fix adding studio watermark with audio/video split HLS file
|
||||
* Reset video state on studio failure
|
||||
* Fix updating a user in administration
|
||||
* Fix error when getting a S3 object with some S3 providers
|
||||
* Specify charset when uploading caption files in S3
|
||||
* Fix theme color parsing with some web browsers
|
||||
* Improve channel description in custom markup miniature
|
||||
* Ensure ffmpeg process is killed if download is aborted
|
||||
* Correctly reload playlist on playlist change in watch page
|
||||
* Use `indexifembedded` in embeds instead of `noindex`
|
||||
* Fix extra space on links of remote comments
|
||||
* Don't convert webp images to jpeg
|
||||
|
||||
|
||||
## v7.1.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Remove NodeJS 18 support. Please upgrade to NodeJS 20 before upgrading PeerTube
|
||||
* Remove NodeJS 18 support. Please upgrade to NodeJS 20 (>= 20.9) before upgrading PeerTube
|
||||
* Due to a bug in the remote video thumbnail update, we recommend running the [prune storage](https://docs.joinpeertube.org/maintain/tools#prune-filesystem-object-storage) script to clean up the filesystem
|
||||
* Let's encrypt is removing [OCSP support in 2025](https://letsencrypt.org/2024/12/05/ending-ocsp/), so remove SSL stapling from your nginx configuration: https://github.com/Chocobozzz/PeerTube/commit/0abaaa8ccbce19deb6fcd09c8bf00d4cf4248505
|
||||
* Safari desktop versions < 14 are not supported anymore
|
||||
* If you are using object storage, you will need to create the captions bucket or configure PeerTube to use an existing one [in the configuration file](https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L262) or using environment variables if you use Docker (`PEERTUBE_OBJECT_STORAGE_CAPTIONS_BUCKET_NAME`, `PEERTUBE_OBJECT_STORAGE_CAPTIONS_PREFIX`, `PEERTUBE_OBJECT_STORAGE_CAPTIONS_BASE_URL`)
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
|
@ -53,7 +309,7 @@
|
|||
|
||||
* :tada: Redesign *About Platform*, *About PeerTube* and *About Network* pages :tada:
|
||||
* Highlight author host in video miniature using a new dropdown component that explains where the content is coming from
|
||||
* Add ability to put video captions in object storage
|
||||
* Add ability to put video captions in object storage. Use the [CLI](https://docs.joinpeertube.org/maintain/tools#move-video-files-from-filesystem-to-object-storage) after the upgrade to move existing captions to object storage
|
||||
* Add ability for [Mastodon to verify](https://joinmastodon.org/verification) PeerTube links
|
||||
* Enable viewer protocol V2 for better [concurrent viewer scalability](https://joinpeertube.org/news/stress-test-2023)
|
||||
* Add ability for admins to set the default player auto play behaviour [#6167](https://github.com/Chocobozzz/PeerTube/pull/6788)
|
||||
|
|
145
CREDITS.md
145
CREDITS.md
|
@ -3,20 +3,23 @@
|
|||
* Chocobozzz
|
||||
* Rigel Kent
|
||||
* DignifiedSilence
|
||||
* Александр
|
||||
* T.S
|
||||
* josé m
|
||||
* Александр
|
||||
* Hồ Nhất Duy
|
||||
* Jeff Huang
|
||||
* josé m
|
||||
* Milo Ivir
|
||||
* Ihor Hordiichuk
|
||||
* Filip Bengtsson
|
||||
* fran secs
|
||||
* kontrollanten
|
||||
* Payman Moghadam
|
||||
* Berto Te
|
||||
* kontrollanten
|
||||
* Milo Ivir
|
||||
* Simon Brosdetzko
|
||||
* Jiri Podhorecky
|
||||
* Phongpanot
|
||||
* Sveinn í Felli
|
||||
* Hannes Ylä-Jääski
|
||||
* GunChleoc
|
||||
* hecko
|
||||
* Laurent Ettouati
|
||||
|
@ -24,65 +27,76 @@
|
|||
* Zet
|
||||
* Ewout van Mansom
|
||||
* Aitor Salaberria
|
||||
* Sveinn í Felli
|
||||
* Leif-Jöran Olsson
|
||||
* Clemens Schielicke
|
||||
* Luca Calcaterra
|
||||
* Racida S
|
||||
* Marcin Mikołajczak
|
||||
* Eivind Ødegård
|
||||
* Balázs Meskó
|
||||
* Tirifto
|
||||
* Marcin Mikołajczak
|
||||
* Wicklow
|
||||
* Blood Axe
|
||||
* Eivind Ødegård
|
||||
* John Livingston
|
||||
* Hannes Ylä-Jääski
|
||||
* Kim
|
||||
* Tirifto
|
||||
* Besnik Bleta
|
||||
* Kim
|
||||
* Vodoyo Kamal
|
||||
* Jiří Podhorecký
|
||||
* Armin
|
||||
* Fontan 030
|
||||
* ButterflyOfFire
|
||||
* Mohamad Reza
|
||||
* Quentin PAGÈS
|
||||
* Kimsible
|
||||
* Felix Ableitner
|
||||
* Frank Sträter
|
||||
* Free coss
|
||||
* Ettore Atalan
|
||||
* Andrea Gavioli
|
||||
* Mürteza MERT
|
||||
* Gérald Niel
|
||||
* ButterflyOfFire
|
||||
* Duy
|
||||
* Eric Guichaoua
|
||||
* Renne Rocha
|
||||
* Slimane Selyan AMIRI
|
||||
* Dingzhong Chen
|
||||
* Eric Guichaoua
|
||||
* Filip Hanes
|
||||
* Julien Maulny
|
||||
* Mark Van den Borre
|
||||
* x
|
||||
* Booteille
|
||||
* Manuel Viens
|
||||
* Jorropo
|
||||
* Josh Morel
|
||||
* Renne Rocha
|
||||
* dxuser514
|
||||
* BO41
|
||||
* Ettore Atalan
|
||||
* Marc Strange
|
||||
* vachan
|
||||
* AP
|
||||
* Elegant Codes
|
||||
* Florian CUNY
|
||||
* Francesc
|
||||
* alex gabilondo
|
||||
* mando laress
|
||||
* Ľubomír Šima
|
||||
* Артём Котлубай
|
||||
* Fjuro
|
||||
* Ricardo Biloti
|
||||
* 0que
|
||||
* Blood Axe
|
||||
* Cedric F
|
||||
* Florent
|
||||
* Marc Strange
|
||||
* Ricardo Simões
|
||||
* lutangar
|
||||
* Ch
|
||||
* J. Lavoie
|
||||
* Luc Didry
|
||||
* YILDIRIM YAPRAK
|
||||
* alex gabilondo
|
||||
* barzofarev2
|
||||
* jan Seli
|
||||
* 李奕寯
|
||||
* Erik Guldberg
|
||||
* Kempelen
|
||||
* Kerim Demirkaynak
|
||||
* Martin Hoefler
|
||||
* Porrumentzio
|
||||
* Poslovitch
|
||||
|
@ -91,67 +105,75 @@
|
|||
* Alexander Ivanov
|
||||
* Balázs Úr
|
||||
* Echo Kilo
|
||||
* Erik Guldberg
|
||||
* Jan Keromnes
|
||||
* Jiří Podhorecký
|
||||
* Luc Didry
|
||||
* Siourdakis Thanos
|
||||
* Thomas Citharel
|
||||
* knuxify
|
||||
* tray
|
||||
* Adrià Martín
|
||||
* Agron Selimaj
|
||||
* Attila F
|
||||
* Caroline Chuong
|
||||
* David Soh
|
||||
* Diazepan Medina
|
||||
* Jason Zhou
|
||||
* Kerim Demirkaynak
|
||||
* Loukas Stamellos
|
||||
* Ms Kimsible
|
||||
* NorbiPeti
|
||||
* Sergey Zigachev
|
||||
* Thomas Citharel
|
||||
* Txopi
|
||||
* Benjamin Bouvier
|
||||
* Filip Hanes
|
||||
* Cavernosa
|
||||
* Ghost of Sparta
|
||||
* Joe Bill
|
||||
* Julien
|
||||
* Jure Repinc
|
||||
* Kemal Oktay Aktoğan
|
||||
* Lucas Declercq
|
||||
* Ryan He
|
||||
* Sirxy
|
||||
* Viorel-Cătălin Răpițeanu
|
||||
* matograine
|
||||
* Adrià Martín
|
||||
* 偶尔来巡山
|
||||
* Ahmed ABERWAG
|
||||
* Daniel Santos
|
||||
* David Libeau
|
||||
* Ewald Arnold
|
||||
* Florent F
|
||||
* Florent Poinsaut
|
||||
* Ignacio Carrera González
|
||||
* Jayme Soares Almeida Cruz
|
||||
* Lety Does Stuff
|
||||
* Nassim Bounouas
|
||||
* Rafael Fontenelle
|
||||
* Thomas Kuntz
|
||||
* Tzafrir Cohen
|
||||
* Viorel-Cătălin Răpițeanu
|
||||
* Vri
|
||||
* miro
|
||||
* nexi
|
||||
* owiox8+1viroxeaziaxw@sharklasers.com
|
||||
* spf
|
||||
* wazakovsky
|
||||
* yns bag
|
||||
* Anne-Gaelle Moulun
|
||||
* Arman
|
||||
* Asier Iturralde Sarasola
|
||||
* BRAINS YUM
|
||||
* Belkacem Mohammed
|
||||
* Bob Oob
|
||||
* Côme 744
|
||||
* Dimitri Gilbert
|
||||
* Flavio F. M
|
||||
* Florent Poinsaut
|
||||
* Frank Chang
|
||||
* Green-Star
|
||||
* I_Automne
|
||||
* Ilia
|
||||
* Marek Ľach
|
||||
* Micah Elizabeth Scott
|
||||
* Pierre-Jean
|
||||
* Ret Samys
|
||||
* SVNET Libre
|
||||
* StarAtt
|
||||
* Tomasz
|
||||
* Tony Simoes
|
||||
* William Lahti
|
||||
|
@ -160,18 +182,18 @@
|
|||
* boris joeson
|
||||
* frankstrater
|
||||
* mater
|
||||
* spf
|
||||
* test2a
|
||||
* think4web
|
||||
* 路过是好事
|
||||
* Ajeje Brazorf
|
||||
* Andreas Grupp
|
||||
* Andrey
|
||||
* Angristan
|
||||
* Benjamin Seitz
|
||||
* Bob Oob
|
||||
* Booteille
|
||||
* Cirnos
|
||||
* Cokelat8
|
||||
* DontUseGithub
|
||||
* Eder Etxebarria
|
||||
* Farooq Karimi Zadeh
|
||||
* Frederic Bezies
|
||||
* Iñigo
|
||||
|
@ -180,24 +202,30 @@
|
|||
* José M
|
||||
* Kristoffer Grundström
|
||||
* LecygneNoir
|
||||
* Liu Zhiyu
|
||||
* Lukas
|
||||
* MahdiTurki
|
||||
* Martijn Dekker
|
||||
* Mats Blomdahl
|
||||
* Maxime Louet
|
||||
* Mildred
|
||||
* Murat Hasdemir
|
||||
* Murat Özalp
|
||||
* Nikolay
|
||||
* Okhin
|
||||
* Osama
|
||||
* Pierre-Alain TORET
|
||||
* Serge Victor
|
||||
* Théo Le Calvar
|
||||
* Ugaitz
|
||||
* Vaclovas Intas
|
||||
* Vincent Finance
|
||||
* aschaap
|
||||
* clementbrizard
|
||||
* gohoso9454
|
||||
* helabasa
|
||||
* kaiyou
|
||||
* max
|
||||
* roberto marcolin
|
||||
* Ahsan Haris Ahmed
|
||||
* Alberto Teira
|
||||
|
@ -212,11 +240,12 @@
|
|||
* Asr128
|
||||
* Aurélien Bertron
|
||||
* Axel Viala
|
||||
* Casper Ruttten
|
||||
* Charles-Edouard Gervais
|
||||
* Danail Emandiev
|
||||
* Daniele Garau
|
||||
* Dep Pranata
|
||||
* Dirk Kelly
|
||||
* Eder Etxebarria
|
||||
* Ehsan Gholami
|
||||
* Elga Ahmad Prayoga
|
||||
* Girish Ramakrishnan
|
||||
|
@ -237,13 +266,14 @@
|
|||
* Lukas Winkler
|
||||
* M Z
|
||||
* Manuela Silva
|
||||
* Marian
|
||||
* Morpheus Tao
|
||||
* Mélanie Chauvel
|
||||
* Natsuki Tsukishiro
|
||||
* Paolo Mauri
|
||||
* Pedro
|
||||
* Petr Balíček
|
||||
* Piotr Sikora
|
||||
* Ryan He
|
||||
* Stardream
|
||||
* Stefan Keks
|
||||
* Tom Wellington
|
||||
|
@ -259,8 +289,10 @@
|
|||
* h3n3
|
||||
* iapellaniz
|
||||
* jonathanraes
|
||||
* legiorange
|
||||
* numéro6
|
||||
* saleh oukiki
|
||||
* Àngel Pérez Beroy
|
||||
* Ömer Faruk Çakmak
|
||||
* AQR_Rastiq
|
||||
* Al-Hassan Abdel-Raouf
|
||||
|
@ -277,13 +309,14 @@
|
|||
* Average Dude
|
||||
* BGR2
|
||||
* BitTube
|
||||
* Boo
|
||||
* Boo Teille
|
||||
* Branislav Pavelka
|
||||
* Casper Ruttten
|
||||
* Dashie
|
||||
* David Luís Pereira Pires
|
||||
* David Marzal
|
||||
* Doug Luce
|
||||
* Emv
|
||||
* EndoGai
|
||||
* Fatih Özsoy
|
||||
* FediverseTV
|
||||
|
@ -304,6 +337,8 @@
|
|||
* Jan Hartig
|
||||
* Jan Marsalek
|
||||
* Jerguš Fonfer
|
||||
* Jeroen de Wijn
|
||||
* José Daniel Angulo Plata
|
||||
* Joël Galeran
|
||||
* Julien Lemaire
|
||||
* Julien Rabier
|
||||
|
@ -314,11 +349,12 @@
|
|||
* Mondo Xíbaro
|
||||
* Moritz Warning
|
||||
* Mostafa Ahangarha
|
||||
* Murat Özalp
|
||||
* Neko Nekowazarashi
|
||||
* Nicolai Larsen
|
||||
* Nojus
|
||||
* Olivier Bouillet
|
||||
* Pedro hates github.com
|
||||
* Pep
|
||||
* Pierre Jaury
|
||||
* Piotr Strębski
|
||||
* Puryx
|
||||
|
@ -328,19 +364,24 @@
|
|||
* SerCom_KC
|
||||
* Skid
|
||||
* Stakovicz
|
||||
* Suthep
|
||||
* Takeshi Umeda
|
||||
* Thai Localization
|
||||
* The Cashew Trader
|
||||
* Thijs Kinkhorst
|
||||
* Timur Seber
|
||||
* Toso Malero
|
||||
* Tsuki
|
||||
* Túlio Simões Martins Padilha
|
||||
* Valvin
|
||||
* XblateX
|
||||
* Yaron Shahrabani
|
||||
* YiDai
|
||||
* Yogesh K S
|
||||
* ahmadsharifian
|
||||
* bopol
|
||||
* brucekomike
|
||||
* darek
|
||||
* dingycle
|
||||
* framail
|
||||
* imgradeone Yan
|
||||
|
@ -348,13 +389,13 @@
|
|||
* les
|
||||
* libertas
|
||||
* merty
|
||||
* ou jian bo
|
||||
* plr20
|
||||
* q_h
|
||||
* qwerty
|
||||
* taziden
|
||||
* vancha march
|
||||
* victor héry
|
||||
* Àngel Pérez Beroy
|
||||
* 3risian
|
||||
* A.D.R.S
|
||||
* Acid Chicken (硫酸鶏)
|
||||
|
@ -368,6 +409,7 @@
|
|||
* Alberto Mardegan
|
||||
* Alejandro Criado-Pérez
|
||||
* Aleksandr Sokolov
|
||||
* Alessandro Molina
|
||||
* Alexander F. Rødseth
|
||||
* Ali Alim
|
||||
* Alperen Abak
|
||||
|
@ -388,7 +430,7 @@
|
|||
* Ben Lubar
|
||||
* Benjamin EWFT
|
||||
* Benoît Piédallu
|
||||
* Boo
|
||||
* Bojidar Marinov
|
||||
* Brad Johnson
|
||||
* Cadence Ember
|
||||
* Cale
|
||||
|
@ -397,15 +439,16 @@
|
|||
* Charlie Lambda
|
||||
* Christoph Geschwind
|
||||
* Chronos
|
||||
* Cirnos
|
||||
* Claude
|
||||
* Clifford Garwood II
|
||||
* Clément Brizard
|
||||
* Cédric Bahirwe
|
||||
* DLP
|
||||
* Daniel Dutra
|
||||
* David Baumgold
|
||||
* David Dobryakov
|
||||
* DeeJayBro
|
||||
* Denis Dupont
|
||||
* Deval
|
||||
* Dimitri DI GUSTO
|
||||
* Dimitrios Glentadakis
|
||||
|
@ -418,6 +461,7 @@
|
|||
* Erwan Croze
|
||||
* Esmail_Hazem
|
||||
* Ethan Corgatelli
|
||||
* FB
|
||||
* Fabio Agreles Bezerra
|
||||
* FediThing
|
||||
* Fernandez, ReK2
|
||||
|
@ -433,6 +477,7 @@
|
|||
* Henri BAUDESSON
|
||||
* HesioZ
|
||||
* Hozan Şahin
|
||||
* Hydrolien
|
||||
* ICabaleiro
|
||||
* Iker Garaialde
|
||||
* Ismaël Bouya
|
||||
|
@ -440,6 +485,7 @@
|
|||
* Iván Cabaleiro
|
||||
* J Webb
|
||||
* Jacen
|
||||
* Jackson
|
||||
* Jackson Chen
|
||||
* Jacob
|
||||
* Jacques Foucry
|
||||
|
@ -453,6 +499,7 @@
|
|||
* Jeston Tan
|
||||
* Jinn Koriech
|
||||
* Jlll1
|
||||
* Johan van Dongen
|
||||
* Johnny Jazeix
|
||||
* Jonas Sulzer
|
||||
* Jonatan Nyberg
|
||||
|
@ -465,24 +512,27 @@
|
|||
* Kent Anderson
|
||||
* Kevin Cope
|
||||
* Kevin Pliester
|
||||
* Khyvodul
|
||||
* Knackie
|
||||
* Kody
|
||||
* Konstantinos Agiannis
|
||||
* Kyâne Pichou
|
||||
* Leo Mouyna
|
||||
* Lesterpig
|
||||
* Lety Does Stuff
|
||||
* Levi Bard
|
||||
* LiPeK
|
||||
* Lint
|
||||
* LoveIsGrief
|
||||
* Luca B
|
||||
* Lucian I. Last
|
||||
* Lucien A
|
||||
* Lupinard
|
||||
* Léane GRASSER
|
||||
* Léo Andrès
|
||||
* ManMade-cube42
|
||||
* Marcel Fuhrmann
|
||||
* Marco Zehe
|
||||
* Marcus Schwarz
|
||||
* Marian Steinbach
|
||||
* Mario Pepe
|
||||
* Markus Richter
|
||||
|
@ -491,11 +541,14 @@
|
|||
* Mateusz Piotrowski
|
||||
* Mathieu Agopian
|
||||
* Mathieu Brunot
|
||||
* Matthias Frey
|
||||
* Matthieu De Beule
|
||||
* Max Rosenfors
|
||||
* Michael Koppmann
|
||||
* Michael Williams
|
||||
* Midgard
|
||||
* Miguel Mayol Tur
|
||||
* Miguel P.L
|
||||
* Mike
|
||||
* Mikel Gartzia Santamaria
|
||||
* Milo van der Linden
|
||||
|
@ -510,10 +563,11 @@
|
|||
* Novel Martin Harianto
|
||||
* Nuño Sempere
|
||||
* Olivier Jolly
|
||||
* Oliwier Jaszczyszyn
|
||||
* Pablo Joubert
|
||||
* Paul FLORENCE
|
||||
* Paul V
|
||||
* Pedro hates github.com
|
||||
* Pavel 7 Tomsk
|
||||
* PhieF
|
||||
* Philip Durbin
|
||||
* Philipp Fischbeck
|
||||
|
@ -522,15 +576,18 @@
|
|||
* Quantic Axe
|
||||
* Quentin Dupont
|
||||
* Quentí
|
||||
* RF9A5V
|
||||
* ROPEDE
|
||||
* Ramazan Geven
|
||||
* Ramiellll
|
||||
* Rangel Prodanov
|
||||
* Raphael
|
||||
* Raphaël Droz
|
||||
* Ray
|
||||
* Rebecca
|
||||
* Rech
|
||||
* Rep Dolsay
|
||||
* RiQuY
|
||||
* Robert Riemann
|
||||
* Roberto Resoli
|
||||
* Robin
|
||||
|
@ -542,6 +599,7 @@
|
|||
* Scott Starkey
|
||||
* Sebastian Paweł Wolski
|
||||
* Seth Falco
|
||||
* Shalabh Agarwal
|
||||
* Showfom
|
||||
* Shun Sakai
|
||||
* Simon Gilliot
|
||||
|
@ -549,8 +607,10 @@
|
|||
* Stefan Schüller
|
||||
* Steffen
|
||||
* Steffen Möller
|
||||
* Subh B
|
||||
* Sumit Khanna
|
||||
* SupC
|
||||
* Sébastien NOBILI
|
||||
* TA
|
||||
* Tanguy BERNARD
|
||||
* Thavarasa Prasanth
|
||||
|
@ -562,7 +622,6 @@
|
|||
* Tomás Sebastián Romero
|
||||
* TrashMacNugget
|
||||
* Treacle
|
||||
* Tsuki
|
||||
* Unetelle Inconnue
|
||||
* Vagelis F
|
||||
* Varik Valefor
|
||||
|
@ -577,10 +636,12 @@
|
|||
* Yehuda Deutsch
|
||||
* Yorwba
|
||||
* Yun
|
||||
* Zack Birkenbuel
|
||||
* Zekovski
|
||||
* Zig-03
|
||||
* [ Bie ] Watcharapong Suriyawan
|
||||
* adam iter
|
||||
* allmiha2
|
||||
* anmol26s
|
||||
* april
|
||||
* ar9708
|
||||
|
@ -600,7 +661,6 @@
|
|||
* jomo
|
||||
* kukhariev
|
||||
* lambdacastix
|
||||
* legiorange
|
||||
* libertysoft3
|
||||
* lost_geographer
|
||||
* lsde
|
||||
|
@ -624,16 +684,19 @@
|
|||
* philippe lhardy
|
||||
* pitchum
|
||||
* potedeo
|
||||
* q0ntinuum
|
||||
* rdxuan
|
||||
* retiolus
|
||||
* ruvilonix
|
||||
* sanchis
|
||||
* skyone-wzw
|
||||
* slendermon
|
||||
* smilekison
|
||||
* sn0wygecko
|
||||
* soonsouth
|
||||
* thecashewtrader
|
||||
* tilllt
|
||||
* tmpod
|
||||
* tomamplius
|
||||
* toobad
|
||||
* treac1e
|
||||
|
@ -646,6 +709,7 @@
|
|||
* Артур Кирпо
|
||||
* Дмитрий Кузнецов
|
||||
* noisawe
|
||||
* 姚霁恒
|
||||
* abdhessuk
|
||||
* abidin24
|
||||
* aditoo
|
||||
|
@ -782,6 +846,7 @@
|
|||
* `peertube-x` by Solen DP (CC-BY)
|
||||
* `flame` by Freepik (Flaticon License)
|
||||
* `local` by Larea (CC-BY)
|
||||
* X (Twitter) icon: [Wikimedia Commons](https://fr.m.wikipedia.org/wiki/Fichier:X_logo_2023.svg)
|
||||
|
||||
|
||||
# Contributors to our 2020 crowdfunding :heart:
|
||||
|
|
5
apps/peertube-cli/CHANGELOG.md
Normal file
5
apps/peertube-cli/CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## v1.0.3
|
||||
|
||||
* Fix `util.isArray` deprecation warning
|
|
@ -10,8 +10,7 @@ See https://docs.joinpeertube.org/maintain/tools#remote-tools
|
|||
|
||||
```bash
|
||||
cd peertube-root
|
||||
yarn install --pure-lockfile
|
||||
cd apps/peertube-cli && yarn install --pure-lockfile
|
||||
npm run install-node-dependencies
|
||||
```
|
||||
|
||||
## Develop
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@peertube/peertube-cli",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"type": "module",
|
||||
"main": "dist/peertube.js",
|
||||
"bin": "dist/peertube.js",
|
||||
|
|
|
@ -16,7 +16,6 @@ export function defineAuthProgram () {
|
|||
.option('-p, --password <token>', 'Password')
|
||||
.option('--default', 'add the entry as the new default')
|
||||
.action(options => {
|
||||
/* eslint-disable no-import-assign */
|
||||
prompt.override = options
|
||||
prompt.start()
|
||||
prompt.get({
|
||||
|
@ -39,7 +38,6 @@ export function defineAuthProgram () {
|
|||
}
|
||||
}
|
||||
}, async (_, result) => {
|
||||
|
||||
// Check credentials
|
||||
try {
|
||||
// Strip out everything after the domain:port.
|
||||
|
@ -111,11 +109,13 @@ export function defineAuthProgram () {
|
|||
}
|
||||
})
|
||||
|
||||
program.addHelpText('after', '\n\n Examples:\n\n' +
|
||||
' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' +
|
||||
' $ peertube auth add -u https://peertube.cpy.re -U root\n' +
|
||||
' $ peertube auth list\n' +
|
||||
' $ peertube auth del https://peertube.cpy.re\n'
|
||||
program.addHelpText(
|
||||
'after',
|
||||
'\n\n Examples:\n\n' +
|
||||
' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' +
|
||||
' $ peertube auth add -u https://peertube.cpy.re -U root\n' +
|
||||
' $ peertube auth list\n' +
|
||||
' $ peertube auth del https://peertube.cpy.re\n'
|
||||
)
|
||||
|
||||
return program
|
||||
|
|
|
@ -72,7 +72,12 @@ export function defineUploadProgram () {
|
|||
|
||||
await run({ ...options, url, username, password })
|
||||
} catch (err) {
|
||||
console.error('Cannot upload video: ' + err.message)
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
console.error(`Server is not responding`)
|
||||
} else {
|
||||
console.error('Cannot upload video: ' + err.message)
|
||||
}
|
||||
|
||||
process.exit(-1)
|
||||
}
|
||||
})
|
||||
|
@ -127,7 +132,7 @@ async function buildVideoAttributesFromCommander (server: PeerTubeServer, option
|
|||
waitTranscoding: true
|
||||
}
|
||||
|
||||
const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {}
|
||||
const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } = {} as any
|
||||
|
||||
for (const key of Object.keys(defaultBooleanAttributes)) {
|
||||
if (options[key] !== undefined) {
|
||||
|
|
|
@ -72,8 +72,7 @@ function getRemoteObjectOrDie (
|
|||
settings: Settings,
|
||||
netrc: Netrc
|
||||
): { url: string, username: string, password: string } {
|
||||
|
||||
function exitIfNoOptions (optionNames: string[], errorPrefix: string = '') {
|
||||
function exitIfNoOptions (optionNames: string[], errorPrefix = '') {
|
||||
let exit = false
|
||||
|
||||
for (const key of optionNames) {
|
||||
|
@ -126,9 +125,9 @@ function listOptions (val: string) {
|
|||
|
||||
function getServerCredentials (options: CommonProgramOptions) {
|
||||
return Promise.all([ getSettings(), getNetrc() ])
|
||||
.then(([ settings, netrc ]) => {
|
||||
return getRemoteObjectOrDie(options, settings, netrc)
|
||||
})
|
||||
.then(([ settings, netrc ]) => {
|
||||
return getRemoteObjectOrDie(options, settings, netrc)
|
||||
})
|
||||
}
|
||||
|
||||
function buildServer (url: string) {
|
||||
|
@ -184,11 +183,8 @@ export {
|
|||
getRemoteObjectOrDie,
|
||||
writeSettings,
|
||||
deleteSettings,
|
||||
|
||||
getServerCredentials,
|
||||
|
||||
listOptions,
|
||||
|
||||
getAdminTokenOrDie,
|
||||
buildServer,
|
||||
assignToken
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
application-config-path@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-1.0.0.tgz#9c25b8c00ac9a342db27275abd3f38c67bbe5a05"
|
||||
integrity sha512-6ZDlLTlfqrTybVzZJDpX2K2ZufqyMyiTbOG06GpxmkmczFgTN+YYRGcTcMCXv/F5P5SrZijVjzzpPUE9BvheLg==
|
||||
|
||||
application-config@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/application-config/-/application-config-3.0.0.tgz#9adec84dd2d81e97dd78ea0dffcbf97381a1f55c"
|
||||
integrity sha512-7ViR4soQJDx2O9iLf1vGxvekkPqvwqV/AZ2OL3DNcAQrg03UjJE1VeBk7oYNoN9AKB0eNyVrcM7kPD30NKeLLw==
|
||||
dependencies:
|
||||
application-config-path "^1.0.0"
|
||||
load-json-file "^7.0.1"
|
||||
write-json-file "^5.0.0"
|
||||
|
||||
cli-table3@^0.6.0:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f"
|
||||
integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
optionalDependencies:
|
||||
"@colors/colors" "1.5.0"
|
||||
|
||||
cross-spawn@^6.0.0:
|
||||
version "6.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
|
||||
integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
semver "^5.5.0"
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
detect-indent@^7.0.0:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25"
|
||||
integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
execa@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
|
||||
integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^3.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==
|
||||
|
||||
imurmurhash@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-plain-obj@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
|
||||
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
|
||||
|
||||
is-typedarray@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
load-json-file@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-7.0.1.tgz#a3c9fde6beffb6bedb5acf104fad6bb1604e1b00"
|
||||
integrity sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
netrc-parser@^3.1.6:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72"
|
||||
integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw==
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
execa "^0.10.0"
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
|
||||
dependencies:
|
||||
path-key "^2.0.0"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
|
||||
|
||||
path-key@^2.0.0, path-key@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
|
||||
|
||||
semver@^5.5.0:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
|
||||
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
|
||||
|
||||
shebang-command@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
|
||||
dependencies:
|
||||
shebang-regex "^1.0.0"
|
||||
|
||||
shebang-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
sort-keys@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.1.0.tgz#50a3f3d1ad3c5a76d043e0aeeba7299241e9aa5c"
|
||||
integrity sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==
|
||||
dependencies:
|
||||
is-plain-obj "^4.0.0"
|
||||
|
||||
string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-eof@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
|
||||
|
||||
typedarray-to-buffer@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
which@^1.2.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
write-file-atomic@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
|
||||
integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
|
||||
dependencies:
|
||||
imurmurhash "^0.1.4"
|
||||
is-typedarray "^1.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
typedarray-to-buffer "^3.1.5"
|
||||
|
||||
write-json-file@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-5.0.0.tgz#11c329a8ea9e8e23fb92a87cc27412a15f87708b"
|
||||
integrity sha512-ddSsCLa4aQ3kI21BthINo4q905/wfhvQ3JL3774AcRjBaiQmfn5v4rw77jQ7T6CmAit9VOQO+FsLyPkwxoB1fw==
|
||||
dependencies:
|
||||
detect-indent "^7.0.0"
|
||||
is-plain-obj "^4.0.0"
|
||||
sort-keys "^5.0.0"
|
||||
write-file-atomic "^3.0.3"
|
|
@ -1,8 +1,30 @@
|
|||
# Changelog
|
||||
|
||||
## v0.3.0
|
||||
|
||||
* Add generate storyboard support (PeerTube >= 8.0)
|
||||
|
||||
## v0.2.0
|
||||
|
||||
* Add runner version in request and register payloads
|
||||
* Update dependencies to fix vulnerabilities
|
||||
|
||||
## v0.1.3
|
||||
|
||||
* Disable log coloring when TTY does not support it
|
||||
* Add download file timeout (2 hours) to prevent stuck jobs
|
||||
|
||||
## v0.1.2
|
||||
|
||||
* Support query params in custom upload URL
|
||||
|
||||
## v0.1.1
|
||||
|
||||
* Fix adding studio watermark with audio/video split HLS file
|
||||
|
||||
## v0.1.0
|
||||
|
||||
* Requires Node 20
|
||||
* Introduce `list-jobs` command to list processing jobs
|
||||
* Update dependencies
|
||||
* Send last chunks/playlist content to correctly end the live
|
||||
* Requires Node 20
|
||||
* Introduce `list-jobs` command to list processing jobs
|
||||
* Update dependencies
|
||||
* Send last chunks/playlist content to correctly end the live
|
||||
|
|
|
@ -10,8 +10,7 @@ Commands below has to be run at the root of PeerTube git repository.
|
|||
|
||||
```bash
|
||||
cd peertube-root
|
||||
yarn install --pure-lockfile
|
||||
cd apps/peertube-runner && yarn install --pure-lockfile
|
||||
npm run install-node-dependencies
|
||||
```
|
||||
|
||||
### Develop
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"name": "@peertube/peertube-runner",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"main": "dist/peertube-runner.js",
|
||||
"bin": "dist/peertube-runner.js",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@peertube/net-ipc": "^2.2.0",
|
||||
"@types/follow-redirects": "1.14.4",
|
||||
"cli-table3": "^0.6.5",
|
||||
"env-paths": "^3.0.0",
|
||||
"follow-redirects": "^1.15.5",
|
||||
"pino": "^9.2.0",
|
||||
"pino-pretty": "^11.2.1"
|
||||
"net-ipc": "^2.2.2",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,13 @@ import {
|
|||
RunnerJobVODWebVideoTranscodingPayload
|
||||
} from '@peertube/peertube-models'
|
||||
import { logger } from '../../shared/index.js'
|
||||
import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared/index.js'
|
||||
import {
|
||||
processAudioMergeTranscoding,
|
||||
processGenerateStoryboard,
|
||||
processHLSTranscoding,
|
||||
ProcessOptions,
|
||||
processWebVideoTranscoding
|
||||
} from './shared/index.js'
|
||||
import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live.js'
|
||||
import { processStudioTranscoding } from './shared/process-studio.js'
|
||||
import { processVideoTranscription } from './shared/process-transcription.js'
|
||||
|
@ -15,7 +21,7 @@ import { processVideoTranscription } from './shared/process-transcription.js'
|
|||
export async function processJob (options: ProcessOptions) {
|
||||
const { server, job } = options
|
||||
|
||||
logger.info(`[${server.url}] Processing job of type ${job.type}: ${job.uuid}`, { payload: job.payload })
|
||||
logger.info({ payload: job.payload }, `[${server.url}] Processing job of type ${job.type}: ${job.uuid}`)
|
||||
|
||||
switch (job.type) {
|
||||
case 'vod-audio-merge-transcoding':
|
||||
|
@ -42,6 +48,10 @@ export async function processJob (options: ProcessOptions) {
|
|||
await processVideoTranscription(options as ProcessOptions<RunnerJobTranscriptionPayload>)
|
||||
break
|
||||
|
||||
case 'generate-video-storyboard':
|
||||
await processGenerateStoryboard(options as any)
|
||||
break
|
||||
|
||||
default:
|
||||
logger.error(`Unknown job ${job.type} to process`)
|
||||
return
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { FFmpegEdition, FFmpegLive, FFmpegVOD, getDefaultAvailableEncoders, getDefaultEncodersToTry } from '@peertube/peertube-ffmpeg'
|
||||
import {
|
||||
FFmpegEdition,
|
||||
FFmpegImage,
|
||||
FFmpegLive,
|
||||
FFmpegVOD,
|
||||
getDefaultAvailableEncoders,
|
||||
getDefaultEncodersToTry
|
||||
} from '@peertube/peertube-ffmpeg'
|
||||
import { RunnerJob, RunnerJobPayload } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
@ -8,9 +15,9 @@ import { join } from 'path'
|
|||
import { ConfigManager, downloadFile, logger } from '../../../shared/index.js'
|
||||
import { getWinstonLogger } from './winston-logger.js'
|
||||
|
||||
export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
|
||||
export type JobWithToken<T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
|
||||
|
||||
export type ProcessOptions <T extends RunnerJobPayload = RunnerJobPayload> = {
|
||||
export type ProcessOptions<T extends RunnerJobPayload = RunnerJobPayload> = {
|
||||
server: PeerTubeServer
|
||||
job: JobWithToken<T>
|
||||
runnerToken: string
|
||||
|
@ -108,6 +115,10 @@ export function buildFFmpegEdition () {
|
|||
return new FFmpegEdition(getCommonFFmpegOptions())
|
||||
}
|
||||
|
||||
export function buildFFmpegImage () {
|
||||
return new FFmpegImage(getCommonFFmpegOptions())
|
||||
}
|
||||
|
||||
function getCommonFFmpegOptions () {
|
||||
const config = ConfigManager.Instance.getConfig()
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './common.js'
|
||||
export * from './process-vod.js'
|
||||
export * from './winston-logger.js'
|
||||
export * from './process-storyboard.js'
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { RunnerJobGenerateStoryboardPayload, GenerateStoryboardSuccess } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { ConfigManager } from '../../../shared/config-manager.js'
|
||||
import { logger } from '../../../shared/index.js'
|
||||
import { buildFFmpegImage, downloadInputFile, ProcessOptions, scheduleTranscodingProgress } from './common.js'
|
||||
|
||||
export async function processGenerateStoryboard (options: ProcessOptions<RunnerJobGenerateStoryboardPayload>) {
|
||||
const { server, job, runnerToken } = options
|
||||
|
||||
const payload = job.payload
|
||||
|
||||
let ffmpegProgress: number
|
||||
let videoInputPath: string
|
||||
|
||||
const outputPath = join(ConfigManager.Instance.getStoryboardDirectory(), `storyboard-${buildUUID()}.jpg`)
|
||||
|
||||
const updateProgressInterval = scheduleTranscodingProgress({
|
||||
job,
|
||||
server,
|
||||
runnerToken,
|
||||
progressGetter: () => ffmpegProgress
|
||||
})
|
||||
|
||||
try {
|
||||
logger.info(`Downloading input file ${payload.input.videoFileUrl} for storyboard job ${job.jobToken}`)
|
||||
|
||||
videoInputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
|
||||
|
||||
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Generating storyboard.`)
|
||||
|
||||
const ffmpegImage = buildFFmpegImage()
|
||||
|
||||
await ffmpegImage.generateStoryboardFromVideo({
|
||||
path: videoInputPath,
|
||||
destination: outputPath,
|
||||
inputFileMutexReleaser: () => {},
|
||||
sprites: payload.sprites
|
||||
})
|
||||
|
||||
const successBody: GenerateStoryboardSuccess = {
|
||||
storyboardFile: outputPath
|
||||
}
|
||||
|
||||
await server.runnerJobs.success({
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (videoInputPath) await remove(videoInputPath)
|
||||
if (outputPath) await remove(outputPath)
|
||||
if (updateProgressInterval) clearInterval(updateProgressInterval)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
|
||||
import { PeerTubeProblemDocument, RunnerJobType, ServerErrorCode } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
|
||||
import { ensureDir, remove } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
|
||||
import { PeerTubeProblemDocument, RunnerJobType, ServerErrorCode } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
|
||||
import { ConfigManager } from '../shared/index.js'
|
||||
import { IPCServer } from '../shared/ipc/index.js'
|
||||
import { logger } from '../shared/logger.js'
|
||||
|
@ -57,7 +57,7 @@ export class RunnerServer {
|
|||
try {
|
||||
await ipcServer.run(this)
|
||||
} catch (err) {
|
||||
logger.error('Cannot start local socket for IPC communication', err)
|
||||
logger.error(err, 'Cannot start local socket for IPC communication')
|
||||
process.exit(-1)
|
||||
}
|
||||
|
||||
|
@ -74,9 +74,11 @@ export class RunnerServer {
|
|||
|
||||
// Process jobs
|
||||
await ensureDir(ConfigManager.Instance.getTranscodingDirectory())
|
||||
await ensureDir(ConfigManager.Instance.getStoryboardDirectory())
|
||||
await this.cleanupTMP()
|
||||
|
||||
logger.info(`Using ${ConfigManager.Instance.getTranscodingDirectory()} for transcoding directory`)
|
||||
logger.info(`Using ${ConfigManager.Instance.getStoryboardDirectory()} for storyboard directory`)
|
||||
|
||||
this.initialized = true
|
||||
await this.checkAvailableJobs()
|
||||
|
@ -95,7 +97,12 @@ export class RunnerServer {
|
|||
logger.info(`Registering runner ${runnerName} on ${url}...`)
|
||||
|
||||
const serverCommand = new PeerTubeServerCommand({ url })
|
||||
const { runnerToken } = await serverCommand.runners.register({ name: runnerName, description: runnerDescription, registrationToken })
|
||||
const { runnerToken } = await serverCommand.runners.register({
|
||||
name: runnerName,
|
||||
description: runnerDescription,
|
||||
registrationToken,
|
||||
version: process.env.PACKAGE_VERSION
|
||||
})
|
||||
|
||||
const server: PeerTubeServer = Object.assign(serverCommand, {
|
||||
runnerToken,
|
||||
|
@ -268,7 +275,9 @@ export class RunnerServer {
|
|||
|
||||
jobTypes: this.enabledJobsArray.length !== getSupportedJobsList().length
|
||||
? this.enabledJobsArray
|
||||
: undefined
|
||||
: undefined,
|
||||
|
||||
version: process.env.PACKAGE_VERSION
|
||||
})
|
||||
|
||||
// FIXME: remove in PeerTube v8: jobTypes has been introduced in PeerTube v7, so do the filter here too
|
||||
|
@ -358,6 +367,8 @@ export class RunnerServer {
|
|||
|
||||
try {
|
||||
for (const { server, job } of this.processingJobs) {
|
||||
logger.info(`Aborting job ${job.uuid} on ${server.url} as the runner is stopping`)
|
||||
|
||||
await server.runnerJobs.abort({
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
|
|
|
@ -7,7 +7,8 @@ import {
|
|||
RunnerJobVODAudioMergeTranscodingPayload,
|
||||
RunnerJobVODHLSTranscodingPayload,
|
||||
RunnerJobVODWebVideoTranscodingPayload,
|
||||
VideoStudioTaskPayload
|
||||
VideoStudioTaskPayload,
|
||||
RunnerJobGenerateStoryboardPayload
|
||||
} from '@peertube/peertube-models'
|
||||
|
||||
const supportedMatrix: { [ id in RunnerJobType ]: (payload: RunnerJobPayload) => boolean } = {
|
||||
|
@ -33,7 +34,8 @@ const supportedMatrix: { [ id in RunnerJobType ]: (payload: RunnerJobPayload) =>
|
|||
},
|
||||
'video-transcription': (_payload: RunnerJobTranscriptionPayload) => {
|
||||
return true
|
||||
}
|
||||
},
|
||||
'generate-video-storyboard': (_payload: RunnerJobGenerateStoryboardPayload) => true
|
||||
}
|
||||
|
||||
export function isJobSupported (job: { type: RunnerJobType, payload: RunnerJobPayload }, enabledJobs?: Set<RunnerJobType>) {
|
||||
|
|
|
@ -108,6 +108,10 @@ export class ConfigManager {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getStoryboardDirectory () {
|
||||
return join(paths.cache, this.id, 'storyboard')
|
||||
}
|
||||
|
||||
getTranscodingDirectory () {
|
||||
return join(paths.cache, this.id, 'transcoding')
|
||||
}
|
||||
|
@ -137,7 +141,7 @@ export class ConfigManager {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Thanks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
|
||||
private deepFreeze <T extends object> (object: T) {
|
||||
private deepFreeze<T extends object> (object: T) {
|
||||
const propNames = Reflect.ownKeys(object)
|
||||
|
||||
// Freeze properties before freezing self
|
||||
|
|
|
@ -41,6 +41,14 @@ export function downloadFile (options: {
|
|||
}
|
||||
|
||||
const file = createWriteStream(destination)
|
||||
|
||||
file.on('error', err => {
|
||||
remove(destination)
|
||||
.catch(err => logger.error(err))
|
||||
|
||||
return rej(err)
|
||||
})
|
||||
|
||||
file.on('finish', () => res())
|
||||
|
||||
response.pipe(file)
|
||||
|
@ -55,6 +63,10 @@ export function downloadFile (options: {
|
|||
|
||||
request.write(body)
|
||||
request.end()
|
||||
|
||||
setTimeout(() => {
|
||||
request.destroy(new Error('Global request timeout'))
|
||||
}, 2 * 3600 * 1000) // 2 hours
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Client as NetIPC } from '@peertube/net-ipc'
|
||||
import { Client as NetIPC } from 'net-ipc'
|
||||
import CliTable3 from 'cli-table3'
|
||||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { ConfigManager } from '../config-manager.js'
|
||||
|
@ -20,7 +20,7 @@ export class IPCClient {
|
|||
if (err.code === 'ECONNREFUSED') {
|
||||
throw new Error(
|
||||
'This runner is not currently running in server mode on this system. ' +
|
||||
'Please run it using the `server` command first (in another terminal for example) and then retry your command.'
|
||||
'Please run it using the `server` command first (in another terminal for example) and then retry your command.'
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { Server as NetIPC } from '@peertube/net-ipc'
|
||||
import { Server as NetIPC } from 'net-ipc'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { RunnerServer } from '../../server/index.js'
|
||||
import { ConfigManager } from '../config-manager.js'
|
||||
|
@ -58,11 +58,11 @@ export class IPCServer {
|
|||
}
|
||||
}
|
||||
|
||||
private sendResponse <T extends IPCResponseData> (
|
||||
private sendResponse<T extends IPCResponseData> (
|
||||
response: (data: any) => Promise<void>,
|
||||
body: IPCResponse<T>
|
||||
) {
|
||||
response(body)
|
||||
.catch(err => logger.error('Cannot send response after IPC request', err))
|
||||
.catch(err => logger.error(err, 'Cannot send response after IPC request'))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import { pino } from 'pino'
|
||||
import pretty from 'pino-pretty'
|
||||
|
||||
const logger = pino(pretty({
|
||||
colorize: true
|
||||
}))
|
||||
|
||||
logger.level = 'info'
|
||||
|
||||
export {
|
||||
logger
|
||||
}
|
||||
export const logger = pino({ level: 'info' }, pretty())
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@iarna/toml@^2.2.5":
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
|
||||
integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11"
|
||||
integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855"
|
||||
integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb"
|
||||
integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159"
|
||||
integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3"
|
||||
integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242"
|
||||
integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==
|
||||
|
||||
"@peertube/net-ipc@^2.2.0":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@peertube/net-ipc/-/net-ipc-2.2.1.tgz#3d1c154a08b57cfea31ed760ec76fe2f69e35a19"
|
||||
integrity sha512-RyKIGC3EeQ+xnSccf592qqsaXWrGp4wGfGl4W+wxDoZkwsThZJuiSbX8aCC1qZBHaDo3EuRH3ZrwsKpNjnyDAQ==
|
||||
optionalDependencies:
|
||||
fast-zlib "^2.0.1"
|
||||
msgpackr "^1.3.2"
|
||||
|
||||
"@types/follow-redirects@1.14.4":
|
||||
version "1.14.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/follow-redirects/-/follow-redirects-1.14.4.tgz#ca054d72ef574c77949fc5fff278b430fcd508ec"
|
||||
integrity sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "22.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.3.tgz#08f3d64b3bc6d74b162d36f60213e8a6704ef2b4"
|
||||
integrity sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==
|
||||
dependencies:
|
||||
undici-types "~6.19.8"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
atomic-sleep@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
colorette@^2.0.7:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
dateformat@^4.6.3:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
detect-libc@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
||||
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
env-paths@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da"
|
||||
integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
events@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
fast-copy@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35"
|
||||
integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
|
||||
|
||||
fast-redact@^3.1.1:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4"
|
||||
integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
|
||||
|
||||
fast-safe-stringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fast-zlib@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-zlib/-/fast-zlib-2.0.1.tgz#be624f592fc80ad8019ee2025d16a367a4e9b024"
|
||||
integrity sha512-DCoYgNagM2Bt1VIpXpdGnRx4LzqJeYG0oh6Nf/7cWo6elTXkFGMw9CrRCYYUIapYNrozYMoyDRflx9mgT3Awyw==
|
||||
|
||||
follow-redirects@^1.15.5:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
help-me@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6"
|
||||
integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==
|
||||
|
||||
ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
joycon@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
msgpackr-extract@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012"
|
||||
integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==
|
||||
dependencies:
|
||||
node-gyp-build-optional-packages "5.2.2"
|
||||
optionalDependencies:
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3"
|
||||
|
||||
msgpackr@^1.3.2:
|
||||
version "1.11.2"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.2.tgz#4463b7f7d68f2e24865c395664973562ad24473d"
|
||||
integrity sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==
|
||||
optionalDependencies:
|
||||
msgpackr-extract "^3.0.2"
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4"
|
||||
integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==
|
||||
dependencies:
|
||||
detect-libc "^2.0.1"
|
||||
|
||||
on-exit-leak-free@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
|
||||
integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
|
||||
|
||||
once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
pino-abstract-transport@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60"
|
||||
integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==
|
||||
dependencies:
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-pretty@^11.2.1:
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.3.0.tgz#390b3be044cf3d2e9192c7d19d44f6b690468f2e"
|
||||
integrity sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==
|
||||
dependencies:
|
||||
colorette "^2.0.7"
|
||||
dateformat "^4.6.3"
|
||||
fast-copy "^3.0.2"
|
||||
fast-safe-stringify "^2.1.1"
|
||||
help-me "^5.0.0"
|
||||
joycon "^3.1.1"
|
||||
minimist "^1.2.6"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pump "^3.0.0"
|
||||
readable-stream "^4.0.0"
|
||||
secure-json-parse "^2.4.0"
|
||||
sonic-boom "^4.0.1"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
pino-std-serializers@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b"
|
||||
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
|
||||
|
||||
pino@^9.2.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-9.5.0.tgz#a7ef0fea868d22d52d8a4ce46e6e03c5dc46fdd6"
|
||||
integrity sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
fast-redact "^3.1.1"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pino-std-serializers "^7.0.0"
|
||||
process-warning "^4.0.0"
|
||||
quick-format-unescaped "^4.0.3"
|
||||
real-require "^0.2.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
sonic-boom "^4.0.1"
|
||||
thread-stream "^3.0.0"
|
||||
|
||||
process-warning@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.0.tgz#581e3a7a1fb456c5f4fd239f76bce75897682d5a"
|
||||
integrity sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==
|
||||
|
||||
process@^0.11.10:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
|
||||
integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
quick-format-unescaped@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
|
||||
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
|
||||
|
||||
readable-stream@^4.0.0:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
|
||||
integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==
|
||||
dependencies:
|
||||
abort-controller "^3.0.0"
|
||||
buffer "^6.0.3"
|
||||
events "^3.3.0"
|
||||
process "^0.11.10"
|
||||
string_decoder "^1.3.0"
|
||||
|
||||
real-require@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
|
||||
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
|
||||
|
||||
safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-stable-stringify@^2.3.1:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
|
||||
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
|
||||
|
||||
secure-json-parse@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
|
||||
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
|
||||
|
||||
sonic-boom@^4.0.1:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d"
|
||||
integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
|
||||
split2@^4.0.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
|
||||
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
|
||||
|
||||
string_decoder@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
thread-stream@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1"
|
||||
integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==
|
||||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
undici-types@~6.19.8:
|
||||
version "6.19.8"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
|
@ -1,181 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*",
|
||||
"node_modules/",
|
||||
"src/standalone/embed-player-api/dist"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.eslint.json"
|
||||
],
|
||||
"EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true,
|
||||
"createDefaultProgram": false
|
||||
},
|
||||
"extends": [
|
||||
"../.eslintrc.json",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"jsdoc/newline-after-description": "off",
|
||||
"jsdoc/check-alignment": "off",
|
||||
"lines-between-class-members": "off",
|
||||
"@typescript-eslint/lines-between-class-members": [ "off" ],
|
||||
"arrow-body-style": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"n/no-callback-literal": "off",
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": [ "element", "attribute" ],
|
||||
"prefix": "my",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": [ "element", "attribute" ],
|
||||
"prefix": "my",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-this-alias": [
|
||||
"error",
|
||||
{
|
||||
"allowDestructuring": true,
|
||||
"allowedNames": ["self", "player"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-readonly": "off",
|
||||
"@angular-eslint/use-component-view-encapsulation": "error",
|
||||
"prefer-arrow/prefer-arrow-functions": "off",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"@typescript-eslint/dot-notation": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"off",
|
||||
{
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-ordering": [
|
||||
"off"
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-for-of": "off",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-shadow": [
|
||||
"off",
|
||||
{
|
||||
"hoist": "all"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowTaggedTemplates": true,
|
||||
"allowShortCircuit": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"brace-style": [
|
||||
"error",
|
||||
"1tbs"
|
||||
],
|
||||
"comma-dangle": "error",
|
||||
"curly": [
|
||||
"error",
|
||||
"multi-line"
|
||||
],
|
||||
"dot-notation": "off",
|
||||
"no-useless-return": "off",
|
||||
"indent": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-console": "off",
|
||||
"no-return-assign": "off",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-function": [
|
||||
"error",
|
||||
{ "allow": [ "constructors" ] }
|
||||
],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-multiple-empty-lines": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-return-await": "error",
|
||||
"no-shadow": "off",
|
||||
"no-unused-expressions": "error",
|
||||
"semi": "error",
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"object-shorthand": [
|
||||
"error",
|
||||
"properties"
|
||||
],
|
||||
"quote-props": [
|
||||
"error",
|
||||
"consistent-as-needed"
|
||||
],
|
||||
"no-constant-binary-expression": "error",
|
||||
"@typescript-eslint/unbound-method": [
|
||||
"error",
|
||||
{ "ignoreStatic": true }
|
||||
],
|
||||
"import/no-named-default": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended",
|
||||
"plugin:@angular-eslint/template/accessibility"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
104
client/.github/instructions/angular.instructions.md
vendored
Normal file
104
client/.github/instructions/angular.instructions.md
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
description: 'Angular-specific coding standards and best practices'
|
||||
applyTo: 'src/app/**/*.ts, src/app/**/*.html, src/app/**/*.scss, src/app/**/*.css'
|
||||
---
|
||||
|
||||
# Angular Development Instructions
|
||||
|
||||
Instructions for generating high-quality Angular applications with TypeScript, using Angular Signals for state management, adhering to Angular best practices as outlined at https://angular.dev.
|
||||
|
||||
## Project Context
|
||||
- Latest Angular version (use standalone components by default)
|
||||
- TypeScript for type safety
|
||||
- Angular CLI for project setup and scaffolding
|
||||
- Follow Angular Style Guide (https://angular.dev/style-guide)
|
||||
- Use Angular Material or other modern UI libraries for consistent styling (if specified)
|
||||
|
||||
## Development Standards
|
||||
|
||||
### Architecture
|
||||
- Use standalone components unless modules are explicitly required
|
||||
- Organize code by feature modules or domains for scalability
|
||||
- Implement lazy loading for feature modules to optimize performance
|
||||
- Use Angular's built-in dependency injection system effectively
|
||||
- Structure components with a clear separation of concerns (smart vs. presentational components)
|
||||
|
||||
### TypeScript
|
||||
- Enable strict mode in `tsconfig.json` for type safety
|
||||
- Define clear interfaces and types for components, services, and models
|
||||
- Use type guards and union types for robust type checking
|
||||
- Implement proper error handling with RxJS operators (e.g., `catchError`)
|
||||
- Use typed forms (e.g., `FormGroup`, `FormControl`) for reactive forms
|
||||
|
||||
### Component Design
|
||||
- Follow Angular's component lifecycle hooks best practices
|
||||
- When using Angular >= 19, Use `input()` `output()`, `viewChild()`, `viewChildren()`, `contentChild()` and `viewChildren()` functions instead of decorators; otherwise use decorators
|
||||
- Leverage Angular's change detection strategy (default or `OnPush` for performance)
|
||||
- Keep templates clean and logic in component classes or services
|
||||
- Use Angular directives and pipes for reusable functionality
|
||||
|
||||
### Styling
|
||||
- Use Angular's component-level CSS encapsulation (default: ViewEncapsulation.Emulated)
|
||||
- Prefer SCSS for styling with consistent theming
|
||||
- Implement responsive design using CSS Grid, Flexbox, or Angular CDK Layout utilities
|
||||
- Follow Angular Material's theming guidelines if used
|
||||
- Maintain accessibility (a11y) with ARIA attributes and semantic HTML
|
||||
|
||||
### State Management
|
||||
- Use Angular Signals for reactive state management in components and services
|
||||
- Leverage `signal()`, `computed()`, and `effect()` for reactive state updates
|
||||
- Use writable signals for mutable state and computed signals for derived state
|
||||
- Handle loading and error states with signals and proper UI feedback
|
||||
- Use Angular's `AsyncPipe` to handle observables in templates when combining signals with RxJS
|
||||
|
||||
### Data Fetching
|
||||
- Use Angular's `HttpClient` for API calls with proper typing
|
||||
- Implement RxJS operators for data transformation and error handling
|
||||
- Use Angular's `inject()` function for dependency injection in standalone components
|
||||
- Implement caching strategies (e.g., `shareReplay` for observables)
|
||||
- Store API response data in signals for reactive updates
|
||||
- Handle API errors with global interceptors for consistent error handling
|
||||
|
||||
### Security
|
||||
- Sanitize user inputs using Angular's built-in sanitization
|
||||
- Implement route guards for authentication and authorization
|
||||
- Use Angular's `HttpInterceptor` for CSRF protection and API authentication headers
|
||||
- Validate form inputs with Angular's reactive forms and custom validators
|
||||
- Follow Angular's security best practices (e.g., avoid direct DOM manipulation)
|
||||
|
||||
### Performance
|
||||
- Enable production builds with `ng build --prod` for optimization
|
||||
- Use lazy loading for routes to reduce initial bundle size
|
||||
- Optimize change detection with `OnPush` strategy and signals for fine-grained reactivity
|
||||
- Use trackBy in `ngFor` loops to improve rendering performance
|
||||
- Implement server-side rendering (SSR) or static site generation (SSG) with Angular Universal (if specified)
|
||||
|
||||
### Testing
|
||||
- Write unit tests for components, services, and pipes using Jasmine and Karma
|
||||
- Use Angular's `TestBed` for component testing with mocked dependencies
|
||||
- Test signal-based state updates using Angular's testing utilities
|
||||
- Write end-to-end tests with Cypress or Playwright (if specified)
|
||||
- Mock HTTP requests using `HttpClientTestingModule`
|
||||
- Ensure high test coverage for critical functionality
|
||||
|
||||
## Implementation Process
|
||||
1. Plan project structure and feature modules
|
||||
2. Define TypeScript interfaces and models
|
||||
3. Scaffold components, services, and pipes using Angular CLI
|
||||
4. Implement data services and API integrations with signal-based state
|
||||
5. Build reusable components with clear inputs and outputs
|
||||
6. Add reactive forms and validation
|
||||
7. Apply styling with SCSS and responsive design
|
||||
8. Implement lazy-loaded routes and guards
|
||||
9. Add error handling and loading states using signals
|
||||
10. Write unit and end-to-end tests
|
||||
11. Optimize performance and bundle size
|
||||
|
||||
## Additional Guidelines
|
||||
- Follow Angular's naming conventions (e.g., `feature.component.ts`, `feature.service.ts`)
|
||||
- Use Angular CLI commands for generating boilerplate code
|
||||
- Document components and services with clear JSDoc comments
|
||||
- Ensure accessibility compliance (WCAG 2.1) where applicable
|
||||
- Use Angular's built-in i18n for internationalization (if specified)
|
||||
- Keep code DRY by creating reusable utilities and shared modules
|
||||
- Use signals consistently for state management to ensure reactive updates
|
112
client/.github/instructions/copilot-instructions.md
vendored
Normal file
112
client/.github/instructions/copilot-instructions.md
vendored
Normal file
|
@ -0,0 +1,112 @@
|
|||
# PeerTube Client Development Instructions for Coding Agents
|
||||
|
||||
## Client Overview
|
||||
|
||||
This is the Angular frontend for PeerTube, a decentralized video hosting platform. The client is built with Angular 20+, TypeScript, and SCSS. It communicates with the PeerTube server API and provides the web interface for users, administrators, and content creators.
|
||||
|
||||
**Key Technologies:**
|
||||
- Angular 20+ with standalone components
|
||||
- TypeScript 5+
|
||||
- SCSS for styling
|
||||
- RxJS for reactive programming
|
||||
- PrimeNg and Bootstrap for UI components
|
||||
- WebdriverIO for E2E testing
|
||||
- Angular CLI
|
||||
|
||||
## Client Build and Development Commands
|
||||
|
||||
### Prerequisites (for client development)
|
||||
- Node.js 20+
|
||||
- yarn 1
|
||||
- Running PeerTube server (see ../server instructions)
|
||||
|
||||
### Essential Client Commands
|
||||
|
||||
```bash
|
||||
# From the client directory:
|
||||
cd /client
|
||||
|
||||
# 1. Install dependencies (ALWAYS first)
|
||||
yarn install --pure-lockfile
|
||||
|
||||
# 2. Development server with hot reload
|
||||
npm run dev
|
||||
|
||||
# 3. Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Client Testing Commands
|
||||
```bash
|
||||
# From client directory:
|
||||
npm run lint # ESLint for client code
|
||||
```
|
||||
|
||||
### Common Client Issues and Solutions
|
||||
|
||||
**Angular Build Failures:**
|
||||
- Always run `yarn install --pure-lockfile` after pulling changes
|
||||
- Clear `node_modules` and reinstall if dependency errors occur
|
||||
- Build may fail on memory issues: `NODE_OPTIONS="--max-old-space-size=4096" npm run build`
|
||||
- Check TypeScript errors carefully - Angular is strict about types
|
||||
|
||||
**Development Server Issues:**
|
||||
- Default port is 3000, ensure it's not in use
|
||||
- Hot reload may fail on file permission issues
|
||||
- Clear browser cache if changes don't appear
|
||||
|
||||
## Client Architecture and File Structure
|
||||
|
||||
### Client Directory Structure
|
||||
```
|
||||
/src/
|
||||
/app/
|
||||
+admin/ # Admin interface components
|
||||
+my-account/ # User account management pages
|
||||
+my-library/ # User's videos, playlists, subscriptions
|
||||
+search/ # Search functionality and results
|
||||
+shared/ # Shared Angular components, services, pipes
|
||||
+standalone/ # Standalone Angular components
|
||||
+videos/ # Video-related components (watch, upload, etc.)
|
||||
/core/ # Core services (auth, server, notifications)
|
||||
/helpers/ # Utility functions and helpers
|
||||
/menu/ # Navigation menu components
|
||||
/assets/ # Static assets (images, icons, etc.)
|
||||
/environments/ # Environment configurations
|
||||
/locale/ # Internationalization files
|
||||
/sass/ # Global SCSS styles
|
||||
```
|
||||
|
||||
### Key Client Configuration Files
|
||||
|
||||
- `angular.json` - Angular CLI workspace configuration
|
||||
- `tsconfig.json` - TypeScript configuration for client
|
||||
- `e2e/wdio*.conf.js` - WebdriverIO E2E test configurations
|
||||
- `src/environments/` - Environment-specific configurations
|
||||
|
||||
### Shared Code with Server (`../shared/`)
|
||||
|
||||
The client imports TypeScript models and utilities from the shared directory:
|
||||
- `../shared/models/` - Data models (Video, User, Channel, etc.). Import these in client code: `import { Video } from '@peertube/peertube-models'`
|
||||
- `../shared/core-utils/` - Utility functions shared between client/server. Import these in client code: `import { ... } from '@peertube/peertube-core-utils'`
|
||||
-
|
||||
|
||||
## Client Development Workflow
|
||||
|
||||
### Making Client Changes
|
||||
|
||||
1. **Angular Components:** Create/modify in `/src/app/` following existing patterns
|
||||
2. **Shared Components:** Reusable components go in `/src/app/shared/`
|
||||
3. **Services:** Core services in `/src/app/core/`, feature services with components
|
||||
4. **Styles:** Component styles in `.scss` files, global styles in `/src/sass/`
|
||||
5. **Assets:** Images, icons in `/src/assets/`
|
||||
6. **Routing:** Routes defined in feature modules or `app-routing.module.ts`
|
||||
|
||||
## Trust These Instructions
|
||||
|
||||
These instructions are comprehensive and tested specifically for client development. Only search for additional information if:
|
||||
1. Commands fail despite following instructions exactly
|
||||
2. New error messages appear that aren't documented here
|
||||
3. You need specific Angular implementation details not covered above
|
||||
|
||||
For server-side questions, refer to the server instructions in `../.github/copilot-instructions.md`.
|
|
@ -185,12 +185,11 @@
|
|||
"."
|
||||
],
|
||||
"sass": {
|
||||
"silenceDeprecations": [ "import", "mixed-decls", "color-functions", "global-builtin" ]
|
||||
"silenceDeprecations": [ "import", "color-functions", "global-builtin" ]
|
||||
}
|
||||
},
|
||||
"assets": [
|
||||
"src/assets/images",
|
||||
"src/manifest.webmanifest"
|
||||
"src/assets/images"
|
||||
],
|
||||
"styles": [
|
||||
"src/sass/application.scss"
|
||||
|
@ -214,7 +213,6 @@
|
|||
"escape-string-regexp",
|
||||
"is-plain-object",
|
||||
"parse-srcset",
|
||||
"deepmerge",
|
||||
"core-js/features/reflect",
|
||||
"hammerjs",
|
||||
"jschannel"
|
||||
|
@ -246,7 +244,7 @@
|
|||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "120kb"
|
||||
"maximumError": "140kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
|
|
|
@ -1,65 +1,73 @@
|
|||
import { browserSleep, getCheckbox, go, isCheckboxSelected } from '../utils'
|
||||
import { NSFWPolicyType } from '@peertube/peertube-models'
|
||||
import { browserSleep, go, setCheckboxEnabled } from '../utils'
|
||||
|
||||
export class AdminConfigPage {
|
||||
async navigateTo (page: 'information' | 'live' | 'general' | 'homepage') {
|
||||
const url = '/admin/settings/config/' + page
|
||||
|
||||
async navigateTo (tab: 'instance-homepage' | 'basic-configuration' | 'instance-information') {
|
||||
const waitTitles = {
|
||||
'instance-homepage': 'INSTANCE HOMEPAGE',
|
||||
'basic-configuration': 'APPEARANCE',
|
||||
'instance-information': 'INSTANCE'
|
||||
const currentUrl = await browser.getUrl()
|
||||
if (!currentUrl.endsWith(url)) {
|
||||
await go(url)
|
||||
}
|
||||
await go('/admin/settings/config/edit-custom#' + tab)
|
||||
|
||||
await $('h2=' + waitTitles[tab]).waitForDisplayed()
|
||||
await $('a.active[href="' + url + '"]').waitForDisplayed()
|
||||
}
|
||||
|
||||
async updateNSFWSetting (newValue: 'do_not_list' | 'blur' | 'display') {
|
||||
const elem = $('#instanceDefaultNSFWPolicy')
|
||||
async updateNSFWSetting (newValue: NSFWPolicyType) {
|
||||
await this.navigateTo('information')
|
||||
|
||||
const elem = $(`#instanceDefaultNSFWPolicy-${newValue} + label`)
|
||||
|
||||
await elem.waitForDisplayed()
|
||||
await elem.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
|
||||
await elem.waitForClickable()
|
||||
|
||||
return elem.selectByAttribute('value', newValue)
|
||||
return elem.click()
|
||||
}
|
||||
|
||||
updateHomepage (newValue: string) {
|
||||
return $('#instanceCustomHomepageContent').setValue(newValue)
|
||||
async updateHomepage (newValue: string) {
|
||||
await this.navigateTo('homepage')
|
||||
|
||||
return $('#homepageContent').setValue(newValue)
|
||||
}
|
||||
|
||||
async toggleSignup (enabled: boolean) {
|
||||
if (await isCheckboxSelected('signupEnabled') === enabled) return
|
||||
await this.navigateTo('general')
|
||||
|
||||
const checkbox = await getCheckbox('signupEnabled')
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
return setCheckboxEnabled('signupEnabled', enabled)
|
||||
}
|
||||
|
||||
async toggleSignupApproval (required: boolean) {
|
||||
if (await isCheckboxSelected('signupRequiresApproval') === required) return
|
||||
await this.navigateTo('general')
|
||||
|
||||
const checkbox = await getCheckbox('signupRequiresApproval')
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
return setCheckboxEnabled('signupRequiresApproval', required)
|
||||
}
|
||||
|
||||
async toggleSignupEmailVerification (required: boolean) {
|
||||
if (await isCheckboxSelected('signupRequiresEmailVerification') === required) return
|
||||
await this.navigateTo('general')
|
||||
|
||||
const checkbox = await getCheckbox('signupRequiresEmailVerification')
|
||||
return setCheckboxEnabled('signupRequiresEmailVerification', required)
|
||||
}
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
async toggleLive (enabled: boolean) {
|
||||
await this.navigateTo('live')
|
||||
|
||||
return setCheckboxEnabled('liveEnabled', enabled)
|
||||
}
|
||||
|
||||
async save () {
|
||||
const button = $('input[type=submit]')
|
||||
const button = $('my-admin-save-bar .save-button')
|
||||
|
||||
try {
|
||||
await button.waitForClickable()
|
||||
} catch {
|
||||
// The config may have not been changed
|
||||
return
|
||||
} finally {
|
||||
await browserSleep(1000) // Wait for the button to be clickable
|
||||
}
|
||||
|
||||
await button.waitForClickable()
|
||||
await button.click()
|
||||
|
||||
await browserSleep(1000)
|
||||
await button.waitForClickable({ reverse: true })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
import { browserSleep, findParentElement, go } from '../utils'
|
||||
|
||||
export class AdminRegistrationPage {
|
||||
|
||||
async navigateToRegistratonsList () {
|
||||
async navigateToRegistrationsList () {
|
||||
await go('/admin/moderation/registrations/list')
|
||||
|
||||
await $('my-registration-list').waitForDisplayed()
|
||||
}
|
||||
|
||||
async accept (username: string, moderationResponse: string) {
|
||||
const usernameEl = await $('*=' + username)
|
||||
const usernameEl = $('*=' + username)
|
||||
await usernameEl.waitForDisplayed()
|
||||
|
||||
const tr = await findParentElement(usernameEl, async el => await el.getTagName() === 'tr')
|
||||
|
||||
await tr.$('.action-cell .dropdown-root').click()
|
||||
|
||||
const accept = await $('span*=Accept this request')
|
||||
const accept = $('span*=Accept this request')
|
||||
await accept.waitForClickable()
|
||||
await accept.click()
|
||||
|
||||
const moderationResponseTextarea = await $('#moderationResponse')
|
||||
const moderationResponseTextarea = $('#moderationResponse')
|
||||
await moderationResponseTextarea.waitForDisplayed()
|
||||
|
||||
await moderationResponseTextarea.setValue(moderationResponse)
|
||||
|
@ -31,5 +30,4 @@ export class AdminRegistrationPage {
|
|||
|
||||
await browserSleep(1000)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
24
client/e2e/src/po/admin-user.po.ts
Normal file
24
client/e2e/src/po/admin-user.po.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export class AdminUserPage {
|
||||
async createUser (options: {
|
||||
username: string
|
||||
password: string
|
||||
}) {
|
||||
const { username, password } = options
|
||||
|
||||
await $('.menu-link[title=Overview]').click()
|
||||
await $('a*=Create user').click()
|
||||
|
||||
await $('#username').waitForDisplayed()
|
||||
await $('#username').setValue(username)
|
||||
await $('#password').setValue(password)
|
||||
await $('#channelName').setValue(`${username}_channel`)
|
||||
await $('#email').setValue(`${username}@example.com`)
|
||||
|
||||
const submit = $('my-user-create .primary-button')
|
||||
await submit.scrollIntoView()
|
||||
await submit.waitForClickable()
|
||||
await submit.click()
|
||||
|
||||
await $('.cell-username*=' + username).waitForDisplayed()
|
||||
}
|
||||
}
|
|
@ -1,19 +1,45 @@
|
|||
import { NSFWPolicyType } from '@peertube/peertube-models'
|
||||
import { getCheckbox } from '../utils'
|
||||
|
||||
export class AnonymousSettingsPage {
|
||||
|
||||
async openSettings () {
|
||||
const link = await $('my-header .settings-button')
|
||||
const link = $('my-header .settings-button')
|
||||
await link.waitForClickable()
|
||||
await link.click()
|
||||
|
||||
await $('my-user-video-settings').waitForDisplayed()
|
||||
}
|
||||
|
||||
async closeSettings () {
|
||||
const closeModal = $('.modal.show .modal-header > button')
|
||||
await closeModal.waitForClickable()
|
||||
await closeModal.click()
|
||||
|
||||
await $('.modal.show').waitForDisplayed({ reverse: true })
|
||||
}
|
||||
|
||||
async clickOnP2PCheckbox () {
|
||||
const p2p = await getCheckbox('p2pEnabled')
|
||||
await p2p.waitForClickable()
|
||||
|
||||
await p2p.click()
|
||||
}
|
||||
|
||||
async updateNSFW (newValue: NSFWPolicyType) {
|
||||
const nsfw = $(`#nsfwPolicy-${newValue} + label`)
|
||||
|
||||
await nsfw.waitForClickable()
|
||||
await nsfw.click()
|
||||
|
||||
await $(`#nsfwPolicy-${newValue}:checked`).waitForExist()
|
||||
}
|
||||
|
||||
async updateViolentFlag (newValue: NSFWPolicyType) {
|
||||
const nsfw = $(`#nsfwFlagViolent-${newValue} + label`)
|
||||
|
||||
await nsfw.waitForClickable()
|
||||
await nsfw.click()
|
||||
|
||||
await $(`#nsfwFlagViolent-${newValue}:checked`).waitForExist()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { browserSleep, go, isAndroid } from '../utils'
|
||||
|
||||
export class LoginPage {
|
||||
|
||||
constructor (private isMobileDevice: boolean) {
|
||||
|
||||
}
|
||||
|
||||
async login (options: {
|
||||
|
@ -51,8 +49,12 @@ export class LoginPage {
|
|||
return $('.alert-danger').getText()
|
||||
}
|
||||
|
||||
async loginAsRootUser () {
|
||||
return this.login({ username: 'root', password: 'test' + this.getSuffix() })
|
||||
loginAsRootUser () {
|
||||
return this.login({ username: 'root', password: this.getRootPassword() })
|
||||
}
|
||||
|
||||
getRootPassword () {
|
||||
return 'test' + this.getSuffix()
|
||||
}
|
||||
|
||||
loginOnPeerTube2 () {
|
||||
|
@ -64,7 +66,7 @@ export class LoginPage {
|
|||
}
|
||||
|
||||
async logout () {
|
||||
const loggedInDropdown = $('.logged-in-container .logged-in-info')
|
||||
const loggedInDropdown = $('.logged-in-container .dropdown-toggle')
|
||||
|
||||
await loggedInDropdown.waitForClickable()
|
||||
await loggedInDropdown.click()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { NSFWPolicyType } from '@peertube/peertube-models'
|
||||
import { getCheckbox, go, selectCustomSelect } from '../utils'
|
||||
|
||||
export class MyAccountPage {
|
||||
|
||||
navigateToMyVideos () {
|
||||
return $('a[href="/my-library/videos"]').click()
|
||||
}
|
||||
|
@ -14,20 +14,34 @@ export class MyAccountPage {
|
|||
return $('a[href="/my-library/history/videos"]').click()
|
||||
}
|
||||
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
// My account settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
navigateToMySettings () {
|
||||
return $('a[href="/my-account"]').click()
|
||||
}
|
||||
|
||||
async updateNSFW (newValue: 'do_not_list' | 'blur' | 'display') {
|
||||
const nsfw = $('#nsfwPolicy')
|
||||
async updateNSFW (newValue: NSFWPolicyType) {
|
||||
const nsfw = $(`#nsfwPolicy-${newValue} + label`)
|
||||
|
||||
await nsfw.waitForDisplayed()
|
||||
await nsfw.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
|
||||
await nsfw.waitForClickable()
|
||||
|
||||
await nsfw.selectByAttribute('value', newValue)
|
||||
await nsfw.click()
|
||||
|
||||
await this.submitVideoSettings()
|
||||
}
|
||||
|
||||
async updateViolentFlag (newValue: NSFWPolicyType) {
|
||||
const nsfw = $(`#nsfwFlagViolent-${newValue} + label`)
|
||||
|
||||
await nsfw.waitForDisplayed()
|
||||
await nsfw.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
|
||||
await nsfw.waitForClickable()
|
||||
|
||||
await nsfw.click()
|
||||
|
||||
await this.submitVideoSettings()
|
||||
}
|
||||
|
@ -51,10 +65,28 @@ export class MyAccountPage {
|
|||
await submit.click()
|
||||
}
|
||||
|
||||
// My account Videos
|
||||
async updateEmail (email: string, password: string) {
|
||||
const emailInput = $('my-account-change-email #new-email')
|
||||
await emailInput.waitForDisplayed()
|
||||
await emailInput.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
|
||||
await emailInput.setValue(email)
|
||||
|
||||
const passwordInput = $('my-account-change-email #password')
|
||||
await passwordInput.waitForDisplayed()
|
||||
await passwordInput.setValue(password)
|
||||
|
||||
const submit = $('my-account-change-email input[type=submit]')
|
||||
await submit.waitForClickable()
|
||||
await submit.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
|
||||
await submit.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// My account videos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async removeVideo (name: string) {
|
||||
const container = await this.getVideoElement(name)
|
||||
const container = await this.getVideoRow(name)
|
||||
|
||||
await container.$('my-action-dropdown .dropdown-toggle').click()
|
||||
|
||||
|
@ -76,8 +108,8 @@ export class MyAccountPage {
|
|||
}
|
||||
|
||||
async countVideos (names: string[]) {
|
||||
const elements = await $$('.video').filter(async e => {
|
||||
const t = await e.$('.video-name').getText()
|
||||
const elements = await $$('.video-cell-name .name').filter(async e => {
|
||||
const t = await e.getText()
|
||||
|
||||
return names.some(n => t.includes(n))
|
||||
})
|
||||
|
@ -85,7 +117,21 @@ export class MyAccountPage {
|
|||
return elements.length
|
||||
}
|
||||
|
||||
async getVideoRow (name: string) {
|
||||
let el = $('.name*=' + name)
|
||||
|
||||
await el.waitForDisplayed()
|
||||
|
||||
while (await el.getTagName() !== 'tr') {
|
||||
el = el.parentElement()
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// My account playlists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getPlaylistVideosText (name: string) {
|
||||
const elem = await this.getPlaylist(name)
|
||||
|
@ -125,7 +171,7 @@ export class MyAccountPage {
|
|||
await selectCustomSelect('videoChannelId', 'Main root channel')
|
||||
await selectCustomSelect('privacy', privacy)
|
||||
|
||||
const submit = await $('form input[type=submit]')
|
||||
const submit = $('form input[type=submit]')
|
||||
await submit.waitForClickable()
|
||||
await submit.scrollIntoView()
|
||||
await submit.click()
|
||||
|
@ -135,33 +181,11 @@ export class MyAccountPage {
|
|||
})
|
||||
}
|
||||
|
||||
// My account Videos
|
||||
|
||||
private async getVideoElement (name: string) {
|
||||
const video = async () => {
|
||||
const videos = await $$('.video').filter(async e => {
|
||||
const t = await e.$('.video-name').getText()
|
||||
|
||||
return t.includes(name)
|
||||
})
|
||||
|
||||
return videos[0]
|
||||
}
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
return (await video()).isDisplayed()
|
||||
})
|
||||
|
||||
return video()
|
||||
}
|
||||
|
||||
// My account playlists
|
||||
|
||||
private async getPlaylist (name: string) {
|
||||
const playlist = () => {
|
||||
return $$('my-video-playlist-miniature')
|
||||
.filter(async e => {
|
||||
const t = await e.$('.miniature-name').getText()
|
||||
const t = await e.$('img').getAttribute('aria-label')
|
||||
|
||||
return t.includes(name)
|
||||
})
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils'
|
||||
|
||||
export class PlayerPage {
|
||||
|
||||
getWatchVideoPlayerCurrentTime () {
|
||||
const elem = $('video')
|
||||
|
||||
|
@ -10,7 +9,7 @@ export class PlayerPage {
|
|||
: elem.getProperty('currentTime')
|
||||
|
||||
return p.then(t => parseInt(t + '', 10))
|
||||
.then(t => Math.ceil(t))
|
||||
.then(t => Math.ceil(t))
|
||||
}
|
||||
|
||||
waitUntilPlaylistInfo (text: string, maxTime: number) {
|
||||
|
@ -23,9 +22,11 @@ export class PlayerPage {
|
|||
}
|
||||
|
||||
waitUntilPlayerWrapper () {
|
||||
return browser.waitUntil(async () => {
|
||||
return !!(await $('#placeholder-preview'))
|
||||
})
|
||||
return $('#video-wrapper').waitForExist()
|
||||
}
|
||||
|
||||
waitUntilPlaying () {
|
||||
return $('.video-js.vjs-playing').waitForDisplayed()
|
||||
}
|
||||
|
||||
async playAndPauseVideo (isAutoplay: boolean, waitUntilSec: number) {
|
||||
|
@ -66,16 +67,36 @@ export class PlayerPage {
|
|||
return this.clickOnPlayButton()
|
||||
}
|
||||
|
||||
private async clickOnPlayButton () {
|
||||
const playButton = () => $('.vjs-big-play-button')
|
||||
getPlayButton () {
|
||||
return $('.vjs-big-play-button')
|
||||
}
|
||||
|
||||
await playButton().waitForClickable()
|
||||
await playButton().click()
|
||||
getNSFWContentText () {
|
||||
return $('.video-js .nsfw-info').getText()
|
||||
}
|
||||
|
||||
getNSFWDetailsContent () {
|
||||
return $('.video-js .nsfw-details-content')
|
||||
}
|
||||
|
||||
getMoreNSFWInfoButton () {
|
||||
return $('.video-js .nsfw-info button')
|
||||
}
|
||||
|
||||
async hasPoster () {
|
||||
const property = await $('.video-js .vjs-poster').getCSSProperty('background-image')
|
||||
|
||||
return property.value.startsWith('url(')
|
||||
}
|
||||
|
||||
private async clickOnPlayButton () {
|
||||
await this.getPlayButton().waitForClickable()
|
||||
await this.getPlayButton().click()
|
||||
}
|
||||
|
||||
async fillEmbedVideoPassword (videoPassword: string) {
|
||||
const videoPasswordInput = $('input#video-password-input')
|
||||
const confirmButton = await $('button#video-password-submit')
|
||||
const confirmButton = $('button#video-password-submit')
|
||||
|
||||
await videoPasswordInput.clearValue()
|
||||
await videoPasswordInput.setValue(videoPassword)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { getCheckbox } from '../utils'
|
||||
|
||||
export class SignupPage {
|
||||
|
||||
getRegisterMenuButton () {
|
||||
return $('.create-account-button')
|
||||
}
|
||||
|
@ -47,7 +46,7 @@ export class SignupPage {
|
|||
await $('#displayName').setValue(options.displayName || `${options.username} display name`)
|
||||
|
||||
await $('#username').setValue(options.username)
|
||||
await $('#password').setValue(options.password || 'password')
|
||||
await $('#password').setValue(options.password || 'superpassword')
|
||||
|
||||
// Fix weird bug on firefox that "cannot scroll into view" when using just `setValue`
|
||||
await $('#email').scrollIntoView({ block: 'center' })
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { browserSleep, go } from '../utils'
|
||||
import { browserSleep, findParentElement, go } from '../utils'
|
||||
|
||||
export class VideoListPage {
|
||||
|
||||
constructor (private isMobileDevice: boolean, private isSafari: boolean) {
|
||||
|
||||
}
|
||||
|
||||
async goOnVideosList () {
|
||||
|
@ -53,30 +51,54 @@ export class VideoListPage {
|
|||
await this.waitForList()
|
||||
}
|
||||
|
||||
async getNSFWFilter () {
|
||||
async getNSFWFilterText () {
|
||||
const el = $('.active-filter*=Sensitive')
|
||||
|
||||
await el.waitForDisplayed()
|
||||
|
||||
return el
|
||||
return el.getText()
|
||||
}
|
||||
|
||||
async getVideosListName () {
|
||||
const elems = await $$('.videos .video-miniature .video-name')
|
||||
const elems = $$('.videos .video-miniature .video-name')
|
||||
const texts = await elems.map(e => e.getText())
|
||||
|
||||
return texts.map(t => t.trim())
|
||||
}
|
||||
|
||||
videoExists (name: string) {
|
||||
isVideoDisplayed (name: string) {
|
||||
return $('.video-name=' + name).isDisplayed()
|
||||
}
|
||||
|
||||
async videoIsBlurred (name: string) {
|
||||
const filter = await $('.video-name=' + name).getCSSProperty('filter')
|
||||
async isVideoBlurred (name: string) {
|
||||
const miniature = await this.getVideoMiniature(name)
|
||||
const filter = await miniature.$('my-video-thumbnail img').getCSSProperty('filter')
|
||||
|
||||
return filter.value !== 'none'
|
||||
}
|
||||
|
||||
async hasVideoWarning (name: string) {
|
||||
const miniature = await this.getVideoMiniature(name)
|
||||
|
||||
return miniature.$('.nsfw-warning').isDisplayed()
|
||||
}
|
||||
|
||||
async expectVideoNSFWTooltip (name: string, summary?: string) {
|
||||
const miniature = await this.getVideoMiniature(name)
|
||||
|
||||
const warning = miniature.$('.nsfw-warning')
|
||||
await warning.waitForDisplayed()
|
||||
|
||||
expect(await warning.getAttribute('aria-label')).toEqual(summary)
|
||||
}
|
||||
|
||||
private async getVideoMiniature (name: string) {
|
||||
const videoName = $('.video-name=' + name)
|
||||
await videoName.waitForDisplayed()
|
||||
|
||||
return findParentElement(videoName, async el => await el.getTagName() === 'my-video-miniature')
|
||||
}
|
||||
|
||||
async clickOnVideo (videoName: string) {
|
||||
const video = async () => {
|
||||
const videos = await $$('.videos .video-miniature .video-name').filter(async e => {
|
||||
|
@ -92,9 +114,8 @@ export class VideoListPage {
|
|||
const elem = await video()
|
||||
|
||||
return elem?.isClickable()
|
||||
});
|
||||
|
||||
(await video()).click()
|
||||
})
|
||||
;(await video()).click()
|
||||
|
||||
await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
|
||||
}
|
||||
|
@ -116,8 +137,4 @@ export class VideoListPage {
|
|||
private waitForList () {
|
||||
return $('.videos .video-miniature .video-name').waitForDisplayed()
|
||||
}
|
||||
|
||||
private waitForTitle (title: string) {
|
||||
return $('h1=' + title).waitForDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
148
client/e2e/src/po/video-manage.ts
Normal file
148
client/e2e/src/po/video-manage.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
import { clickOnRadio, getCheckbox, go, isRadioSelected, selectCustomSelect, setCheckboxEnabled } from '../utils'
|
||||
|
||||
export abstract class VideoManage {
|
||||
async clickOnSave () {
|
||||
const button = this.getSaveButton()
|
||||
await button.waitForClickable()
|
||||
await button.click()
|
||||
|
||||
await this.waitForSaved()
|
||||
}
|
||||
|
||||
async clickOnWatch () {
|
||||
// Simulate the click, because the button opens a new tab
|
||||
const button = $('.watch-save > my-button[icon=external-link] a')
|
||||
await button.waitForClickable()
|
||||
|
||||
await go(await button.getAttribute('href'))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setAsNSFW (options: {
|
||||
violent?: boolean
|
||||
summary?: string
|
||||
} = {}) {
|
||||
await this.goOnPage('Moderation')
|
||||
|
||||
const checkbox = await getCheckbox('nsfw')
|
||||
await checkbox.waitForClickable()
|
||||
|
||||
await checkbox.click()
|
||||
|
||||
if (options.violent) {
|
||||
await setCheckboxEnabled('nsfwFlagViolent', true)
|
||||
}
|
||||
|
||||
if (options.summary) {
|
||||
await $('#nsfwSummary').setValue(options.summary)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setAsPublic () {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
return selectCustomSelect('privacy', 'Public')
|
||||
}
|
||||
|
||||
async setAsPrivate () {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
return selectCustomSelect('privacy', 'Private')
|
||||
}
|
||||
|
||||
async setAsPasswordProtected (videoPassword: string) {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
selectCustomSelect('privacy', 'Password protected')
|
||||
|
||||
const videoPasswordInput = $('input#videoPassword')
|
||||
await videoPasswordInput.waitForClickable()
|
||||
await videoPasswordInput.clearValue()
|
||||
|
||||
return videoPasswordInput.setValue(videoPassword)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async scheduleUpload () {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
selectCustomSelect('privacy', 'Scheduled')
|
||||
|
||||
const input = this.getScheduleInput()
|
||||
await input.waitForClickable()
|
||||
await input.click()
|
||||
|
||||
const nextMonth = $('.p-datepicker-next-button')
|
||||
await nextMonth.click()
|
||||
|
||||
await $('.p-datepicker-calendar td[aria-label="1"] > span').click()
|
||||
await $('.p-datepicker-calendar').waitForDisplayed({ reverse: true, timeout: 15000 }) // Can be slow
|
||||
}
|
||||
|
||||
getScheduleInput () {
|
||||
return $('#schedulePublicationAt input')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setNormalLive () {
|
||||
await this.goOnPage('Live settings')
|
||||
|
||||
await clickOnRadio('permanentLiveFalse')
|
||||
}
|
||||
|
||||
async setPermanentLive () {
|
||||
await this.goOnPage('Live settings')
|
||||
|
||||
await clickOnRadio('permanentLiveTrue')
|
||||
}
|
||||
|
||||
async getLiveState () {
|
||||
await this.goOnPage('Live settings')
|
||||
|
||||
if (await isRadioSelected('permanentLiveTrue')) return 'permanent'
|
||||
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async refresh (videoName: string) {
|
||||
await browser.refresh()
|
||||
await browser.waitUntil(async () => {
|
||||
const url = await browser.getUrl()
|
||||
|
||||
return url.includes('/videos/manage')
|
||||
})
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
return await $('#name').getValue() === videoName
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
protected getSaveButton () {
|
||||
return $('.save-button > button:not([disabled])')
|
||||
}
|
||||
|
||||
protected waitForSaved () {
|
||||
return $('.save-button > button[disabled], my-manage-errors').waitForDisplayed()
|
||||
}
|
||||
|
||||
protected async goOnPage (page: 'Main information' | 'Moderation' | 'Live settings') {
|
||||
const urls = {
|
||||
'Main information': '',
|
||||
'Moderation': 'moderation',
|
||||
'Live settings': 'live'
|
||||
}
|
||||
|
||||
const el = $(`my-video-manage-container .menu a[href*="/${urls[page]}"]`)
|
||||
await el.waitForClickable()
|
||||
await el.click()
|
||||
}
|
||||
}
|
80
client/e2e/src/po/video-publish.po.ts
Normal file
80
client/e2e/src/po/video-publish.po.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { join } from 'path'
|
||||
import { VideoManage } from './video-manage'
|
||||
import { FIXTURE_URLS } from '../utils'
|
||||
|
||||
export class VideoPublishPage extends VideoManage {
|
||||
async navigateTo (tab?: 'Go live') {
|
||||
const publishButton = $('.publish-button > a')
|
||||
|
||||
await publishButton.waitForClickable()
|
||||
await publishButton.click()
|
||||
|
||||
await $('.upload-video-container').waitForDisplayed()
|
||||
|
||||
if (tab) {
|
||||
const el = $(`.nav-link*=${tab}`)
|
||||
await el.waitForClickable()
|
||||
await el.click()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async uploadVideo (fixtureName: 'video.mp4' | 'video2.mp4' | 'video3.mp4') {
|
||||
const fileToUpload = join(__dirname, '../../fixtures/' + fixtureName)
|
||||
const fileInputSelector = '.upload-video-container input[type=file]'
|
||||
const parentFileInput = '.upload-video-container .button-file'
|
||||
|
||||
// Avoid sending keys on non visible element
|
||||
await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
|
||||
await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
|
||||
|
||||
await browser.pause(1000)
|
||||
|
||||
const elem = $(fileInputSelector)
|
||||
await elem.chooseFile(fileToUpload)
|
||||
|
||||
// Wait for the upload to finish
|
||||
await this.getSaveButton().waitForClickable()
|
||||
}
|
||||
|
||||
async importVideo () {
|
||||
const tab = $('.nav-link*=Import with URL')
|
||||
await tab.waitForClickable()
|
||||
await tab.click()
|
||||
|
||||
const input = $('#targetUrl')
|
||||
await input.waitForDisplayed()
|
||||
await input.setValue(FIXTURE_URLS.IMPORT_URL)
|
||||
|
||||
const submit = $('.first-step-block .primary-button:not([disabled])')
|
||||
await submit.waitForClickable()
|
||||
await submit.click()
|
||||
|
||||
// Wait for the import to finish
|
||||
await this.getSaveButton().waitForClickable({ timeout: 15000 }) // Can be slow
|
||||
}
|
||||
|
||||
async publishLive () {
|
||||
await $('#permanentLiveTrue').parentElement().click()
|
||||
|
||||
const submit = $('.upload-video-container .primary-button:not([disabled])')
|
||||
await submit.waitForClickable()
|
||||
await submit.click()
|
||||
|
||||
await this.getSaveButton().waitForClickable()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async validSecondStep (videoName: string) {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
const nameInput = $('input#name')
|
||||
await nameInput.scrollIntoView()
|
||||
await nameInput.clearValue()
|
||||
await nameInput.setValue(videoName)
|
||||
|
||||
await this.clickOnSave()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
export class VideoUpdatePage {
|
||||
import { VideoManage } from './video-manage'
|
||||
|
||||
export class VideoUpdatePage extends VideoManage {
|
||||
async updateName (videoName: string) {
|
||||
const nameInput = $('input#name')
|
||||
|
||||
|
@ -7,14 +8,4 @@ export class VideoUpdatePage {
|
|||
await nameInput.clearValue()
|
||||
await nameInput.setValue(videoName)
|
||||
}
|
||||
|
||||
async validUpdate () {
|
||||
const submitButton = await this.getSubmitButton()
|
||||
|
||||
return submitButton.click()
|
||||
}
|
||||
|
||||
private getSubmitButton () {
|
||||
return $('.submit-container .action-button')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { join } from 'path'
|
||||
import { getCheckbox, selectCustomSelect } from '../utils'
|
||||
|
||||
export class VideoUploadPage {
|
||||
async navigateTo () {
|
||||
const publishButton = await $('.publish-button > a')
|
||||
|
||||
await publishButton.waitForClickable()
|
||||
await publishButton.click()
|
||||
|
||||
await $('.upload-video-container').waitForDisplayed()
|
||||
}
|
||||
|
||||
async uploadVideo (fixtureName: 'video.mp4' | 'video2.mp4' | 'video3.mp4') {
|
||||
const fileToUpload = join(__dirname, '../../fixtures/' + fixtureName)
|
||||
const fileInputSelector = '.upload-video-container input[type=file]'
|
||||
const parentFileInput = '.upload-video-container .button-file'
|
||||
|
||||
// Avoid sending keys on non visible element
|
||||
await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
|
||||
await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
|
||||
|
||||
await browser.pause(1000)
|
||||
|
||||
const elem = await $(fileInputSelector)
|
||||
await elem.chooseFile(fileToUpload)
|
||||
|
||||
// Wait for the upload to finish
|
||||
await browser.waitUntil(async () => {
|
||||
const warning = await $('=Publish will be available when upload is finished').isDisplayed()
|
||||
const progress = await $('.progress-container=100%').isDisplayed()
|
||||
|
||||
return !warning && progress
|
||||
})
|
||||
}
|
||||
|
||||
async setAsNSFW () {
|
||||
const checkbox = await getCheckbox('nsfw')
|
||||
await checkbox.waitForClickable()
|
||||
|
||||
return checkbox.click()
|
||||
}
|
||||
|
||||
async validSecondUploadStep (videoName: string) {
|
||||
const nameInput = $('input#name')
|
||||
await nameInput.clearValue()
|
||||
await nameInput.setValue(videoName)
|
||||
|
||||
const button = this.getSecondStepSubmitButton()
|
||||
await button.waitForClickable()
|
||||
|
||||
await button.click()
|
||||
|
||||
return browser.waitUntil(async () => {
|
||||
return (await browser.getUrl()).includes('/w/')
|
||||
})
|
||||
}
|
||||
|
||||
setAsPublic () {
|
||||
return selectCustomSelect('privacy', 'Public')
|
||||
}
|
||||
|
||||
setAsPrivate () {
|
||||
return selectCustomSelect('privacy', 'Private')
|
||||
}
|
||||
|
||||
async setAsPasswordProtected (videoPassword: string) {
|
||||
selectCustomSelect('privacy', 'Password protected')
|
||||
|
||||
const videoPasswordInput = $('input#videoPassword')
|
||||
await videoPasswordInput.waitForClickable()
|
||||
await videoPasswordInput.clearValue()
|
||||
|
||||
return videoPasswordInput.setValue(videoPassword)
|
||||
}
|
||||
|
||||
private getSecondStepSubmitButton () {
|
||||
return $('.submit-container my-button')
|
||||
}
|
||||
}
|
|
@ -1,24 +1,16 @@
|
|||
import { browserSleep, FIXTURE_URLS, go } from '../utils'
|
||||
|
||||
export class VideoWatchPage {
|
||||
|
||||
constructor (private isMobileDevice: boolean, private isSafari: boolean) {
|
||||
|
||||
}
|
||||
|
||||
waitWatchVideoName (videoName: string) {
|
||||
waitWatchVideoName (videoName: string, maxTime?: number) {
|
||||
if (this.isSafari) return browserSleep(5000)
|
||||
|
||||
// On mobile we display the first node, on desktop the second one
|
||||
const index = this.isMobileDevice ? 0 : 1
|
||||
|
||||
return browser.waitUntil(async () => {
|
||||
if (!await $('.video-info .video-info-name').isExisting()) return false
|
||||
|
||||
const elem = await $$('.video-info .video-info-name')[index]
|
||||
|
||||
return (await elem.getText()).includes(videoName) && elem.isDisplayed()
|
||||
})
|
||||
return (await this.getVideoName()) === videoName
|
||||
}, { timeout: maxTime })
|
||||
}
|
||||
|
||||
getVideoName () {
|
||||
|
@ -48,7 +40,7 @@ export class VideoWatchPage {
|
|||
}
|
||||
|
||||
isPrivacyWarningDisplayed () {
|
||||
return $('my-privacy-concerns').isDisplayed()
|
||||
return $('.privacy-concerns-text').isDisplayed()
|
||||
}
|
||||
|
||||
async goOnAssociatedEmbed (passwordProtected = false) {
|
||||
|
@ -82,23 +74,112 @@ export class VideoWatchPage {
|
|||
return go(FIXTURE_URLS.HLS_PLAYLIST_EMBED)
|
||||
}
|
||||
|
||||
async clickOnUpdate () {
|
||||
await this.clickOnMoreDropdownIcon()
|
||||
getModalTitleEl () {
|
||||
return $('.modal-content .modal-title')
|
||||
}
|
||||
|
||||
const items = await $$('.dropdown-menu.show .dropdown-item')
|
||||
confirmModal () {
|
||||
return $('.modal-content .modal-footer .primary-button').click()
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const href = await item.getAttribute('href')
|
||||
private async getVideoNameElement () {
|
||||
// We have 2 video info name block, pick the first that is not empty
|
||||
const elem = async () => {
|
||||
const elems = await $$('.video-info-first-row .video-info-name').filter(e => e.isDisplayed())
|
||||
|
||||
if (href?.includes('/update/')) {
|
||||
await item.click()
|
||||
return
|
||||
return elems[0]
|
||||
}
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const e = await elem()
|
||||
|
||||
return e?.isDisplayed()
|
||||
})
|
||||
|
||||
return elem()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Video password
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isPasswordProtected () {
|
||||
return $('#confirmInput').isExisting()
|
||||
}
|
||||
|
||||
async fillVideoPassword (videoPassword: string) {
|
||||
const videoPasswordInput = $('input#confirmInput')
|
||||
await videoPasswordInput.waitForClickable()
|
||||
await videoPasswordInput.clearValue()
|
||||
await videoPasswordInput.setValue(videoPassword)
|
||||
|
||||
const confirmButton = $('input[value="Confirm"]')
|
||||
await confirmButton.waitForClickable()
|
||||
return confirmButton.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Video actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async like () {
|
||||
const likeButton = $('.action-button-like')
|
||||
const isActivated = (await likeButton.getAttribute('class')).includes('activated')
|
||||
|
||||
let count: number
|
||||
try {
|
||||
count = parseInt(await $('.action-button-like > .count').getText())
|
||||
} catch (error) {
|
||||
count = 0
|
||||
}
|
||||
|
||||
await likeButton.waitForClickable()
|
||||
await likeButton.click()
|
||||
|
||||
if (isActivated) {
|
||||
if (count === 1) {
|
||||
return expect(!await $('.action-button-like > .count').isExisting())
|
||||
} else {
|
||||
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count - 1)
|
||||
}
|
||||
} else {
|
||||
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count + 1)
|
||||
}
|
||||
}
|
||||
|
||||
clickOnSave () {
|
||||
return $('.action-button-save').click()
|
||||
async clickOnManage () {
|
||||
await this.clickOnMoreDropdownIcon()
|
||||
|
||||
// We need the await expression
|
||||
return $$('.dropdown-menu.show .dropdown-item').mapSeries(async item => {
|
||||
const content = await item.getText()
|
||||
|
||||
if (content.includes('Manage')) {
|
||||
await item.click()
|
||||
await $('#name').waitForClickable()
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async clickOnMoreDropdownIcon () {
|
||||
const dropdown = $('my-video-actions-dropdown .action-button')
|
||||
await dropdown.scrollIntoView({ block: 'center' })
|
||||
await dropdown.click()
|
||||
|
||||
await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Playlists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async clickOnSave () {
|
||||
const button = $('.action-button-save')
|
||||
|
||||
await button.scrollIntoView({ block: 'center' })
|
||||
|
||||
return button.click()
|
||||
}
|
||||
|
||||
async createPlaylist (name: string) {
|
||||
|
@ -123,107 +204,41 @@ export class VideoWatchPage {
|
|||
return playlist().click()
|
||||
}
|
||||
|
||||
waitUntilVideoName (name: string, maxTime: number) {
|
||||
return browser.waitUntil(async () => {
|
||||
return (await this.getVideoName()) === name
|
||||
}, { timeout: maxTime })
|
||||
}
|
||||
|
||||
async clickOnMoreDropdownIcon () {
|
||||
const dropdown = $('my-video-actions-dropdown .action-button')
|
||||
await dropdown.click()
|
||||
|
||||
await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
|
||||
}
|
||||
|
||||
private async getVideoNameElement () {
|
||||
// We have 2 video info name block, pick the first that is not empty
|
||||
const elem = async () => {
|
||||
const elems = await $$('.video-info-first-row .video-info-name').filter(e => e.isDisplayed())
|
||||
|
||||
return elems[0]
|
||||
}
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const e = await elem()
|
||||
|
||||
return e?.isDisplayed()
|
||||
})
|
||||
|
||||
return elem()
|
||||
}
|
||||
|
||||
isPasswordProtected () {
|
||||
return $('#confirmInput').isExisting()
|
||||
}
|
||||
|
||||
async fillVideoPassword (videoPassword: string) {
|
||||
const videoPasswordInput = await $('input#confirmInput')
|
||||
await videoPasswordInput.waitForClickable()
|
||||
await videoPasswordInput.clearValue()
|
||||
await videoPasswordInput.setValue(videoPassword)
|
||||
|
||||
const confirmButton = await $('input[value="Confirm"]')
|
||||
await confirmButton.waitForClickable()
|
||||
return confirmButton.click()
|
||||
}
|
||||
|
||||
async like () {
|
||||
const likeButton = await $('.action-button-like')
|
||||
const isActivated = (await likeButton.getAttribute('class')).includes('activated')
|
||||
|
||||
let count: number
|
||||
try {
|
||||
count = parseInt(await $('.action-button-like > .count').getText())
|
||||
} catch (error) {
|
||||
count = 0
|
||||
}
|
||||
|
||||
await likeButton.waitForClickable()
|
||||
await likeButton.click()
|
||||
|
||||
if (isActivated) {
|
||||
if (count === 1) {
|
||||
return expect(!await $('.action-button-like > .count').isExisting())
|
||||
} else {
|
||||
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count - 1)
|
||||
}
|
||||
} else {
|
||||
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count + 1)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Comments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async createThread (comment: string) {
|
||||
const textarea = await $('my-video-comment-add textarea')
|
||||
const textarea = $('my-video-comment-add textarea')
|
||||
await textarea.waitForClickable()
|
||||
|
||||
await textarea.setValue(comment)
|
||||
|
||||
const confirmButton = await $('.comment-buttons .primary-button')
|
||||
const confirmButton = $('.comment-buttons .primary-button')
|
||||
await confirmButton.waitForClickable()
|
||||
await confirmButton.click()
|
||||
|
||||
const createdComment = await (await $('.comment-html p')).getText()
|
||||
const createdComment = await $('.comment-html p').getText()
|
||||
|
||||
return expect(createdComment).toBe(comment)
|
||||
}
|
||||
|
||||
async createReply (comment: string) {
|
||||
const replyButton = await $('button.comment-action-reply')
|
||||
const replyButton = $('button.comment-action-reply')
|
||||
await replyButton.waitForClickable()
|
||||
await replyButton.scrollIntoView({ block: 'center' })
|
||||
await replyButton.click()
|
||||
|
||||
const textarea = await $('my-video-comment my-video-comment-add textarea')
|
||||
const textarea = $('my-video-comment my-video-comment-add textarea')
|
||||
await textarea.waitForClickable()
|
||||
await textarea.setValue(comment)
|
||||
|
||||
const confirmButton = await $('my-video-comment .comment-buttons .primary-button')
|
||||
const confirmButton = $('my-video-comment .comment-buttons .primary-button')
|
||||
await confirmButton.waitForClickable()
|
||||
await replyButton.scrollIntoView({ block: 'center' })
|
||||
await confirmButton.click()
|
||||
|
||||
const createdComment = await $('.is-child .comment-html p')
|
||||
const createdComment = $('.is-child .comment-html p')
|
||||
await createdComment.waitForDisplayed()
|
||||
|
||||
return expect(await createdComment.getText()).toBe(comment)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari } from '../utils'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
|
||||
|
||||
describe('Live all workflow', () => {
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
@ -10,9 +10,7 @@ describe('Live all workflow', () => {
|
|||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
playerPage = new PlayerPage()
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should go to the live page', async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari } from '../utils'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
|
||||
|
||||
async function checkCorrectlyPlay (playerPage: PlayerPage) {
|
||||
await playerPage.playAndPauseVideo(false, 2)
|
||||
|
@ -22,9 +22,7 @@ describe('Private videos all workflow', () => {
|
|||
loginPage = new LoginPage(isMobileDevice())
|
||||
playerPage = new PlayerPage()
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should log in', async () => {
|
||||
|
|
|
@ -2,10 +2,10 @@ import { LoginPage } from '../po/login.po'
|
|||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoListPage } from '../po/video-list.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoUpdatePage } from '../po/video-update.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
function isUploadUnsupported () {
|
||||
if (isMobileDevice() || isSafari()) {
|
||||
|
@ -19,7 +19,7 @@ function isUploadUnsupported () {
|
|||
describe('Videos all workflow', () => {
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let videoListPage: VideoListPage
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let videoUpdatePage: VideoUpdatePage
|
||||
let myAccountPage: MyAccountPage
|
||||
let loginPage: LoginPage
|
||||
|
@ -46,16 +46,14 @@ describe('Videos all workflow', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoUpdatePage = new VideoUpdatePage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
playerPage = new PlayerPage()
|
||||
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should log in', async () => {
|
||||
|
@ -70,10 +68,10 @@ describe('Videos all workflow', () => {
|
|||
it('Should upload a video', async () => {
|
||||
if (isUploadUnsupported()) return
|
||||
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoPublishPage.navigateTo()
|
||||
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
return videoUploadPage.validSecondUploadStep(videoName)
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep(videoName)
|
||||
})
|
||||
|
||||
it('Should list videos', async () => {
|
||||
|
@ -124,12 +122,12 @@ describe('Videos all workflow', () => {
|
|||
|
||||
await go(videoWatchUrl)
|
||||
|
||||
await videoWatchPage.clickOnUpdate()
|
||||
await videoWatchPage.clickOnManage()
|
||||
|
||||
videoName += ' updated'
|
||||
await videoUpdatePage.updateName(videoName)
|
||||
|
||||
await videoUpdatePage.validUpdate()
|
||||
await videoUpdatePage.clickOnSave()
|
||||
await videoUpdatePage.clickOnWatch()
|
||||
|
||||
const name = await videoWatchPage.getVideoName()
|
||||
expect(name).toEqual(videoName)
|
||||
|
@ -145,10 +143,11 @@ describe('Videos all workflow', () => {
|
|||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
await browser.pause(5000)
|
||||
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoPublishPage.navigateTo()
|
||||
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(video2Name)
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.validSecondStep(video2Name)
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.clickOnSave()
|
||||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
|
@ -173,7 +172,7 @@ describe('Videos all workflow', () => {
|
|||
|
||||
await myAccountPage.playPlaylist()
|
||||
|
||||
await videoWatchPage.waitUntilVideoName(video2Name, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(video2Name, 40 * 1000)
|
||||
})
|
||||
|
||||
it('Should watch the Web Video playlist in the embed', async () => {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Custom server defaults', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
||||
|
@ -12,10 +12,10 @@ describe('Custom server defaults', () => {
|
|||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Publish default values', function () {
|
||||
|
@ -24,10 +24,11 @@ describe('Custom server defaults', () => {
|
|||
})
|
||||
|
||||
it('Should upload a video with custom default values', async function () {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
const videoUrl = await browser.getUrl()
|
||||
|
@ -66,11 +67,12 @@ describe('Custom server defaults', () => {
|
|||
|
||||
before(async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.setAsPublic()
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.setAsPublic()
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
videoUrl = await browser.getUrl()
|
||||
|
|
393
client/e2e/src/suites-local/nsfw.e2e-spec.ts
Normal file
393
client/e2e/src/suites-local/nsfw.e2e-spec.ts
Normal file
|
@ -0,0 +1,393 @@
|
|||
import { NSFWPolicyType } from '@peertube/peertube-models'
|
||||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { AdminUserPage } from '../po/admin-user.po'
|
||||
import { AnonymousSettingsPage } from '../po/anonymous-settings.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoListPage } from '../po/video-list.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoSearchPage } from '../po/video-search.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('NSFW', () => {
|
||||
let videoListPage: VideoListPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
let loginPage: LoginPage
|
||||
let adminUserPage: AdminUserPage
|
||||
let myAccountPage: MyAccountPage
|
||||
let videoSearchPage: VideoSearchPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let playerPage: PlayerPage
|
||||
let anonymousSettingsPage: AnonymousSettingsPage
|
||||
|
||||
const seed = Math.random()
|
||||
const nsfwVideo = seed + ' - nsfw'
|
||||
const violentVideo = seed + ' - violent'
|
||||
const normalVideo = seed + ' - normal'
|
||||
|
||||
let videoUrl: string
|
||||
|
||||
async function checkVideo (options: {
|
||||
policy: NSFWPolicyType
|
||||
videoName: string
|
||||
nsfwTooltip?: string
|
||||
}) {
|
||||
const { policy, videoName, nsfwTooltip } = options
|
||||
|
||||
if (policy === 'do_not_list') {
|
||||
expect(await videoListPage.isVideoDisplayed(videoName)).toBeFalsy()
|
||||
} else if (policy === 'warn') {
|
||||
expect(await videoListPage.isVideoDisplayed(videoName)).toBeTruthy()
|
||||
expect(await videoListPage.isVideoBlurred(videoName)).toBeFalsy()
|
||||
expect(await videoListPage.hasVideoWarning(videoName)).toBeTruthy()
|
||||
} else if (policy === 'blur') {
|
||||
expect(await videoListPage.isVideoDisplayed(videoName)).toBeTruthy()
|
||||
expect(await videoListPage.isVideoBlurred(videoName)).toBeTruthy()
|
||||
expect(await videoListPage.hasVideoWarning(videoName)).toBeTruthy()
|
||||
} else { // Display
|
||||
expect(await videoListPage.isVideoDisplayed(videoName)).toBeTruthy()
|
||||
expect(await videoListPage.isVideoBlurred(videoName)).toBeFalsy()
|
||||
expect(await videoListPage.hasVideoWarning(videoName)).toBeFalsy()
|
||||
}
|
||||
|
||||
if (nsfwTooltip) {
|
||||
await videoListPage.expectVideoNSFWTooltip(videoName, nsfwTooltip)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFilterText (policy: NSFWPolicyType) {
|
||||
const pagesWithFilters = [
|
||||
videoListPage.goOnRootAccount.bind(videoListPage),
|
||||
videoListPage.goOnBrowseVideos.bind(videoListPage),
|
||||
videoListPage.goOnRootChannel.bind(videoListPage)
|
||||
]
|
||||
|
||||
for (const goOnPage of pagesWithFilters) {
|
||||
await goOnPage()
|
||||
|
||||
const filterText = await videoListPage.getNSFWFilterText()
|
||||
|
||||
if (policy === 'do_not_list') {
|
||||
expect(filterText).toContain('hidden')
|
||||
} else if (policy === 'warn') {
|
||||
expect(filterText).toContain('warned')
|
||||
} else if (policy === 'blur') {
|
||||
expect(filterText).toContain('blurred')
|
||||
} else {
|
||||
expect(filterText).toContain('displayed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCommonVideoListPages (policy: NSFWPolicyType, videos: string[], nsfwTooltip?: string) {
|
||||
const pages = [
|
||||
videoListPage.goOnRootAccount.bind(videoListPage),
|
||||
videoListPage.goOnBrowseVideos.bind(videoListPage),
|
||||
videoListPage.goOnRootChannel.bind(videoListPage),
|
||||
videoListPage.goOnRootAccountChannels.bind(videoListPage),
|
||||
videoListPage.goOnHomepage.bind(videoListPage)
|
||||
]
|
||||
|
||||
for (const goOnPage of pages) {
|
||||
await goOnPage()
|
||||
|
||||
for (const video of videos) {
|
||||
await browser.saveScreenshot(getScreenshotPath('before-test.png'))
|
||||
await checkVideo({ policy, videoName: video, nsfwTooltip })
|
||||
}
|
||||
}
|
||||
|
||||
for (const video of videos) {
|
||||
await videoSearchPage.search(video)
|
||||
|
||||
await browser.saveScreenshot(getScreenshotPath('before-test.png'))
|
||||
await checkVideo({ policy, videoName: video, nsfwTooltip })
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAdminNSFW (nsfw: NSFWPolicyType) {
|
||||
await adminConfigPage.updateNSFWSetting(nsfw)
|
||||
await adminConfigPage.save()
|
||||
}
|
||||
|
||||
async function updateUserNSFW (nsfw: NSFWPolicyType, loggedIn: boolean) {
|
||||
if (loggedIn) {
|
||||
await myAccountPage.navigateToMySettings()
|
||||
await myAccountPage.updateNSFW(nsfw)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await anonymousSettingsPage.openSettings()
|
||||
await anonymousSettingsPage.updateNSFW(nsfw)
|
||||
await anonymousSettingsPage.closeSettings()
|
||||
}
|
||||
|
||||
async function updateUserViolentNSFW (nsfw: NSFWPolicyType, loggedIn: boolean) {
|
||||
if (loggedIn) {
|
||||
await myAccountPage.navigateToMySettings()
|
||||
await myAccountPage.updateViolentFlag(nsfw)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await anonymousSettingsPage.openSettings()
|
||||
await anonymousSettingsPage.updateViolentFlag(nsfw)
|
||||
await anonymousSettingsPage.closeSettings()
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
adminUserPage = new AdminUserPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
videoSearchPage = new VideoSearchPage()
|
||||
playerPage = new PlayerPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Preparation', function () {
|
||||
it('Should login and disable NSFW', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateUserNSFW('display', true)
|
||||
})
|
||||
|
||||
it('Should set the homepage', async () => {
|
||||
await adminConfigPage.updateHomepage('<peertube-videos-list data-sort="-publishedAt"></peertube-videos-list>')
|
||||
await adminConfigPage.save()
|
||||
})
|
||||
|
||||
it('Should create a user', async () => {
|
||||
await adminUserPage.createUser({
|
||||
username: 'user_' + seed,
|
||||
password: 'superpassword'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should upload NSFW and normal videos', async () => {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.setAsNSFW()
|
||||
await videoPublishPage.validSecondStep(nsfwVideo)
|
||||
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.setAsNSFW({ summary: 'bibi is violent', violent: true })
|
||||
await videoPublishPage.validSecondStep(violentVideo)
|
||||
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.validSecondStep(normalVideo)
|
||||
})
|
||||
|
||||
it('Should logout', async function () {
|
||||
await loginPage.logout()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NSFW with an anonymous users using instance default', function () {
|
||||
it('Should correctly handle do not list', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('do_not_list')
|
||||
|
||||
await loginPage.logout()
|
||||
|
||||
await checkCommonVideoListPages('do_not_list', [ nsfwVideo, violentVideo ])
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
|
||||
await checkFilterText('do_not_list')
|
||||
})
|
||||
|
||||
it('Should correctly handle blur', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('blur')
|
||||
|
||||
await loginPage.logout()
|
||||
|
||||
await checkCommonVideoListPages('blur', [ nsfwVideo, violentVideo ])
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
|
||||
await checkFilterText('blur')
|
||||
})
|
||||
|
||||
it('Should not autoplay the video and display a warning on watch/embed page', async function () {
|
||||
await videoListPage.clickOnVideo(nsfwVideo)
|
||||
await videoWatchPage.waitWatchVideoName(nsfwVideo)
|
||||
|
||||
videoUrl = await browser.getUrl()
|
||||
|
||||
const check = async () => {
|
||||
expect(await playerPage.getPlayButton().isDisplayed()).toBeTruthy()
|
||||
|
||||
expect(await playerPage.getNSFWContentText()).toContain('This video contains sensitive content')
|
||||
expect(await playerPage.getMoreNSFWInfoButton().isDisplayed()).toBeFalsy()
|
||||
expect(await playerPage.hasPoster()).toBeFalsy()
|
||||
}
|
||||
|
||||
await check()
|
||||
await videoWatchPage.goOnAssociatedEmbed()
|
||||
await check()
|
||||
})
|
||||
|
||||
it('Should correctly handle warn', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('warn')
|
||||
|
||||
await loginPage.logout()
|
||||
|
||||
await checkCommonVideoListPages('warn', [ nsfwVideo, violentVideo ])
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
|
||||
await checkFilterText('warn')
|
||||
})
|
||||
|
||||
it('Should not autoplay the video and display a warning on watch/embed page', async function () {
|
||||
await videoListPage.clickOnVideo(violentVideo)
|
||||
await videoWatchPage.waitWatchVideoName(violentVideo)
|
||||
|
||||
const check = async () => {
|
||||
expect(await playerPage.getPlayButton().isDisplayed()).toBeTruthy()
|
||||
|
||||
expect(await playerPage.getNSFWContentText()).toContain('This video contains sensitive content')
|
||||
expect(await playerPage.hasPoster()).toBeTruthy()
|
||||
|
||||
const moreButton = playerPage.getMoreNSFWInfoButton()
|
||||
expect(await moreButton.isDisplayed()).toBeTruthy()
|
||||
|
||||
await moreButton.click()
|
||||
await playerPage.getNSFWDetailsContent().waitForDisplayed()
|
||||
|
||||
const moreContent = await playerPage.getNSFWDetailsContent().getText()
|
||||
expect(moreContent).toContain('Potentially violent content')
|
||||
expect(moreContent).toContain('bibi is violent')
|
||||
}
|
||||
|
||||
await check()
|
||||
await videoWatchPage.goOnAssociatedEmbed()
|
||||
await check()
|
||||
})
|
||||
|
||||
it('Should correctly handle display', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('display')
|
||||
|
||||
await loginPage.logout()
|
||||
|
||||
await checkCommonVideoListPages('display', [ nsfwVideo, violentVideo, normalVideo ])
|
||||
|
||||
await checkFilterText('display')
|
||||
})
|
||||
|
||||
it('Should autoplay the video on watch page', async function () {
|
||||
await videoListPage.clickOnVideo(nsfwVideo)
|
||||
await videoWatchPage.waitWatchVideoName(nsfwVideo)
|
||||
|
||||
expect(await playerPage.getPlayButton().isDisplayed()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NSFW settings', function () {
|
||||
function runSuite (loggedIn: boolean) {
|
||||
it('Should correctly handle do not list', async () => {
|
||||
await updateUserNSFW('do_not_list', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('do_not_list', [ nsfwVideo, violentVideo ])
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
|
||||
await checkFilterText('do_not_list')
|
||||
})
|
||||
|
||||
it('Should use a confirm modal when viewing the video and watch the video', async function () {
|
||||
await go(videoUrl)
|
||||
|
||||
const confirmTitle = videoWatchPage.getModalTitleEl()
|
||||
await confirmTitle.waitForDisplayed()
|
||||
expect(await confirmTitle.getText()).toContain('Sensitive video')
|
||||
|
||||
await videoWatchPage.confirmModal()
|
||||
await videoWatchPage.waitWatchVideoName(nsfwVideo)
|
||||
})
|
||||
|
||||
it('Should correctly handle blur', async () => {
|
||||
await updateUserNSFW('blur', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('blur', [ nsfwVideo ], 'This video contains sensitive content')
|
||||
await checkCommonVideoListPages('blur', [ violentVideo ], 'This video contains sensitive content: violence')
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
|
||||
await checkFilterText('blur')
|
||||
})
|
||||
|
||||
it('Should correctly handle warn', async () => {
|
||||
await updateUserNSFW('warn', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('warn', [ nsfwVideo ], 'This video contains sensitive content')
|
||||
await checkCommonVideoListPages('warn', [ violentVideo ], 'This video contains sensitive content: violence')
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
|
||||
await checkFilterText('warn')
|
||||
})
|
||||
|
||||
it('Should correctly handle display', async () => {
|
||||
await updateUserNSFW('display', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('display', [ nsfwVideo, violentVideo, normalVideo ])
|
||||
|
||||
await checkFilterText('display')
|
||||
})
|
||||
|
||||
it('Should update the setting to blur violent video with display NSFW setting', async () => {
|
||||
await updateUserViolentNSFW('blur', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('display', [ nsfwVideo, normalVideo ])
|
||||
await checkCommonVideoListPages('blur', [ violentVideo ])
|
||||
})
|
||||
|
||||
it('Should update the setting to hide NSFW videos but warn violent videos', async () => {
|
||||
await updateUserNSFW('do_not_list', loggedIn)
|
||||
await updateUserViolentNSFW('warn', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
await checkCommonVideoListPages('warn', [ violentVideo ])
|
||||
await checkCommonVideoListPages('do_not_list', [ nsfwVideo ])
|
||||
})
|
||||
|
||||
it('Should update the setting to blur NSFW videos and hide violent videos', async () => {
|
||||
await updateUserNSFW('blur', loggedIn)
|
||||
await updateUserViolentNSFW('do_not_list', loggedIn)
|
||||
|
||||
await checkCommonVideoListPages('display', [ normalVideo ])
|
||||
await checkCommonVideoListPages('do_not_list', [ violentVideo ])
|
||||
await checkCommonVideoListPages('blur', [ nsfwVideo ])
|
||||
})
|
||||
}
|
||||
|
||||
describe('NSFW with an anonymous user', function () {
|
||||
runSuite(false)
|
||||
})
|
||||
|
||||
describe('NSFW with a logged in users', function () {
|
||||
before(async () => {
|
||||
await loginPage.login({ username: 'user_' + seed, password: 'superpassword' })
|
||||
})
|
||||
|
||||
runSuite(true)
|
||||
|
||||
after(async () => {
|
||||
await loginPage.logout()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
119
client/e2e/src/suites-local/page-crash.e2e-spec.ts
Normal file
119
client/e2e/src/suites-local/page-crash.e2e-spec.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, selectCustomSelect, waitServerUp } from '../utils'
|
||||
|
||||
// These tests help to notice crash with invalid translated strings
|
||||
describe('Page crash', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
|
||||
const languages = [
|
||||
'العربية',
|
||||
'Català',
|
||||
'Čeština',
|
||||
'Deutsch',
|
||||
'ελληνικά',
|
||||
'Esperanto',
|
||||
'Español',
|
||||
'Euskara',
|
||||
'فارسی',
|
||||
'Suomi',
|
||||
'Français',
|
||||
'Gàidhlig',
|
||||
'Galego',
|
||||
'Hrvatski',
|
||||
'Magyar',
|
||||
'Íslenska',
|
||||
'Italiano',
|
||||
'日本語',
|
||||
'Taqbaylit',
|
||||
'Norsk bokmål',
|
||||
'Nederlands',
|
||||
'Norsk nynorsk',
|
||||
'Occitan',
|
||||
'Polski',
|
||||
'Português (Brasil)',
|
||||
'Português (Portugal)',
|
||||
'Pусский',
|
||||
'Slovenčina',
|
||||
'Shqip',
|
||||
'Svenska',
|
||||
'ไทย',
|
||||
'Toki Pona',
|
||||
'Türkçe',
|
||||
'украї́нська мо́ва',
|
||||
'Tiếng Việt',
|
||||
'简体中文(中国)',
|
||||
'繁體中文(台灣)'
|
||||
]
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
for (const language of languages) {
|
||||
describe('For language: ' + language, () => {
|
||||
it('Should change the language', async function () {
|
||||
await go('/')
|
||||
|
||||
await $('.settings-button').waitForClickable()
|
||||
await $('.settings-button').click()
|
||||
|
||||
await selectCustomSelect('language', language)
|
||||
|
||||
await $('my-user-interface-settings .primary-button').waitForClickable()
|
||||
await $('my-user-interface-settings .primary-button').click()
|
||||
})
|
||||
|
||||
it('Should upload and watch a video', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
})
|
||||
|
||||
it('Should set a homepage', async function () {
|
||||
await adminConfigPage.updateHomepage('My custom homepage content')
|
||||
await adminConfigPage.save()
|
||||
|
||||
// All tests
|
||||
await go('/home')
|
||||
|
||||
await $('*=My custom homepage content').waitForDisplayed()
|
||||
})
|
||||
|
||||
it('Should go on client pages and not crash', async function () {
|
||||
await $('a[href="/videos/overview"]').waitForClickable()
|
||||
await $('a[href="/videos/overview"]').click()
|
||||
|
||||
await $('my-video-overview').waitForExist()
|
||||
})
|
||||
|
||||
it('Should go on videos from subscriptions pages', async function () {
|
||||
await $('a[href="/videos/subscriptions"]').waitForClickable()
|
||||
await $('a[href="/videos/subscriptions"]').click()
|
||||
|
||||
await $('my-videos-user-subscriptions').waitForExist()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath(`after-page-crash-test-${language}.png`))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
87
client/e2e/src/suites-local/player-settings.e2e-spec.ts
Normal file
87
client/e2e/src/suites-local/player-settings.e2e-spec.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { AnonymousSettingsPage } from '../po/anonymous-settings.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Player settings', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let myAccountPage: MyAccountPage
|
||||
let anonymousSettingsPage: AnonymousSettingsPage
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
myAccountPage = new MyAccountPage()
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('P2P', function () {
|
||||
let videoUrl: string
|
||||
|
||||
async function goOnVideoWatchPage () {
|
||||
await go(videoUrl)
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
}
|
||||
|
||||
async function checkP2P (enabled: boolean) {
|
||||
await goOnVideoWatchPage()
|
||||
expect(await videoWatchPage.isPrivacyWarningDisplayed()).toEqual(enabled)
|
||||
|
||||
await videoWatchPage.goOnAssociatedEmbed()
|
||||
expect(await videoWatchPage.isEmbedWarningDisplayed()).toEqual(enabled)
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
videoUrl = await browser.getUrl()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
await goOnVideoWatchPage()
|
||||
})
|
||||
|
||||
it('Should have P2P enabled for a logged in user', async function () {
|
||||
await checkP2P(true)
|
||||
})
|
||||
|
||||
it('Should disable P2P for a logged in user', async function () {
|
||||
await myAccountPage.navigateToMySettings()
|
||||
await myAccountPage.clickOnP2PCheckbox()
|
||||
|
||||
await checkP2P(false)
|
||||
})
|
||||
|
||||
it('Should have P2P enabled for anonymous users', async function () {
|
||||
await loginPage.logout()
|
||||
|
||||
await checkP2P(true)
|
||||
})
|
||||
|
||||
it('Should disable P2P for an anonymous user', async function () {
|
||||
await anonymousSettingsPage.openSettings()
|
||||
await anonymousSettingsPage.clickOnP2PCheckbox()
|
||||
|
||||
await checkP2P(false)
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
|
@ -1,10 +1,10 @@
|
|||
import { AdminPluginPage } from '../po/admin-plugin.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { getCheckbox, isMobileDevice, waitServerUp } from '../utils'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { getCheckbox, getScreenshotPath, isMobileDevice, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Plugins', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let adminPluginPage: AdminPluginPage
|
||||
|
||||
|
@ -12,11 +12,11 @@ describe('Plugins', () => {
|
|||
return getCheckbox('hello-world-field-4')
|
||||
}
|
||||
|
||||
async function expectSubmitState ({ disabled }: { disabled: boolean }) {
|
||||
const disabledSubmit = await $('my-button [disabled]')
|
||||
async function expectSubmitError (hasError: boolean) {
|
||||
await videoPublishPage.clickOnSave()
|
||||
|
||||
if (disabled) expect(await disabledSubmit.isDisplayed()).toBeTruthy()
|
||||
else expect(await disabledSubmit.isDisplayed()).toBeFalsy()
|
||||
await $('.form-error*=Should be enabled').waitForDisplayed({ reverse: !hasError })
|
||||
await $('li*=Should be enabled').waitForDisplayed({ reverse: !hasError })
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
|
@ -25,10 +25,10 @@ describe('Plugins', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
adminPluginPage = new AdminPluginPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should install hello world plugin', async () => {
|
||||
|
@ -41,15 +41,23 @@ describe('Plugins', () => {
|
|||
})
|
||||
|
||||
it('Should have checkbox in video edit page', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
|
||||
await $('span=Super field 4 in main tab').waitForDisplayed()
|
||||
const el = () => $('span=Super field 4 in main tab')
|
||||
await el().waitForDisplayed()
|
||||
|
||||
// Only displayed if the video is public
|
||||
await videoPublishPage.setAsPrivate()
|
||||
await el().waitForDisplayed({ reverse: true })
|
||||
|
||||
await videoPublishPage.setAsPublic()
|
||||
await el().waitForDisplayed()
|
||||
|
||||
const checkbox = await getPluginCheckbox()
|
||||
expect(await checkbox.isDisplayed()).toBeTruthy()
|
||||
|
||||
await expectSubmitState({ disabled: true })
|
||||
await expectSubmitError(true)
|
||||
})
|
||||
|
||||
it('Should check the checkbox and be able to submit the video', async function () {
|
||||
|
@ -58,7 +66,7 @@ describe('Plugins', () => {
|
|||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
|
||||
await expectSubmitState({ disabled: false })
|
||||
await expectSubmitError(false)
|
||||
})
|
||||
|
||||
it('Should uncheck the checkbox and not be able to submit the video', async function () {
|
||||
|
@ -67,16 +75,16 @@ describe('Plugins', () => {
|
|||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
|
||||
await expectSubmitState({ disabled: true })
|
||||
|
||||
const error = await $('.form-error*=Should be enabled')
|
||||
|
||||
expect(await error.isDisplayed()).toBeTruthy()
|
||||
await expectSubmitError(true)
|
||||
})
|
||||
|
||||
it('Should change the privacy and should hide the checkbox', async function () {
|
||||
await videoUploadPage.setAsPrivate()
|
||||
await videoPublishPage.setAsPrivate()
|
||||
|
||||
await expectSubmitState({ disabled: false })
|
||||
await expectSubmitError(false)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
||||
|
|
61
client/e2e/src/suites-local/publish-live.e2e-spec.ts
Normal file
61
client/e2e/src/suites-local/publish-live.e2e-spec.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish live', function () {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
it('Should enable live', async function () {
|
||||
await adminConfigPage.toggleLive(true)
|
||||
await adminConfigPage.save()
|
||||
})
|
||||
|
||||
it('Should create a classic permanent live', async function () {
|
||||
await videoPublishPage.navigateTo('Go live')
|
||||
|
||||
await videoPublishPage.publishLive()
|
||||
await videoPublishPage.validSecondStep('Permanent live test')
|
||||
|
||||
expect(await videoPublishPage.getLiveState()).toEqual('permanent')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.waitWatchVideoName('Permanent live test')
|
||||
})
|
||||
|
||||
it('Should create a permanent live and update it to a normal live', async function () {
|
||||
await videoPublishPage.navigateTo('Go live')
|
||||
|
||||
await videoPublishPage.publishLive()
|
||||
await videoPublishPage.setNormalLive()
|
||||
await videoPublishPage.validSecondStep('Normal live test')
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.waitWatchVideoName('Normal live test')
|
||||
await videoWatchPage.clickOnManage()
|
||||
|
||||
expect(await videoPublishPage.getLiveState()).toEqual('normal')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
83
client/e2e/src/suites-local/publish.e2e-spec.ts
Normal file
83
client/e2e/src/suites-local/publish.e2e-spec.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish video', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
describe('Default upload values', function () {
|
||||
it('Should have default video values', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
expect(await videoWatchPage.getPrivacy()).toBe('Public')
|
||||
expect(await videoWatchPage.getLicence()).toBe('Unknown')
|
||||
expect(await videoWatchPage.isDownloadEnabled()).toBeTruthy()
|
||||
expect(await videoWatchPage.areCommentsEnabled()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Common', function () {
|
||||
it('Should upload a video and on refresh being redirected to the manage page', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep('first video')
|
||||
|
||||
await videoPublishPage.refresh('first video')
|
||||
})
|
||||
|
||||
it('Should upload a video and schedule upload date', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
|
||||
await videoPublishPage.scheduleUpload()
|
||||
await videoPublishPage.validSecondStep('scheduled')
|
||||
|
||||
await videoPublishPage.refresh('scheduled')
|
||||
|
||||
expect(videoPublishPage.getScheduleInput()).toBeDisplayed()
|
||||
|
||||
const nextDay = new Date()
|
||||
nextDay.setDate(1)
|
||||
nextDay.setMonth(nextDay.getMonth() + 1)
|
||||
|
||||
const inputDate = new Date(await videoPublishPage.getScheduleInput().getValue())
|
||||
expect(inputDate.getDate()).toEqual(nextDay.getDate())
|
||||
expect(inputDate.getMonth()).toEqual(nextDay.getMonth())
|
||||
expect(inputDate.getFullYear()).toEqual(nextDay.getFullYear())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Import', function () {
|
||||
it('Should import a video and on refresh being redirected to the manage page', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.importVideo()
|
||||
await videoPublishPage.validSecondStep('second video')
|
||||
|
||||
await videoPublishPage.refresh('second video')
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
|
@ -5,11 +5,13 @@ import { SignupPage } from '../po/signup.po'
|
|||
import {
|
||||
browserSleep,
|
||||
findEmailTo,
|
||||
getEmailPort,
|
||||
getScreenshotPath,
|
||||
getVerificationLink,
|
||||
go,
|
||||
isMobileDevice,
|
||||
MockSMTPServer,
|
||||
prepareWebBrowser,
|
||||
waitServerUp
|
||||
} from '../utils'
|
||||
|
||||
|
@ -75,7 +77,7 @@ describe('Signup', () => {
|
|||
}) {
|
||||
await loginPage.loginAsRootUser()
|
||||
|
||||
await adminConfigPage.navigateTo('basic-configuration')
|
||||
// Ensure we change the state of the form to "dirty" so we can save the form
|
||||
await adminConfigPage.toggleSignup(options.enabled)
|
||||
|
||||
if (options.enabled) {
|
||||
|
@ -104,7 +106,7 @@ describe('Signup', () => {
|
|||
signupPage = new SignupPage()
|
||||
adminRegistrationPage = new AdminRegistrationPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Signup disabled', function () {
|
||||
|
@ -116,9 +118,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Email verification disabled', function () {
|
||||
|
||||
describe('Direct registration', function () {
|
||||
|
||||
it('Should enable signup without approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: false })
|
||||
|
||||
|
@ -171,7 +171,6 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Registration with approval', function () {
|
||||
|
||||
it('Should enable signup with approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: false })
|
||||
|
||||
|
@ -193,7 +192,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should validate the third step (account)', async function () {
|
||||
await signupPage.fillAccountStep({ username: 'user_2', displayName: 'user_2 display name', password: 'password' })
|
||||
await signupPage.fillAccountStep({ username: 'user_2', displayName: 'user_2 display name', password: 'superpassword' })
|
||||
await signupPage.validateStep()
|
||||
})
|
||||
|
||||
|
@ -216,7 +215,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should display a message when trying to login with this account', async function () {
|
||||
const error = await loginPage.getLoginError('user_2', 'password')
|
||||
const error = await loginPage.getLoginError('user_2', 'superpassword')
|
||||
|
||||
expect(error).toContain('awaiting approval')
|
||||
})
|
||||
|
@ -224,14 +223,14 @@ describe('Signup', () => {
|
|||
it('Should accept the registration', async function () {
|
||||
await loginPage.loginAsRootUser()
|
||||
|
||||
await adminRegistrationPage.navigateToRegistratonsList()
|
||||
await adminRegistrationPage.navigateToRegistrationsList()
|
||||
await adminRegistrationPage.accept('user_2', 'moderation response')
|
||||
|
||||
await loginPage.logout()
|
||||
})
|
||||
|
||||
it('Should be able to login with this new account', async function () {
|
||||
await loginPage.login({ username: 'user_2', password: 'password', displayName: 'user_2 display name' })
|
||||
await loginPage.login({ username: 'user_2', password: 'superpassword', displayName: 'user_2 display name' })
|
||||
|
||||
await loginPage.logout()
|
||||
})
|
||||
|
@ -240,19 +239,12 @@ describe('Signup', () => {
|
|||
|
||||
describe('Email verification enabled', function () {
|
||||
const emails: any[] = []
|
||||
let emailPort: number
|
||||
|
||||
before(async () => {
|
||||
const key = browser.options.baseUrl + '-emailPort'
|
||||
// FIXME: typings are wrong, get returns a promise
|
||||
// FIXME: use * because the key is not properly escaped by the shared store when using get(key)
|
||||
emailPort = (await (browser.sharedStore.get('*') as unknown as Promise<number>))[key]
|
||||
|
||||
await MockSMTPServer.Instance.collectEmails(emailPort, emails)
|
||||
await MockSMTPServer.Instance.collectEmails(await getEmailPort(), emails)
|
||||
})
|
||||
|
||||
describe('Direct registration', function () {
|
||||
|
||||
it('Should enable signup without approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: true })
|
||||
|
||||
|
@ -320,7 +312,6 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Registration with approval', function () {
|
||||
|
||||
it('Should enable signup without approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: true })
|
||||
|
||||
|
@ -346,7 +337,7 @@ describe('Signup', () => {
|
|||
username: 'user_4',
|
||||
displayName: 'user_4 display name',
|
||||
email: 'user_4@example.com',
|
||||
password: 'password'
|
||||
password: 'superpassword'
|
||||
})
|
||||
await signupPage.validateStep()
|
||||
})
|
||||
|
@ -370,7 +361,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should display a message when trying to login with this account', async function () {
|
||||
const error = await loginPage.getLoginError('user_4', 'password')
|
||||
const error = await loginPage.getLoginError('user_4', 'superpassword')
|
||||
|
||||
expect(error).toContain('awaiting approval')
|
||||
})
|
||||
|
@ -378,7 +369,7 @@ describe('Signup', () => {
|
|||
it('Should accept the registration', async function () {
|
||||
await loginPage.loginAsRootUser()
|
||||
|
||||
await adminRegistrationPage.navigateToRegistratonsList()
|
||||
await adminRegistrationPage.navigateToRegistrationsList()
|
||||
await adminRegistrationPage.accept('user_4', 'moderation response 2')
|
||||
|
||||
await loginPage.logout()
|
||||
|
@ -410,4 +401,8 @@ describe('Signup', () => {
|
|||
MockSMTPServer.Instance.kill()
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,82 +1,79 @@
|
|||
import { AnonymousSettingsPage } from '../po/anonymous-settings.po'
|
||||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import {
|
||||
browserSleep,
|
||||
findEmailTo,
|
||||
getEmailPort,
|
||||
getScreenshotPath,
|
||||
getVerificationLink,
|
||||
go,
|
||||
isMobileDevice,
|
||||
MockSMTPServer,
|
||||
prepareWebBrowser,
|
||||
waitServerUp
|
||||
} from '../utils'
|
||||
|
||||
describe('User settings', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let myAccountPage: MyAccountPage
|
||||
let anonymousSettingsPage: AnonymousSettingsPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
|
||||
const emails: any[] = []
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
myAccountPage = new MyAccountPage()
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await MockSMTPServer.Instance.collectEmails(await getEmailPort(), emails)
|
||||
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('P2P', function () {
|
||||
let videoUrl: string
|
||||
|
||||
async function goOnVideoWatchPage () {
|
||||
await go(videoUrl)
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
}
|
||||
|
||||
async function checkP2P (enabled: boolean) {
|
||||
await goOnVideoWatchPage()
|
||||
expect(await videoWatchPage.isPrivacyWarningDisplayed()).toEqual(enabled)
|
||||
|
||||
await videoWatchPage.goOnAssociatedEmbed()
|
||||
expect(await videoWatchPage.isEmbedWarningDisplayed()).toEqual(enabled)
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
describe('Email', function () {
|
||||
before(async function () {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
await adminConfigPage.toggleSignup(true)
|
||||
await adminConfigPage.toggleSignupEmailVerification(true)
|
||||
await adminConfigPage.save()
|
||||
|
||||
videoUrl = await browser.getUrl()
|
||||
await browser.refresh()
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
await goOnVideoWatchPage()
|
||||
})
|
||||
|
||||
it('Should have P2P enabled for a logged in user', async function () {
|
||||
await checkP2P(true)
|
||||
})
|
||||
|
||||
it('Should disable P2P for a logged in user', async function () {
|
||||
it('Should ask to change the email', async function () {
|
||||
await myAccountPage.navigateToMySettings()
|
||||
await myAccountPage.clickOnP2PCheckbox()
|
||||
await myAccountPage.updateEmail('email2@example.com', loginPage.getRootPassword())
|
||||
|
||||
await checkP2P(false)
|
||||
const pendingEmailBlock = $('.pending-email')
|
||||
await pendingEmailBlock.waitForDisplayed()
|
||||
await expect(pendingEmailBlock).toHaveText(expect.stringContaining('email2@example.com is awaiting email verification'))
|
||||
|
||||
let email: { text: string }
|
||||
|
||||
while (!(email = findEmailTo(emails, 'email2@example.com'))) {
|
||||
await browserSleep(100)
|
||||
}
|
||||
|
||||
await go(getVerificationLink(email))
|
||||
|
||||
const alertBlock = $('.alert-success')
|
||||
await alertBlock.waitForDisplayed()
|
||||
await expect(alertBlock).toHaveText(expect.stringContaining('Email updated'))
|
||||
|
||||
await myAccountPage.navigateToMySettings()
|
||||
const changeEmailBlock = $('.change-email .form-group-description')
|
||||
await changeEmailBlock.waitForDisplayed()
|
||||
await expect(changeEmailBlock).toHaveText(expect.stringContaining('Your current email is email2@example.com'))
|
||||
})
|
||||
})
|
||||
|
||||
it('Should have P2P enabled for anonymous users', async function () {
|
||||
await loginPage.logout()
|
||||
after(async () => {
|
||||
MockSMTPServer.Instance.kill()
|
||||
|
||||
await checkP2P(true)
|
||||
})
|
||||
|
||||
it('Should disable P2P for an anonymous user', async function () {
|
||||
await anonymousSettingsPage.openSettings()
|
||||
await anonymousSettingsPage.clickOnP2PCheckbox()
|
||||
|
||||
await checkP2P(false)
|
||||
})
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,12 +2,12 @@ import { LoginPage } from '../po/login.po'
|
|||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { SignupPage } from '../po/signup.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Password protected videos', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let signupPage: SignupPage
|
||||
|
@ -44,13 +44,13 @@ describe('Password protected videos', () => {
|
|||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
signupPage = new SignupPage()
|
||||
playerPage = new PlayerPage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Owner', function () {
|
||||
|
@ -59,9 +59,12 @@ describe('Password protected videos', () => {
|
|||
})
|
||||
|
||||
it('Should login, upload a public video and save it to a playlist', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(publicVideoName1)
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep(publicVideoName1)
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName1)
|
||||
|
||||
await videoWatchPage.clickOnSave()
|
||||
|
||||
|
@ -69,15 +72,15 @@ describe('Password protected videos', () => {
|
|||
|
||||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
await browser.pause(5000)
|
||||
|
||||
})
|
||||
|
||||
it('Should upload a password protected video', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.setAsPasswordProtected(videoPassword)
|
||||
await videoUploadPage.validSecondUploadStep(passwordProtectedVideoName)
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.setAsPasswordProtected(videoPassword)
|
||||
await videoPublishPage.validSecondStep(passwordProtectedVideoName)
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
|
||||
|
||||
passwordProtectedVideoUrl = await browser.getUrl()
|
||||
|
@ -89,11 +92,13 @@ describe('Password protected videos', () => {
|
|||
})
|
||||
|
||||
it('Should upload a second public video and save it to playlist', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoPublishPage.navigateTo()
|
||||
|
||||
await videoUploadPage.uploadVideo('video3.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(publicVideoName2)
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep(publicVideoName2)
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2)
|
||||
await videoWatchPage.clickOnSave()
|
||||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
})
|
||||
|
@ -143,11 +148,11 @@ describe('Password protected videos', () => {
|
|||
await myAccountPage.clickOnPlaylist(playlistName)
|
||||
await myAccountPage.playPlaylist()
|
||||
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName1, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName1, 40 * 1000)
|
||||
playlistUrl = await browser.getUrl()
|
||||
|
||||
await videoWatchPage.waitUntilVideoName(passwordProtectedVideoName, 40 * 1000)
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
|
@ -156,7 +161,6 @@ describe('Password protected videos', () => {
|
|||
})
|
||||
|
||||
describe('Regular users', function () {
|
||||
|
||||
before(async () => {
|
||||
await signupPage.fullSignup({
|
||||
accountInfo: {
|
||||
|
@ -192,7 +196,7 @@ describe('Password protected videos', () => {
|
|||
it('Should watch the playlist without password protected video', async () => {
|
||||
await go(playlistUrl)
|
||||
await playerPage.playVideo()
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
|
@ -222,7 +226,7 @@ describe('Password protected videos', () => {
|
|||
it('Should watch the playlist without password protected video', async () => {
|
||||
await go(playlistUrl)
|
||||
await playerPage.playVideo()
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoListPage } from '../po/video-list.po'
|
||||
import { VideoSearchPage } from '../po/video-search.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { NSFWPolicy } from '../types/common'
|
||||
import { isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
describe('Videos list', () => {
|
||||
let videoListPage: VideoListPage
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
let loginPage: LoginPage
|
||||
let myAccountPage: MyAccountPage
|
||||
let videoSearchPage: VideoSearchPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
||||
const seed = Math.random()
|
||||
const nsfwVideo = seed + ' - nsfw'
|
||||
const normalVideo = seed + ' - normal'
|
||||
|
||||
async function checkNormalVideo () {
|
||||
expect(await videoListPage.videoExists(normalVideo)).toBeTruthy()
|
||||
expect(await videoListPage.videoIsBlurred(normalVideo)).toBeFalsy()
|
||||
}
|
||||
|
||||
async function checkNSFWVideo (policy: NSFWPolicy, filterText?: string) {
|
||||
if (policy === 'do_not_list') {
|
||||
if (filterText) expect(filterText).toContain('hidden')
|
||||
|
||||
expect(await videoListPage.videoExists(nsfwVideo)).toBeFalsy()
|
||||
return
|
||||
}
|
||||
|
||||
if (policy === 'blur') {
|
||||
if (filterText) expect(filterText).toContain('blurred')
|
||||
|
||||
expect(await videoListPage.videoExists(nsfwVideo)).toBeTruthy()
|
||||
expect(await videoListPage.videoIsBlurred(nsfwVideo)).toBeTruthy()
|
||||
return
|
||||
}
|
||||
|
||||
// display
|
||||
if (filterText) expect(filterText).toContain('displayed')
|
||||
|
||||
expect(await videoListPage.videoExists(nsfwVideo)).toBeTruthy()
|
||||
expect(await videoListPage.videoIsBlurred(nsfwVideo)).toBeFalsy()
|
||||
}
|
||||
|
||||
async function checkCommonVideoListPages (policy: NSFWPolicy) {
|
||||
const promisesWithFilters = [
|
||||
videoListPage.goOnRootAccount.bind(videoListPage),
|
||||
videoListPage.goOnBrowseVideos.bind(videoListPage),
|
||||
videoListPage.goOnRootChannel.bind(videoListPage)
|
||||
]
|
||||
|
||||
for (const p of promisesWithFilters) {
|
||||
await p()
|
||||
|
||||
const filter = await videoListPage.getNSFWFilter()
|
||||
const filterText = await filter.getText()
|
||||
|
||||
await checkNormalVideo()
|
||||
await checkNSFWVideo(policy, filterText)
|
||||
}
|
||||
|
||||
const promisesWithoutFilters = [
|
||||
videoListPage.goOnRootAccountChannels.bind(videoListPage),
|
||||
videoListPage.goOnHomepage.bind(videoListPage)
|
||||
]
|
||||
for (const p of promisesWithoutFilters) {
|
||||
await p()
|
||||
|
||||
await checkNormalVideo()
|
||||
await checkNSFWVideo(policy)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSearchPage (policy: NSFWPolicy) {
|
||||
await videoSearchPage.search(normalVideo)
|
||||
await checkNormalVideo()
|
||||
|
||||
await videoSearchPage.search(nsfwVideo)
|
||||
await checkNSFWVideo(policy)
|
||||
}
|
||||
|
||||
async function updateAdminNSFW (nsfw: NSFWPolicy) {
|
||||
await adminConfigPage.navigateTo('instance-information')
|
||||
await adminConfigPage.updateNSFWSetting(nsfw)
|
||||
await adminConfigPage.save()
|
||||
}
|
||||
|
||||
async function updateUserNSFW (nsfw: NSFWPolicy) {
|
||||
await myAccountPage.navigateToMySettings()
|
||||
await myAccountPage.updateNSFW(nsfw)
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
videoSearchPage = new VideoSearchPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
})
|
||||
|
||||
it('Should login and disable NSFW', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateUserNSFW('display')
|
||||
})
|
||||
|
||||
it('Should set the homepage', async () => {
|
||||
await adminConfigPage.navigateTo('instance-homepage')
|
||||
await adminConfigPage.updateHomepage('<peertube-videos-list data-sort="-publishedAt"></peertube-videos-list>')
|
||||
await adminConfigPage.save()
|
||||
})
|
||||
|
||||
it('Should upload 2 videos (NSFW and classic videos)', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.setAsNSFW()
|
||||
await videoUploadPage.validSecondUploadStep(nsfwVideo)
|
||||
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(normalVideo)
|
||||
})
|
||||
|
||||
it('Should logout', async function () {
|
||||
await loginPage.logout()
|
||||
})
|
||||
|
||||
describe('Anonymous users', function () {
|
||||
|
||||
it('Should correctly handle do not list', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('do_not_list')
|
||||
|
||||
await loginPage.logout()
|
||||
await checkCommonVideoListPages('do_not_list')
|
||||
await checkSearchPage('do_not_list')
|
||||
})
|
||||
|
||||
it('Should correctly handle blur', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('blur')
|
||||
|
||||
await loginPage.logout()
|
||||
await checkCommonVideoListPages('blur')
|
||||
await checkSearchPage('blur')
|
||||
})
|
||||
|
||||
it('Should correctly handle display', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('display')
|
||||
|
||||
await loginPage.logout()
|
||||
await checkCommonVideoListPages('display')
|
||||
await checkSearchPage('display')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logged in users', function () {
|
||||
|
||||
before(async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
it('Should correctly handle do not list', async () => {
|
||||
await updateUserNSFW('do_not_list')
|
||||
await checkCommonVideoListPages('do_not_list')
|
||||
await checkSearchPage('do_not_list')
|
||||
})
|
||||
|
||||
it('Should correctly handle blur', async () => {
|
||||
await updateUserNSFW('blur')
|
||||
await checkCommonVideoListPages('blur')
|
||||
await checkSearchPage('blur')
|
||||
})
|
||||
|
||||
it('Should correctly handle display', async () => {
|
||||
await updateUserNSFW('display')
|
||||
await checkCommonVideoListPages('display')
|
||||
await checkSearchPage('display')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await loginPage.logout()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Default upload values', function () {
|
||||
|
||||
it('Should have default video values', async function () {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video3.mp4')
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
expect(await videoWatchPage.getPrivacy()).toBe('Public')
|
||||
expect(await videoWatchPage.getLicence()).toBe('Unknown')
|
||||
expect(await videoWatchPage.isDownloadEnabled()).toBeTruthy()
|
||||
expect(await videoWatchPage.areCommentsEnabled()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
export type NSFWPolicy = 'do_not_list' | 'blur' | 'display'
|
|
@ -1,29 +1,31 @@
|
|||
async function browserSleep (amount: number) {
|
||||
export async function browserSleep (amount: number) {
|
||||
await browser.pause(amount)
|
||||
}
|
||||
|
||||
function isMobileDevice () {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isMobileDevice () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android' || platformName === 'ios'
|
||||
}
|
||||
|
||||
function isAndroid () {
|
||||
export function isAndroid () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android'
|
||||
}
|
||||
|
||||
function isSafari () {
|
||||
export function isSafari () {
|
||||
return browser.capabilities['browserName'] &&
|
||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||
}
|
||||
|
||||
function isIOS () {
|
||||
export function isIOS () {
|
||||
return isMobileDevice() && isSafari()
|
||||
}
|
||||
|
||||
async function go (url: string) {
|
||||
export async function go (url: string) {
|
||||
await browser.url(url)
|
||||
|
||||
await browser.execute(() => {
|
||||
|
@ -33,7 +35,20 @@ async function go (url: string) {
|
|||
})
|
||||
}
|
||||
|
||||
async function waitServerUp () {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function prepareWebBrowser () {
|
||||
if (isMobileDevice()) return
|
||||
|
||||
// Window size on chromium doesn't seem to work in "new" headless mode
|
||||
if (process.env.MOZ_HEADLESS_WIDTH) {
|
||||
await browser.setWindowSize(+process.env.MOZ_HEADLESS_WIDTH, +process.env.MOZ_HEADLESS_HEIGHT)
|
||||
}
|
||||
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
|
||||
export async function waitServerUp () {
|
||||
await browser.waitUntil(async () => {
|
||||
await go('/')
|
||||
await browserSleep(500)
|
||||
|
@ -41,13 +56,3 @@ async function waitServerUp () {
|
|||
return $('<my-app>').isDisplayed()
|
||||
}, { timeout: 20 * 1000 })
|
||||
}
|
||||
|
||||
export {
|
||||
isMobileDevice,
|
||||
isSafari,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
waitServerUp,
|
||||
go,
|
||||
browserSleep
|
||||
}
|
||||
|
|
|
@ -1,43 +1,75 @@
|
|||
async function getCheckbox (name: string) {
|
||||
export async function getCheckbox (name: string) {
|
||||
const input = $(`my-peertube-checkbox input[id=${name}]`)
|
||||
await input.waitForExist()
|
||||
|
||||
return input.parentElement()
|
||||
}
|
||||
|
||||
function isCheckboxSelected (name: string) {
|
||||
export function isCheckboxSelected (name: string) {
|
||||
return $(`input[id=${name}]`).isSelected()
|
||||
}
|
||||
|
||||
async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
export async function setCheckboxEnabled (name: string, enabled: boolean) {
|
||||
if (await isCheckboxSelected(name) === enabled) return
|
||||
|
||||
const checkbox = await getCheckbox(name)
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function isRadioSelected (name: string) {
|
||||
await $(`input[id=${name}] + label`).waitForClickable()
|
||||
|
||||
return $(`input[id=${name}]`).isSelected()
|
||||
}
|
||||
|
||||
export async function clickOnRadio (name: string) {
|
||||
const label = $(`input[id=${name}] + label`)
|
||||
|
||||
await label.waitForClickable()
|
||||
await label.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
const wrapper = $(`[formcontrolname=${id}] span[role=combobox]`)
|
||||
|
||||
await wrapper.waitForExist()
|
||||
await wrapper.scrollIntoView({ block: 'center' })
|
||||
await wrapper.waitForClickable()
|
||||
await wrapper.click()
|
||||
|
||||
const option = await $$(`[formcontrolname=${id}] li[role=option]`).filter(async o => {
|
||||
const text = await o.getText()
|
||||
const getOption = async () => {
|
||||
const options = await $$(`[formcontrolname=${id}] li[role=option]`).filter(async o => {
|
||||
const text = await o.getText()
|
||||
|
||||
return text.trimStart().startsWith(valueLabel)
|
||||
}).then(options => options[0])
|
||||
return text.trimStart().startsWith(valueLabel)
|
||||
})
|
||||
|
||||
await option.waitForDisplayed()
|
||||
if (options.length === 0) return undefined
|
||||
|
||||
return option.click()
|
||||
return options[0]
|
||||
}
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
const option = await getOption()
|
||||
if (!option) return false
|
||||
|
||||
return option.isDisplayed()
|
||||
})
|
||||
|
||||
return (await getOption()).click()
|
||||
}
|
||||
|
||||
async function findParentElement (
|
||||
el: WebdriverIO.Element,
|
||||
finder: (el: WebdriverIO.Element) => Promise<boolean>
|
||||
export async function findParentElement (
|
||||
el: ChainablePromiseElement,
|
||||
finder: (el: ChainablePromiseElement) => Promise<boolean>
|
||||
) {
|
||||
if (await finder(el) === true) return el
|
||||
|
||||
return findParentElement(await el.parentElement(), finder)
|
||||
}
|
||||
|
||||
export {
|
||||
getCheckbox,
|
||||
isCheckboxSelected,
|
||||
selectCustomSelect,
|
||||
findParentElement
|
||||
return findParentElement(el.parentElement(), finder)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
function getVerificationLink (email: { text: string }) {
|
||||
export function getVerificationLink (email: { text: string }) {
|
||||
const { text } = email
|
||||
|
||||
const regexp = /\[(?<link>http:\/\/[^\]]+)\]/g
|
||||
|
@ -9,13 +9,15 @@ function getVerificationLink (email: { text: string }) {
|
|||
for (const match of matched) {
|
||||
const link = match.groups.link
|
||||
|
||||
if (link.includes('/verify-account/')) return link
|
||||
if (link.includes('/verify-account/')) {
|
||||
return link.replace('127.0.0.1', 'localhost')
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not find /verify-account/ link')
|
||||
}
|
||||
|
||||
function findEmailTo (emails: { text: string, to: { address: string }[] }[], to: string) {
|
||||
export function findEmailTo (emails: { text: string, to: { address: string }[] }[], to: string) {
|
||||
for (const email of emails) {
|
||||
for (const { address } of email.to) {
|
||||
if (address === to) return email
|
||||
|
@ -25,7 +27,12 @@ function findEmailTo (emails: { text: string, to: { address: string }[] }[], to:
|
|||
return undefined
|
||||
}
|
||||
|
||||
export {
|
||||
getVerificationLink,
|
||||
findEmailTo
|
||||
export async function getEmailPort () {
|
||||
const key = browser.options.baseUrl + '-emailPort'
|
||||
// FIXME: typings are wrong, get returns a promise
|
||||
// FIXME: use * because the key is not properly escaped by the shared store when using get(key)
|
||||
const emailPort = (await (browser.sharedStore.get('*') as unknown as Promise<number>))[key]
|
||||
if (!emailPort) throw new Error('Invalid email port')
|
||||
|
||||
return emailPort
|
||||
}
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import { mkdirSync } from 'fs'
|
||||
import { mkdir, rm } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
const SCREENSHOTS_DIRECTORY = 'screenshots'
|
||||
|
||||
function createScreenshotsDirectory () {
|
||||
mkdirSync(SCREENSHOTS_DIRECTORY, { recursive: true })
|
||||
export async function createScreenshotsDirectory () {
|
||||
await rm(SCREENSHOTS_DIRECTORY, { recursive: true, force: true })
|
||||
await mkdir(SCREENSHOTS_DIRECTORY, { recursive: true })
|
||||
}
|
||||
|
||||
function getScreenshotPath (filename: string) {
|
||||
export function getScreenshotPath (filename: string) {
|
||||
return join(SCREENSHOTS_DIRECTORY, filename)
|
||||
}
|
||||
|
||||
export {
|
||||
createScreenshotsDirectory,
|
||||
getScreenshotPath
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ function buildConfig (suiteFile: string = undefined) {
|
|||
}
|
||||
}
|
||||
|
||||
if (filename === 'signup.e2e-spec.ts') {
|
||||
if (filename === 'signup.e2e-spec.ts' || filename === 'user-settings.e2e-spec.ts') {
|
||||
return {
|
||||
signup: {
|
||||
limit: -1
|
||||
|
|
|
@ -13,7 +13,9 @@ const FIXTURE_URLS = {
|
|||
HLS_EMBED: 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50',
|
||||
HLS_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a',
|
||||
|
||||
LIVE_VIDEO: 'https://peertube2.cpy.re/w/oBw6LwsMWWRkmXYfuYRpJd'
|
||||
LIVE_VIDEO: 'https://peertube2.cpy.re/w/oBw6LwsMWWRkmXYfuYRpJd',
|
||||
|
||||
IMPORT_URL: 'https://download.cpy.re/peertube/good_video.mp4'
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"module": "commonjs",
|
||||
"target": "ES2018",
|
||||
"typeRoots": [
|
||||
"../node_modules/@wdio",
|
||||
"../node_modules/@types",
|
||||
"../node_modules"
|
||||
],
|
||||
|
|
|
@ -95,18 +95,18 @@ module.exports = {
|
|||
{
|
||||
browserName: 'Chrome',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S8', osVersion: '7.0' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S10', osVersion: '9.0' })
|
||||
},
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 11', osVersion: '14' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 12', osVersion: '14' })
|
||||
},
|
||||
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 11 2020', osVersion: '14' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 12.9 2021', osVersion: '14' })
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -121,7 +121,8 @@ module.exports = {
|
|||
|
||||
services: [
|
||||
[
|
||||
'browserstack', { browserstackLocal: true }
|
||||
'browserstack',
|
||||
{ browserstackLocal: true }
|
||||
]
|
||||
],
|
||||
|
||||
|
@ -174,6 +175,5 @@ module.exports = {
|
|||
|
||||
onPrepare: onBrowserStackPrepare,
|
||||
onComplete: onBrowserStackComplete
|
||||
|
||||
} as WebdriverIO.Config
|
||||
}
|
||||
|
|
|
@ -28,10 +28,19 @@ module.exports = {
|
|||
'browserName': 'chrome',
|
||||
'acceptInsecureCerts': true,
|
||||
'goog:chromeOptions': {
|
||||
args: [ '--disable-gpu', windowSizeArg ],
|
||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
||||
prefs
|
||||
}
|
||||
}
|
||||
// {
|
||||
// 'browserName': 'firefox',
|
||||
// 'moz:firefoxOptions': {
|
||||
// binary: '/usr/bin/firefox-developer-edition',
|
||||
// args: [ '--headless', windowSizeArg ],
|
||||
|
||||
// prefs
|
||||
// }
|
||||
// }
|
||||
],
|
||||
|
||||
services: [ 'shared-store' ],
|
||||
|
|
|
@ -102,13 +102,7 @@ export const config = {
|
|||
bail: true
|
||||
},
|
||||
|
||||
autoCompileOpts: {
|
||||
autoCompile: true,
|
||||
|
||||
tsNodeOpts: {
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
}
|
||||
},
|
||||
tsConfigPath: require('path').join(__dirname, './tsconfig.json'),
|
||||
|
||||
before: function () {
|
||||
require('./src/commands/upload')
|
||||
|
|
204
client/eslint.config.mjs
Normal file
204
client/eslint.config.mjs
Normal file
|
@ -0,0 +1,204 @@
|
|||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import love from 'eslint-config-love'
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
import angular from 'angular-eslint'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
'**/node_modules/',
|
||||
'**/dist',
|
||||
'**/build',
|
||||
'.angular'
|
||||
]),
|
||||
|
||||
{
|
||||
extends: [
|
||||
love,
|
||||
angular.configs.tsRecommended
|
||||
],
|
||||
|
||||
processor: angular.processInlineTemplates,
|
||||
|
||||
plugins: {
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
|
||||
files: [
|
||||
'src/**/*.ts',
|
||||
'e2e/**/*.ts'
|
||||
],
|
||||
|
||||
rules: {
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
'type': [ 'element', 'attribute' ],
|
||||
'prefix': 'my',
|
||||
'style': 'kebab-case'
|
||||
}
|
||||
],
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
'type': [ 'element', 'attribute' ],
|
||||
'prefix': 'my',
|
||||
'style': 'camelCase'
|
||||
}
|
||||
],
|
||||
'@angular-eslint/use-component-view-encapsulation': 'error',
|
||||
|
||||
'@typescript-eslint/prefer-readonly': 'off',
|
||||
"n/no-callback-literal": "off",
|
||||
|
||||
'@stylistic/semi': [ 'error', 'never' ],
|
||||
|
||||
'eol-last': [ 'error', 'always' ],
|
||||
'indent': 'off',
|
||||
'no-lone-blocks': 'off',
|
||||
'no-mixed-operators': 'off',
|
||||
|
||||
'max-len': [ 'error', {
|
||||
code: 140
|
||||
} ],
|
||||
|
||||
'array-bracket-spacing': [ 'error', 'always' ],
|
||||
'quote-props': [ 'error', 'consistent-as-needed' ],
|
||||
'padded-blocks': 'off',
|
||||
'no-async-promise-executor': 'off',
|
||||
'dot-notation': 'off',
|
||||
'promise/param-names': 'off',
|
||||
'import/first': 'off',
|
||||
|
||||
'operator-linebreak': [ 'error', 'after', {
|
||||
overrides: {
|
||||
'?': 'before',
|
||||
':': 'before'
|
||||
}
|
||||
} ],
|
||||
|
||||
'@typescript-eslint/consistent-type-assertions': [ 'error', {
|
||||
assertionStyle: 'as'
|
||||
} ],
|
||||
|
||||
'@typescript-eslint/array-type': [ 'error', {
|
||||
default: 'array'
|
||||
} ],
|
||||
|
||||
'@typescript-eslint/restrict-template-expressions': [ 'off', {
|
||||
allowNumber: 'true'
|
||||
} ],
|
||||
|
||||
'@typescript-eslint/no-this-alias': [ 'error', {
|
||||
allowDestructuring: true,
|
||||
allowedNames: [ 'self' ]
|
||||
} ],
|
||||
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/quotes': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'@typescript-eslint/no-dynamic-delete': 'off',
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-extraneous-class': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
'no-implicit-globals': 'off',
|
||||
'@typescript-eslint/no-confusing-void-expression': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'logical-assignment-operators': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-type-assertion': 'off',
|
||||
'@typescript-eslint/prefer-destructuring': 'off',
|
||||
'promise/avoid-new': 'off',
|
||||
'@typescript-eslint/class-methods-use-this': 'off',
|
||||
'arrow-body-style': 'off',
|
||||
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
|
||||
'@typescript-eslint/consistent-type-exports': 'off',
|
||||
'@typescript-eslint/init-declarations': 'off',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'@typescript-eslint/method-signature-style': 'off',
|
||||
'eslint-comments/require-description': 'off',
|
||||
'max-lines': 'off',
|
||||
'@typescript-eslint/no-misused-spread': 'off',
|
||||
'consistent-this': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'prefer-regex-literals': 'off',
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
||||
'@typescript-eslint/no-unnecessary-template-expression': 'off',
|
||||
'@typescript-eslint/no-loop-func': 'off',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-import-type-side-effects': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'no-useless-return': 'off',
|
||||
'no-return-assign': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'import/no-named-default': 'off',
|
||||
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||
|
||||
"@typescript-eslint/no-deprecated": [ 'error', {
|
||||
allow: [
|
||||
{ from: 'package', package: 'video.js', name: 'options'}
|
||||
]
|
||||
}],
|
||||
|
||||
// Can be interesting to enable
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
// Can be interesting to enable
|
||||
'complexity': 'off',
|
||||
// Interesting but has a bug with specific cases
|
||||
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
|
||||
// TODO: enable
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
// TODO: enable
|
||||
'@typescript-eslint/max-params': 'off',
|
||||
// TODO: enable
|
||||
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||
// TODO: enable
|
||||
'@typescript-eslint/no-deprecated': 'off',
|
||||
// TODO: enable
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
// TODO: enable but it fails in our CI
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
||||
|
||||
// We use many nested callbacks in our tests
|
||||
'max-nested-callbacks': 'off'
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: [ 'src/standalone/build-tools/vite-utils.ts' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
files: [ '**/*.html' ],
|
||||
extends: [
|
||||
angular.configs.templateRecommended,
|
||||
angular.configs.templateAccessibility,
|
||||
],
|
||||
rules: {
|
||||
// TODO: enable
|
||||
'@angular-eslint/template/button-has-type': 'off'
|
||||
}
|
||||
}
|
||||
])
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "7.1.0",
|
||||
"version": "7.3.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"lint": "npm run lint-ts && npm run lint-scss",
|
||||
"lint-ts": "eslint --cache --ext .ts src/standalone/**/*.ts && npm run ng lint",
|
||||
"lint-ts": "eslint",
|
||||
"lint-scss": "stylelint 'src/**/*.scss'",
|
||||
"eslint": "eslint",
|
||||
"ng": "ng",
|
||||
|
@ -34,11 +34,6 @@
|
|||
],
|
||||
"typings": "*.d.ts",
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "^19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "^19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "^19.0.2",
|
||||
"@angular-eslint/schematics": "^19.0.2",
|
||||
"@angular-eslint/template-parser": "^19.0.2",
|
||||
"@angular/animations": "^19.1.4",
|
||||
"@angular/build": "^19.1.5",
|
||||
"@angular/cdk": "^19.1.2",
|
||||
|
@ -58,44 +53,39 @@
|
|||
"@ngx-loading-bar/http-client": "^7.0.0",
|
||||
"@ngx-loading-bar/router": "^7.0.0",
|
||||
"@peertube/maildev": "^1.2.0",
|
||||
"@peertube/player": "workspace:*",
|
||||
"@peertube/xliffmerge": "^2.0.3",
|
||||
"@plussub/srt-vtt-parser": "^2.0.5",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"@primeng/themes": "^19.1.2",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/jschannel": "^1.0.0",
|
||||
"@types/linkifyjs": "^2.1.2",
|
||||
"@types/lodash-es": "^4.17.0",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/node": "^20",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/sha.js": "^2.4.0",
|
||||
"@types/video.js": "^7.3.40",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@wdio/browserstack-service": "^8.10.5",
|
||||
"@wdio/cli": "^8.10.5",
|
||||
"@wdio/local-runner": "^8.10.5",
|
||||
"@wdio/mocha-framework": "^8.10.4",
|
||||
"@wdio/shared-store-service": "^8.10.5",
|
||||
"@wdio/spec-reporter": "^8.10.5",
|
||||
"@wdio/browserstack-service": "^9.12.7",
|
||||
"@wdio/cli": "^9.12.7",
|
||||
"@wdio/globals": "^9.17.0",
|
||||
"@wdio/local-runner": "^9.12.7",
|
||||
"@wdio/mocha-framework": "^9.12.6",
|
||||
"@wdio/shared-store-service": "^9.12.7",
|
||||
"@wdio/spec-reporter": "^9.12.6",
|
||||
"angularx-qrcode": "19.0.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"chartjs-plugin-zoom": "~2.0.1",
|
||||
"chartjs-plugin-zoom": "~2.2.0",
|
||||
"color-bits": "^1.0.4",
|
||||
"core-js": "^3.22.8",
|
||||
"debug": "^4.3.1",
|
||||
"dompurify": "^3.1.6",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.1.0",
|
||||
"eslint-plugin-prefer-arrow": "latest",
|
||||
"expect-webdriverio": "^4.2.3",
|
||||
"hls.js": "~1.5.11",
|
||||
"expect-webdriverio": "^5.1.0",
|
||||
"hls.js": "~1.6.7",
|
||||
"intl-messageformat": "^10.1.0",
|
||||
"jschannel": "^1.0.2",
|
||||
"linkify-html": "^4.0.2",
|
||||
|
@ -104,9 +94,9 @@
|
|||
"markdown-it": "14.1.0",
|
||||
"markdown-it-emoji": "^3.0.0",
|
||||
"ngx-uploadx": "^7.0.0",
|
||||
"p2p-media-loader-core": "^2.1.2",
|
||||
"p2p-media-loader-hlsjs": "^2.1.2",
|
||||
"primeng": "^17",
|
||||
"p2p-media-loader-core": "^2.2.1",
|
||||
"p2p-media-loader-hlsjs": "^2.2.1",
|
||||
"primeng": "^19.1.2",
|
||||
"rxjs": "^7.3.0",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"sha.js": "^2.4.11",
|
||||
|
@ -117,12 +107,19 @@
|
|||
"tinykeys": "^2.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.4.0",
|
||||
"type-fest": "^4.37.0",
|
||||
"typescript": "~5.7.3",
|
||||
"video.js": "^7.19.2",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"video.js": "^8.23.4",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-checker": "^0.9.3",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"angular-eslint": "^19.3.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-love": "^119.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div *ngFor="let subscription of subscriptions" class="follow-block">
|
||||
<my-actor-avatar [actor]="subscription" actorType="instance" size="32"></my-actor-avatar>
|
||||
|
||||
<div>
|
||||
<div class="ellipsis">
|
||||
<a class="follow-name" [href]="subscription.url" target="_blank" rel="noopener noreferrer">{{ subscription.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,7 +68,7 @@
|
|||
<div *ngFor="let follower of followers" class="follow-block">
|
||||
<my-actor-avatar [actor]="follower" actorType="instance" size="32"></my-actor-avatar>
|
||||
|
||||
<div>
|
||||
<div class="ellipsis">
|
||||
<a class="follow-name" [href]="follower.url" target="_blank" rel="noopener noreferrer">{{ follower.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@include rfs(4rem, gap);
|
||||
}
|
||||
|
||||
my-instance-stat-rules {
|
||||
min-width: 600px;
|
||||
max-width: 50%;;
|
||||
}
|
||||
|
||||
@media screen and (max-width: #{breakpoint(xl)}) {
|
||||
|
@ -19,5 +21,6 @@ my-instance-stat-rules {
|
|||
|
||||
my-instance-stat-rules {
|
||||
min-width: 100%;
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,11 +59,10 @@ export class AboutInstanceComponent implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
if (aboutHTML.hardwareInformation) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Technical information`,
|
||||
routerLink: '/about/instance/tech'
|
||||
})
|
||||
}
|
||||
// Always displayed, we have the "features found on this instance" table on this page
|
||||
this.menuEntries.push({
|
||||
label: $localize`Technical information`,
|
||||
routerLink: '/about/instance/tech'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,19 +11,23 @@
|
|||
<span *ngFor="let category of categories" class="pt-badge badge-secondary me-1">{{ category }}</span>
|
||||
</div>
|
||||
|
||||
<div i18n *ngIf="config.instance.isNSFW" class="fw-bold text-content mt-3">{{ config.instance.name }} is dedicated to sensitive/NSFW content.</div>
|
||||
<div i18n *ngIf="config.instance.isNSFW" class="fw-bold text-content mt-3">{{ config.instance.name }} is dedicated to sensitive content.</div>
|
||||
</div>
|
||||
|
||||
@if (descriptionElement) {
|
||||
<div class="block description">
|
||||
<h4 i18n>Description</h4>
|
||||
|
||||
<my-custom-markup-container class="text-content" [content]="descriptionElement"></my-custom-markup-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="block terms">
|
||||
<h4 i18n class="section-title">Terms</h4>
|
||||
@if (aboutHTML.terms) {
|
||||
<div class="block terms">
|
||||
<h4 i18n class="section-title">Terms</h4>
|
||||
|
||||
<div class="text-content" [innerHTML]="aboutHTML.terms"></div>
|
||||
</div>
|
||||
<div class="text-content" [innerHTML]="aboutHTML.terms"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<my-support-modal #supportModal [name]="config.instance.name" [content]="config.instance.support.text"></my-support-modal>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<div *ngIf="aboutHTML.hardwareInformation" myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<h4 i18n class="section-title">Hardware information</h4>
|
||||
<div [innerHTML]="aboutHTML.hardwareInformation"></div>
|
||||
</div>
|
||||
|
|
|
@ -27,17 +27,25 @@
|
|||
*ngIf="config.instance.social.mastodonLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the Mastodon profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.mastodonLink"
|
||||
>
|
||||
>
|
||||
<my-global-icon iconName="mastodon"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.xLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the X profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.xLink"
|
||||
>
|
||||
<my-global-icon iconName="x-twitter"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.blueskyLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the Bluesky profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.blueskyLink"
|
||||
>
|
||||
<my-global-icon iconName="bluesky"></my-global-icon>
|
||||
</a>
|
||||
<my-global-icon iconName="bluesky"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.externalLink"
|
||||
|
|
|
@ -40,7 +40,7 @@ export class AboutComponent implements OnInit {
|
|||
this.menuEntries = [
|
||||
{
|
||||
label: $localize`Platform`,
|
||||
routerLink: '/about/instance/home',
|
||||
routerLink: '/about/instance',
|
||||
pluginSelectorId: 'about-menu-instance'
|
||||
},
|
||||
{
|
||||
|
|
|
@ -59,10 +59,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
|||
views: true,
|
||||
by: false,
|
||||
avatar: false,
|
||||
privacyLabel: false,
|
||||
privacyText: false,
|
||||
state: false,
|
||||
blacklistInfo: false
|
||||
privacyLabel: false
|
||||
}
|
||||
|
||||
private accountSub: Subscription
|
||||
|
@ -110,7 +107,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
|||
nsfw: this.videoService.nsfwPolicyToParam(this.nsfwPolicy)
|
||||
}
|
||||
|
||||
return this.videoService.getVideoChannelVideos(options)
|
||||
return this.videoService.listChannelVideos(options)
|
||||
.pipe(map(data => ({ videoChannel, videos: data.data, total: data.total })))
|
||||
})
|
||||
)
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
[defaultSort]="defaultSort"
|
||||
|
||||
displayFilters="true"
|
||||
displayModerationBlock="true"
|
||||
[displayAsRow]="displayAsRow()"
|
||||
|
||||
hideScopeFilter="true"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||
import { ComponentPaginationLight, DisableForReuseHook, ScreenService } from '@app/core'
|
||||
import { Account } from '@app/shared/shared-main/account/account.model'
|
||||
import { AccountService } from '@app/shared/shared-main/account/account.service'
|
||||
|
@ -37,6 +37,8 @@ export class AccountVideosComponent implements OnInit, OnDestroy, DisableForReus
|
|||
// Parent get the account for us
|
||||
this.accountSub = this.accountService.accountLoaded
|
||||
.subscribe(account => {
|
||||
if (account.id === this.account?.id) return
|
||||
|
||||
this.account = account
|
||||
if (this.alreadyLoaded) this.videosList().reloadVideos()
|
||||
|
||||
|
@ -49,15 +51,14 @@ export class AccountVideosComponent implements OnInit, OnDestroy, DisableForReus
|
|||
}
|
||||
|
||||
getVideosObservable (pagination: ComponentPaginationLight, filters: VideoFilters) {
|
||||
const options = {
|
||||
return this.videoService.listAccountVideos({
|
||||
...filters.toVideosAPIObject(),
|
||||
|
||||
videoPagination: pagination,
|
||||
account: this.account,
|
||||
skipCount: true
|
||||
}
|
||||
|
||||
return this.videoService.getAccountVideos(options)
|
||||
skipCount: true,
|
||||
includeScheduledLive: true
|
||||
})
|
||||
}
|
||||
|
||||
getSyndicationItems () {
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
|
||||
<my-simple-search-input
|
||||
class="ms-auto"
|
||||
[initialValue]="search"
|
||||
[alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
|
||||
(inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
|
||||
i18n-iconTitle icon-title="Search account videos"
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'
|
||||
import { AuthService, MarkdownService, MetaService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
|
||||
import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'
|
||||
import {
|
||||
AuthService,
|
||||
MarkdownService,
|
||||
MetaService,
|
||||
Notifier,
|
||||
PeerTubeRouterService,
|
||||
RedirectService,
|
||||
RestExtractor,
|
||||
ScreenService,
|
||||
UserService
|
||||
} from '@app/core'
|
||||
import { Account } from '@app/shared/shared-main/account/account.model'
|
||||
import { AccountService } from '@app/shared/shared-main/account/account.service'
|
||||
import { DropdownAction } from '@app/shared/shared-main/buttons/action-dropdown.component'
|
||||
|
@ -26,13 +36,12 @@ import { SubscribeButtonComponent } from '../shared/shared-user-subscription/sub
|
|||
templateUrl: './accounts.component.html',
|
||||
styleUrls: [ './accounts.component.scss' ],
|
||||
imports: [
|
||||
NgIf,
|
||||
CommonModule,
|
||||
ActorAvatarComponent,
|
||||
UserModerationDropdownComponent,
|
||||
NgbTooltip,
|
||||
AccountBlockBadgesComponent,
|
||||
CopyButtonComponent,
|
||||
NgClass,
|
||||
RouterLink,
|
||||
SubscribeButtonComponent,
|
||||
RouterLinkActive,
|
||||
|
@ -45,7 +54,6 @@ import { SubscribeButtonComponent } from '../shared/shared-user-subscription/sub
|
|||
})
|
||||
export class AccountsComponent implements OnInit, OnDestroy {
|
||||
private route = inject(ActivatedRoute)
|
||||
private router = inject(Router)
|
||||
private userService = inject(UserService)
|
||||
private accountService = inject(AccountService)
|
||||
private videoChannelService = inject(VideoChannelService)
|
||||
|
@ -58,12 +66,15 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
private blocklist = inject(BlocklistService)
|
||||
private screenService = inject(ScreenService)
|
||||
private metaService = inject(MetaService)
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
|
||||
readonly accountReportModal = viewChild<AccountReportComponent>('accountReportModal')
|
||||
|
||||
account: Account
|
||||
accountUser: User
|
||||
|
||||
search = ''
|
||||
|
||||
videoChannels: VideoChannel[] = []
|
||||
|
||||
links: HorizontalMenuEntry[] = []
|
||||
|
@ -104,6 +115,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
{ label: $localize`Channels`, routerLink: 'video-channels' },
|
||||
{ label: $localize`Videos`, routerLink: 'videos' }
|
||||
]
|
||||
|
||||
this.search = this.route.snapshot.queryParams['search'] || ''
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
|
@ -148,7 +161,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
searchChanged (search: string) {
|
||||
const queryParams = { search }
|
||||
|
||||
this.router.navigate([ './videos' ], { queryParams, relativeTo: this.route, queryParamsHandling: 'merge' })
|
||||
this.peertubeRouter.silentNavigate([ './videos' ], queryParams, this.route)
|
||||
}
|
||||
|
||||
onSearchInputDisplayChanged (displayed: boolean) {
|
||||
|
@ -225,7 +238,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private loadAccountVideosCount () {
|
||||
this.videoService.getAccountVideos({
|
||||
this.videoService.listAccountVideos({
|
||||
account: this.account,
|
||||
videoPagination: {
|
||||
currentPage: 1,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { AccountsComponent } from './accounts.component'
|
|||
export default [
|
||||
{
|
||||
path: 'peertube',
|
||||
redirectTo: '/videos/local'
|
||||
redirectTo: '/videos/browse?scope=local'
|
||||
},
|
||||
{
|
||||
path: ':accountId',
|
||||
|
|
9
client/src/app/+admin/config/admin-config.component.html
Normal file
9
client/src/app/+admin/config/admin-config.component.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="root">
|
||||
<div>
|
||||
<my-lateral-menu [config]="menuConfig"></my-lateral-menu>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
13
client/src/app/+admin/config/admin-config.component.scss
Normal file
13
client/src/app/+admin/config/admin-config.component.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $medium-view) {
|
||||
.root {
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
}
|
72
client/src/app/+admin/config/admin-config.component.ts
Normal file
72
client/src/app/+admin/config/admin-config.component.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { LateralMenuComponent, LateralMenuConfig } from '../../shared/shared-main/menu/lateral-menu.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config',
|
||||
styleUrls: [ './admin-config.component.scss' ],
|
||||
templateUrl: './admin-config.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
LateralMenuComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigComponent implements OnInit {
|
||||
menuConfig: LateralMenuConfig
|
||||
|
||||
ngOnInit (): void {
|
||||
this.menuConfig = {
|
||||
title: $localize`Configuration`,
|
||||
entries: [
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Information`,
|
||||
routerLink: 'information'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Logo`,
|
||||
routerLink: 'logo'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`General`,
|
||||
routerLink: 'general'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Homepage`,
|
||||
routerLink: 'homepage'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Customization`,
|
||||
routerLink: 'customization'
|
||||
},
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`VOD`,
|
||||
routerLink: 'vod'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Live`,
|
||||
routerLink: 'live'
|
||||
},
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Advanced`,
|
||||
routerLink: 'advanced'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,64 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
|
||||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { inject } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, ServerService, UserRightGuard } from '@app/core'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||
import { CustomConfig, UserRight, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { map } from 'rxjs'
|
||||
import { AdminConfigComponent } from './admin-config.component'
|
||||
import {
|
||||
AdminConfigAdvancedComponent,
|
||||
AdminConfigGeneralComponent,
|
||||
AdminConfigHomepageComponent,
|
||||
AdminConfigInformationComponent,
|
||||
AdminConfigLiveComponent,
|
||||
AdminConfigVODComponent
|
||||
} from './pages'
|
||||
import { AdminConfigCustomizationComponent } from './pages/admin-config-customization.component'
|
||||
import { AdminConfigService } from '../../shared/shared-admin/admin-config.service'
|
||||
import { AdminConfigLogoComponent } from './pages/admin-config-logo.component'
|
||||
import { InstanceLogoService } from '../../shared/shared-instance/instance-logo.service'
|
||||
|
||||
export const customConfigResolver: ResolveFn<CustomConfig> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(AdminConfigService).getCustomConfig()
|
||||
}
|
||||
|
||||
export const homepageResolver: ResolveFn<string> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(CustomPageService).getInstanceHomepage()
|
||||
.pipe(map(({ content }) => content))
|
||||
}
|
||||
|
||||
export const categoriesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoCategories()
|
||||
}
|
||||
|
||||
export const languagesResolver: ResolveFn<VideoConstant<string>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoLanguages()
|
||||
}
|
||||
|
||||
export const licencesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoLicences()
|
||||
}
|
||||
|
||||
export const privaciesResolver: ResolveFn<VideoConstant<VideoPrivacyType>[]> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(ServerService).getVideoPrivacies()
|
||||
}
|
||||
|
||||
export const commentPoliciesResolver: ResolveFn<VideoConstant<VideoCommentPolicyType>[]> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(ServerService).getCommentPolicies()
|
||||
}
|
||||
|
||||
export const logosResolver: ResolveFn<ReturnType<InstanceLogoService['getAllLogos']>> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(InstanceLogoService).getAllLogos()
|
||||
}
|
||||
|
||||
export const configRoutes: Routes = [
|
||||
{
|
||||
|
@ -10,18 +67,117 @@ export const configRoutes: Routes = [
|
|||
data: {
|
||||
userRight: UserRight.MANAGE_CONFIGURATION
|
||||
},
|
||||
resolve: {
|
||||
customConfig: customConfigResolver
|
||||
},
|
||||
providers: [
|
||||
InstanceLogoService
|
||||
],
|
||||
component: AdminConfigComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'edit-custom',
|
||||
// Old path with PeerTube < 7.3
|
||||
path: 'edit-custom',
|
||||
redirectTo: 'information',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'edit-custom',
|
||||
component: EditCustomConfigComponent,
|
||||
path: '',
|
||||
redirectTo: 'information',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'homepage',
|
||||
component: AdminConfigHomepageComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
homepageContent: homepageResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Edit custom configuration`
|
||||
title: $localize`Edit your platform homepage`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'customization',
|
||||
component: AdminConfigCustomizationComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Platform customization`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'information',
|
||||
component: AdminConfigInformationComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
categories: categoriesResolver,
|
||||
languages: languagesResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Platform information`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'logo',
|
||||
component: AdminConfigLogoComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
logos: logosResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Platform logos`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'general',
|
||||
component: AdminConfigGeneralComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
privacies: privaciesResolver,
|
||||
licences: licencesResolver,
|
||||
commentPolicies: commentPoliciesResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`General configuration`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vod',
|
||||
component: AdminConfigVODComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`VOD configuration`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
component: AdminConfigLiveComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Live configuration`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'advanced',
|
||||
component: AdminConfigAdvancedComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Advanced configuration`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,139 +0,0 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
|
||||
<div class="pt-two-cols mt-5"> <!-- cache grid -->
|
||||
|
||||
<div class="title-col">
|
||||
<h2 i18n>CACHE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Some files are not federated, and fetched when necessary. Define their caching policies.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="cache">
|
||||
<div class="form-group" formGroupName="previews">
|
||||
<label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cachePreviewsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.previews.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.previews.size" class="form-error" role="alert">{{ formErrors().cache.previews.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="captions">
|
||||
<label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheCaptionsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.captions.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.captions.size" class="form-error" role="alert">{{ formErrors().cache.captions.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrents">
|
||||
<label i18n for="cacheTorrentsSize">Number of video torrents to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheTorrentsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.torrents.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.torrents.size" class="form-error" role="alert">{{ formErrors().cache.torrents.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrents">
|
||||
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.storyboards.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.storyboards.size" class="form-error" role="alert">{{ formErrors().cache.storyboards.size }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- cache grid -->
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="customizations"></div> <!-- customizations anchor -->
|
||||
<h2 i18n>CUSTOMIZATIONS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="instance">
|
||||
<ng-container formGroupName="customizations">
|
||||
<div class="form-group">
|
||||
<label i18n for="customizationJavascript">JavaScript</label>
|
||||
<my-help>
|
||||
<ng-template ptTemplate="customHtml">
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
||||
<pre>console.log('my instance is amazing');</pre>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationJavascript" formControlName="javascript" class="form-control" dir="ltr"
|
||||
[ngClass]="{ 'input-error': formErrors()['instance.customizations.javascript'] }"
|
||||
></textarea>
|
||||
|
||||
<div *ngIf="formErrors().instance.customizations.javascript" class="form-error" role="alert">{{ formErrors().instance.customizations.javascript }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customizationCSS">CSS</label>
|
||||
|
||||
<my-help>
|
||||
<ng-template ptTemplate="customHtml">
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write CSS code directly. Example:</p>
|
||||
<pre>
|
||||
#custom-css {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
<p class="mb-2">Prepend with <em>#custom-css</em> to override styles. Example:</p>
|
||||
<pre>
|
||||
#custom-css .logged-in-email {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationCSS" formControlName="css" class="form-control" dir="ltr"
|
||||
[ngClass]="{ 'input-error': formErrors()['instance.customizations.css'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors().instance.customizations.css" class="form-error" role="alert">{{ formErrors().instance.customizations.css }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
|
@ -1,20 +0,0 @@
|
|||
import { Component, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { NgClass, NgIf } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-advanced-configuration',
|
||||
templateUrl: './edit-advanced-configuration.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [ FormsModule, ReactiveFormsModule, NgClass, NgIf, HelpComponent, PeerTubeTemplateDirective ]
|
||||
})
|
||||
export class EditAdvancedConfigurationComponent {
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
|
||||
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
||||
return this.form().value['cache'][type]['size']
|
||||
}
|
||||
}
|
|
@ -1,745 +0,0 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
<div class="pt-two-cols mt-5"> <!-- appearance grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>APPEARANCE</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-primary" routerLink="/admin/settings/config/edit-custom" fragment="advanced-configuration">customizations</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="theme">
|
||||
<div class="form-group">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="form-group" formGroupName="instance">
|
||||
<label i18n id="instanceDefaultClientRouteLabel" for="instanceDefaultClientRoute">Landing page</label>
|
||||
|
||||
<my-select-custom-value
|
||||
labelId="instanceDefaultClientRouteLabel"
|
||||
inputId="instanceDefaultClientRoute"
|
||||
[items]="defaultLandingPageOptions"
|
||||
formControlName="defaultClientRoute"
|
||||
inputType="text"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().instance.defaultClientRoute" class="form-error" role="alert">{{ formErrors().instance.defaultClientRoute }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="trending">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="algorithms">
|
||||
<label i18n for="trendingVideosAlgorithmsDefault">Default trending algorithm</label>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
|
||||
<option i18n value="hot">Hot videos</option>
|
||||
<option i18n value="most-viewed">Recent views</option>
|
||||
<option i18n value="most-liked">Most liked videos</option>
|
||||
<option i18n value="views">Global views</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().trending.videos.algorithms.default" class="form-error" role="alert">{{ formErrors().trending.videos.algorithms.default }}</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="miniature">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName" formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="menu">
|
||||
<ng-container formGroupName="login">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientMenuLoginRedirectOnSingleExternalAuth" formControlName="redirectOnSingleExternalAuth"
|
||||
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span *ngIf="countExternalAuth() === 0" i18n>⚠️ You don't have any external auth plugin enabled.</span>
|
||||
<span *ngIf="countExternalAuth() > 1" i18n>⚠️ You have multiple external auth plugins enabled.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- broadcast grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>BROADCAST MESSAGE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Display a message on your instance
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="broadcastMessage">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable broadcast message"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageDismissable" formControlName="dismissable"
|
||||
i18n-labelText labelText="Allow users to dismiss the broadcast message "
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="broadcastMessageLevel">Broadcast message level</label>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select id="broadcastMessageLevel" formControlName="level" class="form-control">
|
||||
<option i18n value="info">info</option>
|
||||
<option i18n value="warning">warning</option>
|
||||
<option i18n value="error">error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().broadcastMessage.level" class="form-error" role="alert">{{ formErrors().broadcastMessage.level }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="broadcastMessageMessage" formControlName="message"
|
||||
[formError]="formErrors()['broadcastMessage.message']" markdownType="to-unsafe-html"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors().broadcastMessage.message" class="form-error" role="alert">{{ formErrors().broadcastMessage.message }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- new users grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>NEW USERS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-primary" routerLink="/admin/overview/users">users</a> to set their quota individually.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="signup">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="signupEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable Signup"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
||||
|
||||
<my-alert type="primary" class="alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</my-alert>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresApproval" formControlName="requiresApproval"
|
||||
i18n-labelText labelText="Signup requires approval by moderators"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
||||
i18n-labelText labelText="Signup requires email verification"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="getDisabledSignupClass()">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
<span i18n class="small muted ms-1">When the total number of users in your instance reaches this limit, registrations are disabled. -1 == unlimited</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="-1" id="signupLimit" class="form-control"
|
||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors()['signup.limit'] }"
|
||||
>
|
||||
<span i18n>{form().value['signup']['limit'], plural, =1 {user} other {users}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().signup.limit" class="form-error" role="alert">{{ formErrors().signup.limit }}</div>
|
||||
|
||||
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="getDisabledSignupClass()" class="mt-3">
|
||||
<label i18n for="signupMinimumAge">Minimum required age to create an account</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="signupMinimumAge" class="form-control"
|
||||
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors()['signup.minimumAge'] }"
|
||||
>
|
||||
<span i18n>{form().value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().signup.minimumAge" class="form-error" role="alert">{{ formErrors().signup.minimumAge }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="user">
|
||||
<div class="form-group">
|
||||
<label i18n id="userVideoQuotaLabel" for="userVideoQuota">Default video quota per user</label>
|
||||
|
||||
<my-select-custom-value
|
||||
labelId="userVideoQuotaLabel"
|
||||
inputId="userVideoQuota"
|
||||
[items]="getVideoQuotaOptions()"
|
||||
formControlName="videoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<my-user-real-quota-info class="mt-2 d-block small muted" [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||
|
||||
<div *ngIf="formErrors().user.videoQuota" class="form-error" role="alert">{{ formErrors().user.videoQuota }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n id="userVideoQuotaDaily" for="userVideoQuotaDaily">Default daily upload limit per user</label>
|
||||
|
||||
<my-select-custom-value
|
||||
labelId="userVideoQuotaDailyLabel"
|
||||
inputId="userVideoQuotaDaily"
|
||||
[items]="getVideoQuotaDailyOptions()"
|
||||
formControlName="videoQuotaDaily"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().user.videoQuotaDaily" class="form-error" role="alert">{{ formErrors().user.videoQuotaDaily }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<ng-container formGroupName="history">
|
||||
<ng-container formGroupName="videos">
|
||||
<my-peertube-checkbox
|
||||
inputName="videosHistoryEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically enable video history for new users"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- videos grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>VIDEOS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
|
||||
<ng-container formGroupName="videos">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="importConcurrency">Import jobs concurrency</label>
|
||||
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input type="number" id="importConcurrency" formControlName="concurrency" />
|
||||
<span i18n>jobs in parallel</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().import.concurrency" class="form-error" role="alert">{{ formErrors().import.concurrency }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="http">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosHttpEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrent">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosTorrentEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ We don't recommend to enable this feature if you don't trust your users</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoChannelSynchronization">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importSynchronizationEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow channel synchronization with channel of other platforms like YouTube"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
⛔ You need to allow import with HTTP URL to be able to activate this feature.
|
||||
</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="videoChannelSynchronizationMaxPerUser">Max channel synchronization per user</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors()['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
>
|
||||
<span i18n>{form().value['import']['videoChannelSynchronization']['maxPerUser'], plural, =1 {sync} other {syncs}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors().import.videoChannelSynchronization.maxPerUser }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="autoBlacklist">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="ofUsers">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Block new videos automatically"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoFile">
|
||||
<ng-container formGroupName="update">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoFileUpdateEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow users to upload a new version of their video"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="storyboards">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="storyboardsEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable video storyboards"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoTranscription">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoTranscriptionEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable video transcription"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n><a href="https://docs.joinpeertube.org/admin/configuration#automatic-transcription" target="_blank">Automatically create subtitles</a> for uploaded/imported VOD videos</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getTranscriptionRunnerDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoTranscriptionRemoteRunnersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable remote runners for transcription"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks.
|
||||
Remote runners has to register on your instance first.
|
||||
</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- video channels grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>VIDEO CHANNELS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group" formGroupName="videoChannels">
|
||||
<label i18n for="videoChannelsMaxPerUser">Max video channels per user</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelsMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors()['videoChannels.maxPerUser'] }"
|
||||
>
|
||||
<span i18n>{form().value['videoChannels']['maxPerUser'], plural, =1 {channel} other {channels}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().videoChannels.maxPerUser" class="form-error" role="alert">{{ formErrors().videoChannels.maxPerUser }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- search grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>SEARCH</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="search">
|
||||
<ng-container formGroupName="remoteUri">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchRemoteUriUsers" formControlName="users"
|
||||
i18n-labelText labelText="Allow users to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchRemoteUriAnonymous" formControlName="anonymous"
|
||||
i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="searchIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable global search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div [ngClass]="getDisabledSearchIndexClass()">
|
||||
<label i18n for="searchIndexUrl">Search index URL</label>
|
||||
|
||||
<div i18n class="label-small-info">
|
||||
Use your <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="searchIndexUrl" class="form-control"
|
||||
formControlName="url" [ngClass]="{ 'input-error': formErrors()['search.searchIndex.url'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().search.searchIndex.url" class="form-error" role="alert">{{ formErrors().search.searchIndex.url }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
|
||||
i18n-labelText labelText="Disable local search in search bar"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
|
||||
i18n-labelText labelText="Search bar uses the global search index by default"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Otherwise the local search stays used by default</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- import/export grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>USER IMPORT/EXPORT</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
<ng-container formGroupName="users">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to import a data archive"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Video quota is checked on import so the user doesn't upload a too big archive file</div>
|
||||
<div i18n>Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="export">
|
||||
|
||||
<ng-container formGroupName="users">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="exportUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to export their data"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Users can export their PeerTube data in a .zip for backup or re-import. Only one export at a time is allowed per user</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n id="exportUsersMaxUserVideoQuota" for="exportUsersMaxUserVideoQuota">Max user video quota allowed to generate the export</label>
|
||||
|
||||
<span i18n class="ms-2 small muted">If the user decides to include the video files in the archive</span>
|
||||
|
||||
<my-select-custom-value
|
||||
labelId="exportUsersMaxUserVideoQuota"
|
||||
inputId="exportUsersMaxUserVideoQuota"
|
||||
[items]="exportMaxUserVideoQuotaOptions"
|
||||
formControlName="maxUserVideoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().export.users.maxUserVideoQuota" class="form-error" role="alert">{{ formErrors().export.users.maxUserVideoQuota }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n for="exportUsersExportExpiration">User export expiration</label>
|
||||
|
||||
<my-select-options inputId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"></my-select-options>
|
||||
|
||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>
|
||||
|
||||
<div *ngIf="formErrors().export.users.exportExpiration" class="form-error" role="alert">{{ formErrors().export.users.exportExpiration }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- federation grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>FEDERATION</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other instances.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="followers">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Other instances can follow yours"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
||||
i18n-labelText labelText="Manually approve new instance followers"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="followings">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<ng-container formGroupName="autoFollowBack">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow back instances"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="autoFollowIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow instances of a public index"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||
|
||||
<span i18n>
|
||||
See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
|
||||
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
||||
<input
|
||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors()['followings.instance.autoFollowIndex.indexUrl'] }"
|
||||
>
|
||||
<div *ngIf="formErrors().followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors().followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- administrators grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>ADMINISTRATORS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="form-group" formGroupName="admin">
|
||||
<label i18n for="adminEmail">Admin email</label>
|
||||
|
||||
<input
|
||||
type="text" id="adminEmail" class="form-control"
|
||||
formControlName="email" [ngClass]="{ 'input-error': formErrors()['admin.email'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().admin.email" class="form-error" role="alert">{{ formErrors().admin.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="contactForm">
|
||||
<my-peertube-checkbox
|
||||
inputName="enableContactForm" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable contact form"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- Twitter grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>TWITTER/X</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Extra configuration required by Twitter/X. All other social media (Facebook, Mastodon, etc.) are supported out of the box.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="services">
|
||||
<ng-container formGroupName="twitter">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="servicesTwitterUsername" i18n>Your Twitter/X username</label>
|
||||
|
||||
<div class="label-small-info">
|
||||
<p i18n class="mb-0">Indicates the Twitter/X account for the website or platform where the content was published.</p>
|
||||
|
||||
<p i18n>This is just an extra information injected in PeerTube HTML that is required by Twitter/X. If you don't have a Twitter/X account, just leave the default value.</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="servicesTwitterUsername" class="form-control"
|
||||
formControlName="username" [ngClass]="{ 'input-error': formErrors()['services.twitter.username'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().services.twitter.username" class="form-error" role="alert">{{ formErrors().services.twitter.username }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
|
@ -1,215 +0,0 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { ThemeService } from '@app/core'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-basic-configuration',
|
||||
templateUrl: './edit-basic-configuration.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent,
|
||||
NgIf,
|
||||
PeertubeCheckboxComponent,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
NgClass,
|
||||
UserRealQuotaInfoComponent,
|
||||
SelectOptionsComponent,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||
private configService = inject(ConfigService)
|
||||
private themeService = inject(ThemeService)
|
||||
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
|
||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
||||
|
||||
signupAlertMessage: string
|
||||
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||
availableThemes: SelectOptionsItem[]
|
||||
|
||||
exportExpirationOptions: SelectOptionsItem[] = []
|
||||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.buildLandingPageOptions()
|
||||
this.checkSignupField()
|
||||
this.checkImportSyncField()
|
||||
|
||||
this.availableThemes = [
|
||||
this.themeService.getDefaultThemeItem(),
|
||||
|
||||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.exportExpirationOptions = [
|
||||
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
||||
{ id: 1000 * 3600 * 24 * 2, label: $localize`2 days` },
|
||||
{ id: 1000 * 3600 * 24 * 7, label: $localize`7 days` },
|
||||
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
||||
]
|
||||
|
||||
this.exportMaxUserVideoQuotaOptions = this.configService.videoQuotaOptions.filter(o => (o.id as number) >= 1)
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
if (changes['serverConfig']) {
|
||||
this.buildLandingPageOptions()
|
||||
}
|
||||
}
|
||||
|
||||
countExternalAuth () {
|
||||
return this.serverConfig().plugin.registeredExternalAuths.length
|
||||
}
|
||||
|
||||
getVideoQuotaOptions () {
|
||||
return this.configService.videoQuotaOptions
|
||||
}
|
||||
|
||||
getVideoQuotaDailyOptions () {
|
||||
return this.configService.videoQuotaDailyOptions
|
||||
}
|
||||
|
||||
doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
|
||||
const enabled = this.form().value['trending']['videos']['algorithms']['enabled']
|
||||
if (!Array.isArray(enabled)) return false
|
||||
|
||||
return !!enabled.find((e: string) => e === algorithm)
|
||||
}
|
||||
|
||||
getUserVideoQuota () {
|
||||
return this.form().value['user']['videoQuota']
|
||||
}
|
||||
|
||||
isExportUsersEnabled () {
|
||||
return this.form().value['export']['users']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledExportUsersClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isExportUsersEnabled() }
|
||||
}
|
||||
|
||||
isSignupEnabled () {
|
||||
return this.form().value['signup']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledSignupClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
||||
}
|
||||
|
||||
isImportVideosHttpEnabled (): boolean {
|
||||
return this.form().value['import']['videos']['http']['enabled'] === true
|
||||
}
|
||||
|
||||
importSynchronizationChecked () {
|
||||
return this.isImportVideosHttpEnabled() && this.form().value['import']['videoChannelSynchronization']['enabled']
|
||||
}
|
||||
|
||||
hasUnlimitedSignup () {
|
||||
return this.form().value['signup']['limit'] === -1
|
||||
}
|
||||
|
||||
isSearchIndexEnabled () {
|
||||
return this.form().value['search']['searchIndex']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledSearchIndexClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isSearchIndexEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isTranscriptionEnabled () {
|
||||
return this.form().value['videoTranscription']['enabled'] === true
|
||||
}
|
||||
|
||||
getTranscriptionRunnerDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscriptionEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isAutoFollowIndexEnabled () {
|
||||
return this.form().value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
||||
}
|
||||
|
||||
buildLandingPageOptions () {
|
||||
let links: { label: string, path: string }[] = []
|
||||
|
||||
if (this.serverConfig().homepage.enabled) {
|
||||
links.push({ label: $localize`Home`, path: '/home' })
|
||||
}
|
||||
|
||||
links = links.concat([
|
||||
{ label: $localize`Discover`, path: '/videos/overview' },
|
||||
{ label: $localize`Browse all videos`, path: '/videos/browse' },
|
||||
{ label: $localize`Browse local videos`, path: '/videos/browse?scope=local' }
|
||||
])
|
||||
|
||||
this.defaultLandingPageOptions = links.map(o => ({
|
||||
id: o.path,
|
||||
label: o.label,
|
||||
description: o.path
|
||||
}))
|
||||
}
|
||||
|
||||
private checkImportSyncField () {
|
||||
const importSyncControl = this.form().get('import.videoChannelSynchronization.enabled')
|
||||
const importVideosHttpControl = this.form().get('import.videos.http.enabled')
|
||||
|
||||
importVideosHttpControl.valueChanges
|
||||
.subscribe(httpImportEnabled => {
|
||||
importSyncControl.setValue(httpImportEnabled && importSyncControl.value)
|
||||
if (httpImportEnabled) {
|
||||
importSyncControl.enable()
|
||||
} else {
|
||||
importSyncControl.disable()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private checkSignupField () {
|
||||
const signupControl = this.form().get('signup.enabled')
|
||||
|
||||
signupControl.valueChanges
|
||||
.pipe(pairwise())
|
||||
.subscribe(([ oldValue, newValue ]) => {
|
||||
if (oldValue === false && newValue === true) {
|
||||
/* eslint-disable max-len */
|
||||
this.signupAlertMessage =
|
||||
$localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
|
||||
|
||||
this.form().patchValue({
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
signupControl.updateValueAndValidity()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue