mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-06 11:36:58 +03:00
Compare commits
1656 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a860b89ef0 | ||
|
|
4650316067 | ||
|
|
08d458bdd6 | ||
|
|
0260b4889d | ||
|
|
6abfba1b12 | ||
|
|
d038279d79 | ||
|
|
35d3627760 | ||
|
|
dcffb83e62 | ||
|
|
a0465a44ae | ||
|
|
db55d314ee | ||
|
|
6621ae3730 | ||
|
|
b2af4fc7b0 | ||
|
|
d273095525 | ||
|
|
4f28d92eb8 | ||
|
|
c822a37a6e | ||
|
|
5e4773afdd | ||
|
|
cbdd852566 | ||
|
|
fd636a2770 | ||
|
|
043c8b771e | ||
|
|
55ff3230ed | ||
|
|
ed221c8599 | ||
|
|
29660d998b | ||
|
|
f520e00ed4 | ||
|
|
c2e0d5a98f | ||
|
|
21f7d5d938 | ||
|
|
c9a0fb7bc3 | ||
|
|
e79a8417f4 | ||
|
|
dfbc32617b | ||
|
|
901a42d1b4 | ||
|
|
634d5d083a | ||
|
|
d67b9cdcc5 | ||
|
|
78acb4a76a | ||
|
|
5889e9e557 | ||
|
|
3931328b60 | ||
|
|
f56fbbd2c7 | ||
|
|
dbfc685bf9 | ||
|
|
34771e96fe | ||
|
|
bcb9e33a01 | ||
|
|
72de520781 | ||
|
|
3c34077056 | ||
|
|
2eb53015bc | ||
|
|
4bbaf11502 | ||
|
|
60a3c017e5 | ||
|
|
a89abd5dd8 | ||
|
|
fc99c42e02 | ||
|
|
1146aac3c2 | ||
|
|
3341500fdf | ||
|
|
81ad0dd640 | ||
|
|
ff197d2985 | ||
|
|
8fbb1bb79b | ||
|
|
c0dc174f42 | ||
|
|
d8395163b9 | ||
|
|
b4b7ccec20 | ||
|
|
d75226bde5 | ||
|
|
db700cd7e8 | ||
|
|
591e3a0051 | ||
|
|
b0bcf2b684 | ||
|
|
2e4b7a0c9c | ||
|
|
c5f163e41e | ||
|
|
c5d2cacae2 | ||
|
|
5e3e02c674 | ||
|
|
0fe85b9760 | ||
|
|
5f53859c94 | ||
|
|
5b177a3e53 | ||
|
|
a66588619a | ||
|
|
98494aec4a | ||
|
|
e74b985304 | ||
|
|
fb414ed6db | ||
|
|
c7905d4062 | ||
|
|
8a9c7c869b | ||
|
|
002298648c | ||
|
|
929856fd3f | ||
|
|
1df8117105 | ||
|
|
146ba4ff93 | ||
|
|
8dc87da462 | ||
|
|
41c92da37e | ||
|
|
2d6be12062 | ||
|
|
14ef6682df | ||
|
|
10298efc08 | ||
|
|
b09ed1cbe2 | ||
|
|
d5611fb023 | ||
|
|
f67c0b5762 | ||
|
|
468ce25858 | ||
|
|
d392b1c8fc | ||
|
|
d02d78f325 | ||
|
|
6b5dc54cc7 | ||
|
|
76cc603a3f | ||
|
|
fe0afd58bc | ||
|
|
9bcf7ed199 | ||
|
|
fafcbe0b25 | ||
|
|
4c2b258038 | ||
|
|
bd059c3a68 | ||
|
|
70a507818f | ||
|
|
5f4f45056e | ||
|
|
7ead3ddc63 | ||
|
|
a536e4aeb8 | ||
|
|
2b237a4a8b | ||
|
|
03542ebcac | ||
|
|
b3c2b2c15d | ||
|
|
80e3a55cfe | ||
|
|
9224960dea | ||
|
|
84ea08d6d0 | ||
|
|
e9c43a22f5 | ||
|
|
fe6d2e88bd | ||
|
|
0bb6d29932 | ||
|
|
e9ebf08984 | ||
|
|
f156a5fbe9 | ||
|
|
b182945274 | ||
|
|
6507a3a68d | ||
|
|
e8b8b87190 | ||
|
|
c602285102 | ||
|
|
744b5ea4f6 | ||
|
|
68b2fba24c | ||
|
|
dbb8128a3a | ||
|
|
22b4135d62 | ||
|
|
4029b15233 | ||
|
|
90fc02e340 | ||
|
|
6b1fa7f5d0 | ||
|
|
0e8b6eb506 | ||
|
|
50d8c5f105 | ||
|
|
4da3c2d049 | ||
|
|
a3ddbef38e | ||
|
|
77a1799a7f | ||
|
|
0dbd081063 | ||
|
|
c9b18a4938 | ||
|
|
155c5a9b97 | ||
|
|
1bc9a69b79 | ||
|
|
fb6e98f93f | ||
|
|
61eb2e665b | ||
|
|
de7b985535 | ||
|
|
bbcbd3783a | ||
|
|
6db9404e1c | ||
|
|
857ee04c3c | ||
|
|
8b58d52b12 | ||
|
|
50fab971e0 | ||
|
|
df6c76ede9 | ||
|
|
0ff38b918b | ||
|
|
5713b96d13 | ||
|
|
f3d10bd19f | ||
|
|
a2f2cf9c0d | ||
|
|
2b8944bf15 | ||
|
|
db5922e4b7 | ||
|
|
0854ad1f65 | ||
|
|
75cf14a960 | ||
|
|
985039a86b | ||
|
|
acc983b829 | ||
|
|
ae6255cc69 | ||
|
|
830fdff3be | ||
|
|
c2516461ad | ||
|
|
d367998d39 | ||
|
|
6ece944536 | ||
|
|
5413d8dc9c | ||
|
|
5aa78967de | ||
|
|
50d0cbd378 | ||
|
|
8759f8dbf2 | ||
|
|
acb5a9467e | ||
|
|
a31a4d016f | ||
|
|
b39d323a22 | ||
|
|
bfd017a66a | ||
|
|
72bbc79943 | ||
|
|
677e05e46c | ||
|
|
732eeed41d | ||
|
|
ea196e84f1 | ||
|
|
df23d1510d | ||
|
|
76bfd27b33 | ||
|
|
6fe7200481 | ||
|
|
6f737ab0b6 | ||
|
|
ea4adc0e14 | ||
|
|
6ec80df8f4 | ||
|
|
762f4f6964 | ||
|
|
a1b59cba34 | ||
|
|
0584492b8c | ||
|
|
77de17c810 | ||
|
|
9ca01407e8 | ||
|
|
7472af714a | ||
|
|
164a9dab17 | ||
|
|
b2e37804f5 | ||
|
|
cb9b2ff965 | ||
|
|
ad4c34ef28 | ||
|
|
c0c2b365ae | ||
|
|
77565d7eda | ||
|
|
75cad434b6 | ||
|
|
be441a314f | ||
|
|
d5d73273de | ||
|
|
44ffa40b6c | ||
|
|
b4f8bdd719 | ||
|
|
68c6266139 | ||
|
|
65954520d0 | ||
|
|
da03958e97 | ||
|
|
f89bef74d6 | ||
|
|
6a98a215ac | ||
|
|
89ea13956a | ||
|
|
f86d2ab0e4 | ||
|
|
8fef35dbbc | ||
|
|
8a80b97e8c | ||
|
|
7f7cc3a7ca | ||
|
|
09eec5a4a5 | ||
|
|
05516a5d7d | ||
|
|
a42f023074 | ||
|
|
aaa3179d42 | ||
|
|
ce4270b96d | ||
|
|
df3d6fe9c7 | ||
|
|
dd900497f4 | ||
|
|
a1d28fbfe1 | ||
|
|
9ae16ea9b6 | ||
|
|
b9a620fc3b | ||
|
|
42624a7782 | ||
|
|
f97af4c433 | ||
|
|
98994cee01 | ||
|
|
90f891aee4 | ||
|
|
ceabeefe21 | ||
|
|
00d7e1f058 | ||
|
|
13a4bc3ee9 | ||
|
|
939ec61c0e | ||
|
|
0db3240f58 | ||
|
|
11a1bbc866 | ||
|
|
3e3f7c2972 | ||
|
|
95d14c0fdb | ||
|
|
7b2d9365a0 | ||
|
|
1c304b636f | ||
|
|
4e94cdda72 | ||
|
|
dfe669a0cd | ||
|
|
873551e1c4 | ||
|
|
66af4f60c8 | ||
|
|
3dd9024cc7 | ||
|
|
5ebb638925 | ||
|
|
7aa9524835 | ||
|
|
f6d68bb3f3 | ||
|
|
7caaf833dd | ||
|
|
a137f8a434 | ||
|
|
2ee7c82760 | ||
|
|
b730383293 | ||
|
|
d1ed4376ef | ||
|
|
f54b9f7ae1 | ||
|
|
9d1a401e3e | ||
|
|
4fa4fdf1e3 | ||
|
|
5a358caedb | ||
|
|
e2e4f79cec | ||
|
|
ed44eaffec | ||
|
|
1f9dc92074 | ||
|
|
0f4b0c2f54 | ||
|
|
68bc6469ce | ||
|
|
d7e86aac80 | ||
|
|
89a83f78cc | ||
|
|
544af7ce15 | ||
|
|
ba07137933 | ||
|
|
e0bf2afdc9 | ||
|
|
ec73ae309e | ||
|
|
caf94a7a93 | ||
|
|
4092ce2616 | ||
|
|
14c2f16cb6 | ||
|
|
a0720329d0 | ||
|
|
292b05443f | ||
|
|
98cb2b022d | ||
|
|
6f3b45bdbd | ||
|
|
4964c72e71 | ||
|
|
e3042f7623 | ||
|
|
edd11cc99c | ||
|
|
8d533d0e94 | ||
|
|
8f3a5a0e4d | ||
|
|
bd3e38fe40 | ||
|
|
6f05b5b92d | ||
|
|
9bc65d566a | ||
|
|
0cd25f0498 | ||
|
|
48ac8d1136 | ||
|
|
dd2db4bbbf | ||
|
|
9e86f463d8 | ||
|
|
733bd338e7 | ||
|
|
7df9f0b054 | ||
|
|
91ba6948d8 | ||
|
|
af4b2e4624 | ||
|
|
8fe0a45435 | ||
|
|
d55a860114 | ||
|
|
ad23ec040b | ||
|
|
5a69de730c | ||
|
|
6baa00b76d | ||
|
|
a5a90e3c84 | ||
|
|
7e1550f3c0 | ||
|
|
4584dbc047 | ||
|
|
0a869dbbd7 | ||
|
|
20e9a3e8b1 | ||
|
|
0478491f93 | ||
|
|
4797a7d938 | ||
|
|
726a081df1 | ||
|
|
ad7574308f | ||
|
|
03c95ca158 | ||
|
|
b9ebd00365 | ||
|
|
bc6db0d109 | ||
|
|
7590047c6d | ||
|
|
e85a0d4f28 | ||
|
|
16c5819f5c | ||
|
|
db48953e39 | ||
|
|
1106048b37 | ||
|
|
a2698fb70d | ||
|
|
6919b5433b | ||
|
|
c371d35e82 | ||
|
|
0b7904f891 | ||
|
|
c0072d5c72 | ||
|
|
2733cd6606 | ||
|
|
7eef74a7dc | ||
|
|
32150e4a1d | ||
|
|
0c16448065 | ||
|
|
991d384f11 | ||
|
|
aa40ac7777 | ||
|
|
88a368d0b9 | ||
|
|
246ec7c3c3 | ||
|
|
70d0387356 | ||
|
|
19f7bbcde5 | ||
|
|
dd8b2f8809 | ||
|
|
6cf268330f | ||
|
|
9d761815d8 | ||
|
|
6adfb84c0f | ||
|
|
d0b1608181 | ||
|
|
dc7831c31f | ||
|
|
06af78c770 | ||
|
|
58de2a786d | ||
|
|
452d8a47f5 | ||
|
|
6db89778a9 | ||
|
|
65fa663a1a | ||
|
|
59b1d02075 | ||
|
|
cfae20282f | ||
|
|
d06762418e | ||
|
|
a9adfbc06d | ||
|
|
5743ed5434 | ||
|
|
eecb238e18 | ||
|
|
3877cd580c | ||
|
|
393027c1ea | ||
|
|
6be034362b | ||
|
|
103280ca59 | ||
|
|
b715b30ae6 | ||
|
|
6bcc4cdfa3 | ||
|
|
a3ea732870 | ||
|
|
fd6ae8fc0e | ||
|
|
0a54b18d64 | ||
|
|
05072f5d22 | ||
|
|
28dc0fb512 | ||
|
|
2376d88481 | ||
|
|
db8543a0b4 | ||
|
|
b785658db1 | ||
|
|
2d10dd6c9c | ||
|
|
c6428bf9e4 | ||
|
|
0163a52e55 | ||
|
|
815df94db4 | ||
|
|
109578a3ed | ||
|
|
ac15686b47 | ||
|
|
93536976d0 | ||
|
|
6caab2ef06 | ||
|
|
a316403bb5 | ||
|
|
5cf7a61de1 | ||
|
|
d0deb6fe27 | ||
|
|
4ee35d3cd4 | ||
|
|
ec16828497 | ||
|
|
83b8127eb2 | ||
|
|
a42e933240 | ||
|
|
201f6cb900 | ||
|
|
c91d04760a | ||
|
|
91c52c15fd | ||
|
|
c6944bd7cf | ||
|
|
450f4a2d44 | ||
|
|
bda2ff4d23 | ||
|
|
f6d6e93847 | ||
|
|
9cdccbe7f0 | ||
|
|
2ad52e70bb | ||
|
|
656e35c080 | ||
|
|
895df94493 | ||
|
|
8d07ba7f23 | ||
|
|
7e23bc9eeb | ||
|
|
658e391f50 | ||
|
|
69e321c405 | ||
|
|
b5d9c02d9e | ||
|
|
2388dea859 | ||
|
|
178c4d9792 | ||
|
|
0aa286442f | ||
|
|
543fb535df | ||
|
|
9cb0b356ed | ||
|
|
7135aa3369 | ||
|
|
aacd297b3b | ||
|
|
bb2a007ed1 | ||
|
|
356974b478 | ||
|
|
99d72b92e4 | ||
|
|
eaaf106d7c | ||
|
|
86e26025f4 | ||
|
|
4470f0b60b | ||
|
|
3873aa61bc | ||
|
|
3335d54153 | ||
|
|
ad3f2088ef | ||
|
|
39baa4871a | ||
|
|
9d2c5ed74e | ||
|
|
6d76bea5c5 | ||
|
|
82c5c70eb7 | ||
|
|
bb448d6d55 | ||
|
|
44511acd27 | ||
|
|
5aa48cdbdf | ||
|
|
5f19d3ee09 | ||
|
|
598e4e2b79 | ||
|
|
161091513d | ||
|
|
9619e95283 | ||
|
|
6433f50edf | ||
|
|
28d637f620 | ||
|
|
4e50f0ab4b | ||
|
|
a028eaa41a | ||
|
|
5dcc90b31a | ||
|
|
a5c88693b8 | ||
|
|
9b6808273b | ||
|
|
c779c2c19e | ||
|
|
a23387c877 | ||
|
|
0f0d43cc17 | ||
|
|
94e1e8c377 | ||
|
|
5758f6512d | ||
|
|
22c4468180 | ||
|
|
1dd534cce9 | ||
|
|
ce0073e7b0 | ||
|
|
c0074c9f8c | ||
|
|
0fc39cf5f2 | ||
|
|
d4d9f2d4d7 | ||
|
|
802dd50163 | ||
|
|
9e9fcd47c0 | ||
|
|
e7d55d1e1b | ||
|
|
7cef510b29 | ||
|
|
870ade6fa9 | ||
|
|
803c3f2a29 | ||
|
|
4d9e595470 | ||
|
|
bcdbc5fd60 | ||
|
|
61d7103f82 | ||
|
|
93c69248d9 | ||
|
|
18c1735087 | ||
|
|
76fe3f1165 | ||
|
|
6b7fdb8d9e | ||
|
|
46270cfe84 | ||
|
|
2c2e9a2900 | ||
|
|
3d30eea1cb | ||
|
|
1e05c04a39 | ||
|
|
6e55f9d979 | ||
|
|
4a394dd6dd | ||
|
|
9b05ee96af | ||
|
|
97214bef1b | ||
|
|
1823a91d54 | ||
|
|
6f29b807c5 | ||
|
|
b50b215f20 | ||
|
|
33318ef4c6 | ||
|
|
2a284d7f25 | ||
|
|
9cbea88f6f | ||
|
|
2a1b645241 | ||
|
|
74626f8e3f | ||
|
|
fa1166e5d8 | ||
|
|
664db71104 | ||
|
|
cfefb7c629 | ||
|
|
e12724af15 | ||
|
|
d6a986d6cf | ||
|
|
e9bffd0cf2 | ||
|
|
9bef529486 | ||
|
|
ba7fb6525f | ||
|
|
df904e3744 | ||
|
|
b369916904 | ||
|
|
13480fe96c | ||
|
|
44711267fd | ||
|
|
9849fba97a | ||
|
|
bb02033567 | ||
|
|
aab95a1adc | ||
|
|
9f762ac206 | ||
|
|
5f8a319f48 | ||
|
|
24e82309c4 | ||
|
|
5310205911 | ||
|
|
9f7abf1865 | ||
|
|
e53f65f324 | ||
|
|
7601a1d4bf | ||
|
|
9e488b8917 | ||
|
|
b3320ab0fd | ||
|
|
d494c23059 | ||
|
|
7ef28d859f | ||
|
|
c3a5b22dad | ||
|
|
dea0bce0c4 | ||
|
|
0d6731fcc6 | ||
|
|
840ccb517e | ||
|
|
656d81e5dd | ||
|
|
50df8e7dd9 | ||
|
|
1c2dd055c1 | ||
|
|
c448bb3d9a | ||
|
|
268008945c | ||
|
|
d99ffdabd7 | ||
|
|
4f981c1229 | ||
|
|
265a01dcde | ||
|
|
c3f7b29d44 | ||
|
|
954ac1eb07 | ||
|
|
b5c6b55380 | ||
|
|
28982dc84b | ||
|
|
5d77b7b03e | ||
|
|
fa866b0677 | ||
|
|
f1f5bdb2be | ||
|
|
765e01cb00 | ||
|
|
5965879ed1 | ||
|
|
987d91f293 | ||
|
|
c869e60b04 | ||
|
|
d653f00a2d | ||
|
|
1cbd162a22 | ||
|
|
0ec87b967d | ||
|
|
0fded0022c | ||
|
|
fa901add35 | ||
|
|
f24c962785 | ||
|
|
f3e5b360c4 | ||
|
|
e417dade68 | ||
|
|
54f8f67ed5 | ||
|
|
23b9375570 | ||
|
|
58551ba37f | ||
|
|
5ad7c6a3ba | ||
|
|
71c01c0f3b | ||
|
|
326f9e6e93 | ||
|
|
9f0f9054d1 | ||
|
|
00858b6633 | ||
|
|
0463513d5d | ||
|
|
56a36f34a9 | ||
|
|
73dfe2ef11 | ||
|
|
94b82b6865 | ||
|
|
1678dcac82 | ||
|
|
497a509d60 | ||
|
|
bddadc6a9e | ||
|
|
ed0d832e08 | ||
|
|
38a09b164d | ||
|
|
b39c06a9ef | ||
|
|
09626ee6f6 | ||
|
|
3a8076fc3c | ||
|
|
31a7838851 | ||
|
|
7dff254604 | ||
|
|
d4078f0f91 | ||
|
|
90152240c7 | ||
|
|
85e78d2490 | ||
|
|
3368615a77 | ||
|
|
ee8d20a43d | ||
|
|
8f0f01e6b2 | ||
|
|
bdf0953f35 | ||
|
|
e056c30f05 | ||
|
|
b07979cbc7 | ||
|
|
01cbf67bcb | ||
|
|
ce193b60f7 | ||
|
|
2057b0cfa6 | ||
|
|
366de4955b | ||
|
|
32056ab2f1 | ||
|
|
d4e45cc3b0 | ||
|
|
48aa6d6e05 | ||
|
|
5ca3cb18e4 | ||
|
|
c701998a35 | ||
|
|
4453705938 | ||
|
|
144f3fa035 | ||
|
|
fd507d6657 | ||
|
|
9aa0ff6de6 | ||
|
|
37ea8adb73 | ||
|
|
0d6992b80b | ||
|
|
7b15957bb8 | ||
|
|
b1c5c38bdb | ||
|
|
e6b2243496 | ||
|
|
0eb298a943 | ||
|
|
22debb4374 | ||
|
|
ef71405143 | ||
|
|
0990a9b87d | ||
|
|
87727ef360 | ||
|
|
dbc8558ec8 | ||
|
|
143cdf529d | ||
|
|
919c47c993 | ||
|
|
2773c5f500 | ||
|
|
c070e5f40d | ||
|
|
31103faf92 | ||
|
|
05ec937b06 | ||
|
|
3c5a27d9f5 | ||
|
|
1ad805fda3 | ||
|
|
6171ba7c7a | ||
|
|
c60b82d40a | ||
|
|
13a04a0442 | ||
|
|
db3701d76a | ||
|
|
a02d14e425 | ||
|
|
f774df972d | ||
|
|
04ed1112a4 | ||
|
|
2075ed46e1 | ||
|
|
fdb88dd401 | ||
|
|
205958464a | ||
|
|
a7aaffcfe5 | ||
|
|
0222b1fccd | ||
|
|
b98d6fd8ca | ||
|
|
3f6baebd71 | ||
|
|
ce2aa67832 | ||
|
|
83a77dfc74 | ||
|
|
27f406f757 | ||
|
|
72e17b06fc | ||
|
|
49e1e38f05 | ||
|
|
d64a61738f | ||
|
|
d7b8c32c10 | ||
|
|
42423a86a6 | ||
|
|
8ee51a1cb0 | ||
|
|
5f2bc37e96 | ||
|
|
1ba1450e9c | ||
|
|
afabf3650b | ||
|
|
59bad90480 | ||
|
|
a342ad5bda | ||
|
|
3d0b9edb9c | ||
|
|
a23ec160c0 | ||
|
|
b5a29675d6 | ||
|
|
8b3ffe5d0d | ||
|
|
c0b1d201ad | ||
|
|
a8f7080bf2 | ||
|
|
9445a06871 | ||
|
|
d9a66a5894 | ||
|
|
bbb1db014c | ||
|
|
6b5156468c | ||
|
|
0b7ba793b4 | ||
|
|
ef86fceedd | ||
|
|
db6a205f43 | ||
|
|
e8d0da3ce3 | ||
|
|
7e977ad811 | ||
|
|
81cae514a0 | ||
|
|
2d38ef921e | ||
|
|
9f05595cd6 | ||
|
|
178ed1e5af | ||
|
|
2105cdf10b | ||
|
|
a471e057f5 | ||
|
|
ed5de984f2 | ||
|
|
301f5e7113 | ||
|
|
596dbf4ac8 | ||
|
|
9088d9fb9e | ||
|
|
5e58efb07f | ||
|
|
4a835d5306 | ||
|
|
7d62fcd8cc | ||
|
|
42e7c23ee2 | ||
|
|
50e7f4af8e | ||
|
|
3371c6a099 | ||
|
|
feba2af9ed | ||
|
|
6fcfeb2889 | ||
|
|
14f55c9ee5 | ||
|
|
e94d1d4bae | ||
|
|
d48b6ea80e | ||
|
|
8ff8f9628a | ||
|
|
4272f9de99 | ||
|
|
8e0e66d662 | ||
|
|
48349070b3 | ||
|
|
2379899f02 | ||
|
|
35e1cf5733 | ||
|
|
139dff97ef | ||
|
|
be006403ed | ||
|
|
f67a805c1f | ||
|
|
35cfb01973 | ||
|
|
fcea42bf38 | ||
|
|
ad3fe44418 | ||
|
|
ede02eaa8c | ||
|
|
ab27cbef07 | ||
|
|
c2d4f2578c | ||
|
|
fa4c77b495 | ||
|
|
e78c9703b8 | ||
|
|
28e7ec0ef9 | ||
|
|
c3dcd58390 | ||
|
|
c77814235c | ||
|
|
e8d5dbec3e | ||
|
|
d5204e9813 | ||
|
|
d29c9613b9 | ||
|
|
351c89f235 | ||
|
|
9a4cd431e8 | ||
|
|
cef6d5f365 | ||
|
|
1c116eda3b | ||
|
|
a860e96d35 | ||
|
|
d20320e87e | ||
|
|
43ae471038 | ||
|
|
00ff3ecd38 | ||
|
|
b6d6cb4359 | ||
|
|
5a09134d3f | ||
|
|
719a0956ac | ||
|
|
3c197ec3d9 | ||
|
|
0b27244c06 | ||
|
|
0a60ca6468 | ||
|
|
990572ff31 | ||
|
|
8df0c31d35 | ||
|
|
d89b26b77d | ||
|
|
401be9d9fa | ||
|
|
28341fc1f3 | ||
|
|
be277d0218 | ||
|
|
a7315eaee0 | ||
|
|
a1bcd08658 | ||
|
|
cf592554f1 | ||
|
|
cb950ac5d7 | ||
|
|
4f29e2c197 | ||
|
|
c9e0acd055 | ||
|
|
63254159ca | ||
|
|
c5fd4b41b6 | ||
|
|
f5cfd6bfb5 | ||
|
|
81c2e7b059 | ||
|
|
eb481215e3 | ||
|
|
20335e3f27 | ||
|
|
931b3fdc68 | ||
|
|
36a5b4e1d3 | ||
|
|
107b21c463 | ||
|
|
24480fd18c | ||
|
|
09e50d432e | ||
|
|
402ea35971 | ||
|
|
7f074554c4 | ||
|
|
f1d22c6ca4 | ||
|
|
7a877a9653 | ||
|
|
ce59d3f95f | ||
|
|
081f2d14b7 | ||
|
|
14a97b4879 | ||
|
|
4d8526c24d | ||
|
|
66cd88d4c2 | ||
|
|
1f4da1ab48 | ||
|
|
30a12a600a | ||
|
|
994b68bc4c | ||
|
|
d710b88c3e | ||
|
|
214ddc9807 | ||
|
|
6f18a49ba0 | ||
|
|
8b54e965a2 | ||
|
|
0ddde452e3 | ||
|
|
7dbd4a3150 | ||
|
|
98b5fa7bce | ||
|
|
33a45ce0a2 | ||
|
|
41ba37c04e | ||
|
|
f3542b7402 | ||
|
|
f5bb221ecd | ||
|
|
07ab48da1f | ||
|
|
2f50d80a75 | ||
|
|
b06a6fbb51 | ||
|
|
f2490beea2 | ||
|
|
09ab1dabdf | ||
|
|
e1cf360825 | ||
|
|
de8dd1bb8d | ||
|
|
f29bafe89a | ||
|
|
d4695f0192 | ||
|
|
2ab782f4b6 | ||
|
|
aa29ad2014 | ||
|
|
e7fed0f3cf | ||
|
|
3c3c963fd3 | ||
|
|
ae8a25e481 | ||
|
|
8312cfc0aa | ||
|
|
0bbb2aa60d | ||
|
|
9b43ce0ab7 | ||
|
|
f71cd57bc7 | ||
|
|
1570657e28 | ||
|
|
0fbfee8dc8 | ||
|
|
7269dc4e5f | ||
|
|
c5245cb8e2 | ||
|
|
15b3cb20b3 | ||
|
|
42da1b6c23 | ||
|
|
b62db5675d | ||
|
|
da92f2082d | ||
|
|
746dc4f81d | ||
|
|
7b9e06a471 | ||
|
|
7c3c1988a3 | ||
|
|
a005a961f9 | ||
|
|
fb87e180da | ||
|
|
4b1f5b4a44 | ||
|
|
0b24871415 | ||
|
|
a7cb0959e2 | ||
|
|
11e6c93932 | ||
|
|
dd74dd3b1b | ||
|
|
9351bef61c | ||
|
|
f6d79616a4 | ||
|
|
48d88614fb | ||
|
|
f45241d494 | ||
|
|
8d405c1013 | ||
|
|
df1d742339 | ||
|
|
96015d402b | ||
|
|
85a30369c4 | ||
|
|
47289db13e | ||
|
|
1770608525 | ||
|
|
183462ff85 | ||
|
|
b84241c6ad | ||
|
|
c13bc6cfbd | ||
|
|
04da532962 | ||
|
|
fb68bd46c8 | ||
|
|
4963f4dc08 | ||
|
|
c92e44bb1d | ||
|
|
1dcb63f2cc | ||
|
|
b9bcc35dd2 | ||
|
|
0b967b9f45 | ||
|
|
20ae560bb1 | ||
|
|
7fe787c5ab | ||
|
|
5fe6ce6656 | ||
|
|
47ddaaad7b | ||
|
|
8dcc1a4a53 | ||
|
|
0a8f7aa39d | ||
|
|
31071ddb17 | ||
|
|
a003733e51 | ||
|
|
13b105504b | ||
|
|
3b16cdb920 | ||
|
|
607b7cbb0a | ||
|
|
87c6dab41d | ||
|
|
859ad6ea38 | ||
|
|
3e73148390 | ||
|
|
eb2b41bc8a | ||
|
|
75981a3e5f | ||
|
|
062faba8d1 | ||
|
|
52f61c08a5 | ||
|
|
34fd78961b | ||
|
|
9cf68b8903 | ||
|
|
d0526566c4 | ||
|
|
0271556c07 | ||
|
|
8cce2a5977 | ||
|
|
e06eb96fa7 | ||
|
|
edbbc62e5c | ||
|
|
bbd478f322 | ||
|
|
17eb9604e7 | ||
|
|
588e0abdd8 | ||
|
|
b08f5c8390 | ||
|
|
344e680fe3 | ||
|
|
4225d9b3b3 | ||
|
|
1c8c76831e | ||
|
|
912f878906 | ||
|
|
ec081cf0c5 | ||
|
|
1e5849f504 | ||
|
|
10fcc7885f | ||
|
|
29665d5a03 | ||
|
|
e7337d3cb4 | ||
|
|
0904036009 | ||
|
|
c1609a826a | ||
|
|
2453c45b06 | ||
|
|
63c8f87776 | ||
|
|
f20506bf43 | ||
|
|
ec51ff835a | ||
|
|
6a58a08781 | ||
|
|
3f682d467d | ||
|
|
676fc8ea08 | ||
|
|
a732159a3a | ||
|
|
09fc3ca882 | ||
|
|
300ee0183e | ||
|
|
d7f352d699 | ||
|
|
994dba86f6 | ||
|
|
9990e0b807 | ||
|
|
30d0cb7590 | ||
|
|
52b201ff87 | ||
|
|
eb2ffff780 | ||
|
|
d3210d4e27 | ||
|
|
5c2ab9087a | ||
|
|
194c657ba7 | ||
|
|
a5f9c2a022 | ||
|
|
0051022906 | ||
|
|
cfcb6c6b64 | ||
|
|
799aef0b65 | ||
|
|
1b175d85c0 | ||
|
|
9d19c59d44 | ||
|
|
96ccbbe4a2 | ||
|
|
c7b7732092 | ||
|
|
77da67ce98 | ||
|
|
38360f379f | ||
|
|
5a60dfa988 | ||
|
|
590ed037dd | ||
|
|
e71399f5af | ||
|
|
34aadda47a | ||
|
|
0c64f4b006 | ||
|
|
72aff3a695 | ||
|
|
1122681c4f | ||
|
|
78a7f8a207 | ||
|
|
60a118f391 | ||
|
|
cd66399049 | ||
|
|
b0a4b6ebed | ||
|
|
99a4ed9e84 | ||
|
|
e93c08f33a | ||
|
|
a47906cac9 | ||
|
|
423ea9cbc6 | ||
|
|
35714d4f2d | ||
|
|
129cf8d02d | ||
|
|
f561388a1f | ||
|
|
a587247c0d | ||
|
|
72121f98de | ||
|
|
0a66dcc0d6 | ||
|
|
a7605d5699 | ||
|
|
fe3420a512 | ||
|
|
c7e78c21c9 | ||
|
|
228a9a8807 | ||
|
|
61b64fa904 | ||
|
|
6bcd6e2d80 | ||
|
|
d60e079e50 | ||
|
|
ecfd9da7a1 | ||
|
|
697f1f47dd | ||
|
|
19058d3760 | ||
|
|
3a60b6fff7 | ||
|
|
dad4fbca75 | ||
|
|
d56121fdec | ||
|
|
a3db0a005a | ||
|
|
ddf1fdc89c | ||
|
|
e70ae12f2a | ||
|
|
b6660d656f | ||
|
|
2814ce7b7f | ||
|
|
911ebddd69 | ||
|
|
5eacfff7ae | ||
|
|
a06ab724ad | ||
|
|
d8e7bf130f | ||
|
|
058c05a1f7 | ||
|
|
9d06bda4f8 | ||
|
|
346485da8c | ||
|
|
5b2f05741e | ||
|
|
cb4ecea830 | ||
|
|
112b232f9e | ||
|
|
9b0ba9031e | ||
|
|
389885893c | ||
|
|
77a4c2fe34 | ||
|
|
f6f5570d0c | ||
|
|
6a9b218e22 | ||
|
|
8088f37632 | ||
|
|
7bf09906d3 | ||
|
|
6554e142cc | ||
|
|
bf2d033ac3 | ||
|
|
bc688a3d8d | ||
|
|
5bcfe9f192 | ||
|
|
cfbf8a47d7 | ||
|
|
1eca55d96c | ||
|
|
93d7242021 | ||
|
|
612463165a | ||
|
|
c9a8dc21b1 | ||
|
|
c17f0b1e6e | ||
|
|
e5e63efffe | ||
|
|
5152d7e649 | ||
|
|
37a07ace72 | ||
|
|
46524e4298 | ||
|
|
c7eb5fed35 | ||
|
|
5c827baa1a | ||
|
|
27c2562a7f | ||
|
|
2c3dde0d2e | ||
|
|
8219b0398e | ||
|
|
11b4f642a6 | ||
|
|
514ea03655 | ||
|
|
84b86bb6a1 | ||
|
|
fcd7be632c | ||
|
|
cbf043ac7e | ||
|
|
180d9bfb73 | ||
|
|
8423165df4 | ||
|
|
02e628f533 | ||
|
|
3c89e9c015 | ||
|
|
5544491728 | ||
|
|
29d2c9c25e | ||
|
|
e883f76e54 | ||
|
|
10d445badb | ||
|
|
8f2ea30da0 | ||
|
|
a27adffec0 | ||
|
|
14489e3b4b | ||
|
|
3503024fd7 | ||
|
|
1f01c004ae | ||
|
|
cfdb0f4466 | ||
|
|
4168733825 | ||
|
|
0c4e4af228 | ||
|
|
f4cf646f80 | ||
|
|
b641a0b0b3 | ||
|
|
a4bbc9f2ba | ||
|
|
b0c7a6c537 | ||
|
|
eacd9cb6e8 | ||
|
|
8729796e87 | ||
|
|
3fe7501802 | ||
|
|
54e69b266d | ||
|
|
44f10b9ff9 | ||
|
|
e9b7eac289 | ||
|
|
2772a9dcc6 | ||
|
|
1a66be8665 | ||
|
|
2c211d4730 | ||
|
|
88855ab695 | ||
|
|
46b42da5bd | ||
|
|
da3a5fe787 | ||
|
|
ff4af3786e | ||
|
|
ac945254d6 | ||
|
|
9a9038d5e0 | ||
|
|
fa759ae7b2 | ||
|
|
f358605f70 | ||
|
|
d6ba5684e0 | ||
|
|
09c9b25178 | ||
|
|
e86f442249 | ||
|
|
e51ebdcad6 | ||
|
|
eeaa1614fa | ||
|
|
ef79439557 | ||
|
|
8c910b67b4 | ||
|
|
993f75d014 | ||
|
|
01d318d902 | ||
|
|
157a7743a3 | ||
|
|
597dff7ac3 | ||
|
|
c4c596bbf4 | ||
|
|
e21ebd18a6 | ||
|
|
57adcd3c65 | ||
|
|
ae57bfeb89 | ||
|
|
ef21ceb332 | ||
|
|
53ae826186 | ||
|
|
b040db24d4 | ||
|
|
f9f7870c0d | ||
|
|
51a5e97e11 | ||
|
|
69f95e6398 | ||
|
|
10fa36ccf0 | ||
|
|
3b24dfd4c5 | ||
|
|
21673c1ee9 | ||
|
|
8d15166931 | ||
|
|
ebc580ea76 | ||
|
|
4561148ab2 | ||
|
|
acc9537bb7 | ||
|
|
0bb4ff8417 | ||
|
|
314a7b9c56 | ||
|
|
797e0b4641 | ||
|
|
87d2827f0f | ||
|
|
aabeb5f493 | ||
|
|
7f0a35c88a | ||
|
|
29ef770759 | ||
|
|
5927a24f16 | ||
|
|
432cc42cba | ||
|
|
baa4e73ba5 | ||
|
|
eac6856c1d | ||
|
|
78ef129634 | ||
|
|
2769acecc0 | ||
|
|
7beb521d68 | ||
|
|
30823b752d | ||
|
|
e3e9c89a80 | ||
|
|
bb7cc60118 | ||
|
|
63c3b1f56b | ||
|
|
561d7bcc42 | ||
|
|
e1a9004ed5 | ||
|
|
71aa7ec0ef | ||
|
|
8129a488a9 | ||
|
|
5c4980ed2f | ||
|
|
229da1f62d | ||
|
|
cb0906a52d | ||
|
|
5f4cb63324 | ||
|
|
8cf3caa77e | ||
|
|
475c8c594a | ||
|
|
c8d5cec338 | ||
|
|
f5bd1c1eb9 | ||
|
|
a7f04ad732 | ||
|
|
5deda4603e | ||
|
|
cd373f4bca | ||
|
|
9797d7450c | ||
|
|
f8be719dc1 | ||
|
|
122efc00fc | ||
|
|
1f699ac1d1 | ||
|
|
2cd78d5d2f | ||
|
|
0bac7e8d90 | ||
|
|
0002a7549d | ||
|
|
b4b7b55ce1 | ||
|
|
5c753db661 | ||
|
|
d6977b7be0 | ||
|
|
97bb7b534a | ||
|
|
7da9de9991 | ||
|
|
69e7b35abb | ||
|
|
b3123f4d4a | ||
|
|
9d293b2cb4 | ||
|
|
bb47181daa | ||
|
|
009a489d3a | ||
|
|
06fa6eb874 | ||
|
|
12729b36fb | ||
|
|
62b008e693 | ||
|
|
08d27265fc | ||
|
|
c462323dd5 | ||
|
|
967715ab3b | ||
|
|
7b818154dd | ||
|
|
4f6ff4d177 | ||
|
|
0968394c84 | ||
|
|
809b09a1c2 | ||
|
|
b5f93edc7e | ||
|
|
0a72a64489 | ||
|
|
d6ca579495 | ||
|
|
34b8119a21 | ||
|
|
ad74c2c710 | ||
|
|
064d3ad5d5 | ||
|
|
1f584984f4 | ||
|
|
779b79c54b | ||
|
|
4c222ce2cc | ||
|
|
a7a9c0cdf6 | ||
|
|
e979cbba4e | ||
|
|
d0b34e057b | ||
|
|
e4045952c8 | ||
|
|
678e4187c5 | ||
|
|
d1d04b3fb4 | ||
|
|
830ef7e0dc | ||
|
|
2426b51512 | ||
|
|
bcf90e8094 | ||
|
|
2825cb63fb | ||
|
|
85dd187cb0 | ||
|
|
0e71f80a78 | ||
|
|
e0df3d4208 | ||
|
|
62a047337c | ||
|
|
c25d66b11b | ||
|
|
11eb742540 | ||
|
|
d7763b688d | ||
|
|
9aba0560a1 | ||
|
|
70bc239d01 | ||
|
|
a5f5f72346 | ||
|
|
73b7332639 | ||
|
|
64f93cec65 | ||
|
|
58097f0d60 | ||
|
|
41dc16453e | ||
|
|
fb7ff50feb | ||
|
|
97383a71af | ||
|
|
03a142ff62 | ||
|
|
b244b1e1ad | ||
|
|
62916f6a7e | ||
|
|
25b91af8bc | ||
|
|
cce20319ad | ||
|
|
8e33fdf49f | ||
|
|
fe91d13ff3 | ||
|
|
4cea3c2a3b | ||
|
|
7dcdc883e4 | ||
|
|
31fef27ffd | ||
|
|
50043e6574 | ||
|
|
27947dc65f | ||
|
|
5cfe974310 | ||
|
|
c6afaf81e0 | ||
|
|
61b6b90f77 | ||
|
|
36baeed86e | ||
|
|
7c0bc9afd3 | ||
|
|
138f843f5d | ||
|
|
689c0f7b31 | ||
|
|
5d731463f2 | ||
|
|
36f654f41c | ||
|
|
3c79c0f7a8 | ||
|
|
abc91cc7a5 | ||
|
|
211562a0d6 | ||
|
|
075cb9d5f2 | ||
|
|
2955ac3c9b | ||
|
|
a7933c925b | ||
|
|
b52a862f4f | ||
|
|
1fe68c66c5 | ||
|
|
be86f43947 | ||
|
|
a86dc0fc7b | ||
|
|
eaaf3b8812 | ||
|
|
d3463bbef2 | ||
|
|
43db13ab3f | ||
|
|
4a865c4fb8 | ||
|
|
70a526fb4f | ||
|
|
0839c6b6f9 | ||
|
|
7a83e7901a | ||
|
|
928d41c52d | ||
|
|
fdc20e28ff | ||
|
|
0c14560eaa | ||
|
|
a49db96d49 | ||
|
|
66642e7bdf | ||
|
|
6843e22a7b | ||
|
|
4baf13f82c | ||
|
|
f80e6887fe | ||
|
|
548061ff13 | ||
|
|
9d9cb4f139 | ||
|
|
0611ad54d0 | ||
|
|
29a6528e31 | ||
|
|
3bf92fe2ad | ||
|
|
a068e194e9 | ||
|
|
6485fd0f88 | ||
|
|
dec0971c14 | ||
|
|
bbe31149b4 | ||
|
|
b7ea0fa681 | ||
|
|
d72a0ca009 | ||
|
|
9d94067cd0 | ||
|
|
137a6dc771 | ||
|
|
ce36806169 | ||
|
|
6fe8dfc188 | ||
|
|
cb386b7f8d | ||
|
|
8b132c37a0 | ||
|
|
6e8d644574 | ||
|
|
1536cc085c | ||
|
|
87c7ec405d | ||
|
|
9bcecae9a2 | ||
|
|
fbd8113217 | ||
|
|
66c3109037 | ||
|
|
e8116de4bb | ||
|
|
c3de215b08 | ||
|
|
8de7801662 | ||
|
|
ac80b2322b | ||
|
|
70632dd33a | ||
|
|
9ee5c508e4 | ||
|
|
008963156d | ||
|
|
b79f740e2b | ||
|
|
11b0cfb536 | ||
|
|
6abf556234 | ||
|
|
75de91ae1c | ||
|
|
6dd735eab0 | ||
|
|
dfeb54d7f1 | ||
|
|
a053d87bd2 | ||
|
|
e33062feeb | ||
|
|
b0dc79d071 | ||
|
|
19f092bcbb | ||
|
|
fd81096f63 | ||
|
|
2c32460a6e | ||
|
|
9c9c2a23cc | ||
|
|
2067599843 | ||
|
|
00534d91d4 | ||
|
|
88e6c6f93c | ||
|
|
82d59e159f | ||
|
|
aa878482d3 | ||
|
|
754d3762df | ||
|
|
1e643c1c07 | ||
|
|
36ce803828 | ||
|
|
28bd24022b | ||
|
|
84c8eeccc6 | ||
|
|
1a06502806 | ||
|
|
26c72b006c | ||
|
|
b8d7eef536 | ||
|
|
5f23fdd590 | ||
|
|
84533b544a | ||
|
|
673c3c13e7 | ||
|
|
168229e602 | ||
|
|
f2265f0dce | ||
|
|
9ea98b2e2b | ||
|
|
48bb93e8d4 | ||
|
|
24cdbaeb51 | ||
|
|
febfab40de | ||
|
|
89ee0afd61 | ||
|
|
ffa6b4fac8 | ||
|
|
e46444f3b9 | ||
|
|
f4afe00f15 | ||
|
|
92b0f917d6 | ||
|
|
ba68969943 | ||
|
|
72ce7fcb56 | ||
|
|
4df31aa6b0 | ||
|
|
262d3d3dfd | ||
|
|
49cfbdd95f | ||
|
|
a161316dc2 | ||
|
|
800a3dff9d | ||
|
|
0872c727d5 | ||
|
|
d965308346 | ||
|
|
4ffb1e0cdc | ||
|
|
8fc1fe7d8e | ||
|
|
c6d28d7fc5 | ||
|
|
373548d396 | ||
|
|
f1c520fbe8 | ||
|
|
a1babfb2ce | ||
|
|
407c38b4be | ||
|
|
f868939eb9 | ||
|
|
84b143545d | ||
|
|
938d88fa2b | ||
|
|
67951b39d9 | ||
|
|
4b5a0fb59d | ||
|
|
19363c86f9 | ||
|
|
c7daa72b4e | ||
|
|
f7bde024cb | ||
|
|
f4b36867ff | ||
|
|
2f2273b8a8 | ||
|
|
f7ea7061d6 | ||
|
|
83ec23220c | ||
|
|
dc7dadd9a1 | ||
|
|
3563924c6f | ||
|
|
ec1a294ef0 | ||
|
|
eb9282c60a | ||
|
|
2dc5dea10b | ||
|
|
3066a077c7 | ||
|
|
72b317352f | ||
|
|
75a62a5729 | ||
|
|
3085b0e30c | ||
|
|
5c9aa8c9ca | ||
|
|
2c70f87b93 | ||
|
|
aca4318351 | ||
|
|
16777c30ca | ||
|
|
a897c313fb | ||
|
|
9eddc330c5 | ||
|
|
20d813d2ea | ||
|
|
14a990f7ea | ||
|
|
2597a57f3a | ||
|
|
7ae97e4c69 | ||
|
|
e5aa631da0 | ||
|
|
a268f9a892 | ||
|
|
77d08d2340 | ||
|
|
2ffed72977 | ||
|
|
81cb93d03e | ||
|
|
f7d162b955 | ||
|
|
342a661606 | ||
|
|
ece475076e | ||
|
|
8474b74cbf | ||
|
|
9cebb8769f | ||
|
|
0289b71497 | ||
|
|
dc33a3a383 | ||
|
|
b59b0f5387 | ||
|
|
dc6c6fc5e9 | ||
|
|
17649157c1 | ||
|
|
fba25fea0f | ||
|
|
a5cf59d854 | ||
|
|
a971072cb8 | ||
|
|
c946d2309e | ||
|
|
6bde59c14a | ||
|
|
db3de8ce6f | ||
|
|
f53c541538 | ||
|
|
44ea0c418a | ||
|
|
16c68dd51d | ||
|
|
c1d4ba3c80 | ||
|
|
226197a827 | ||
|
|
2516634856 | ||
|
|
d5a720fa0c | ||
|
|
5ac5c30fd6 | ||
|
|
f4db30e988 | ||
|
|
5fc839513e | ||
|
|
7878336866 | ||
|
|
cadf2640fe | ||
|
|
98b79b042b | ||
|
|
51fe7d5599 | ||
|
|
99c8d5d238 | ||
|
|
0e34423dd6 | ||
|
|
18f5d656de | ||
|
|
22fc90713a | ||
|
|
fe444560fd | ||
|
|
0cda5db916 | ||
|
|
986c9dcf5f | ||
|
|
4bfa5e7de8 | ||
|
|
ec10117179 | ||
|
|
77f856a052 | ||
|
|
15fb572b80 | ||
|
|
e71680c8e3 | ||
|
|
005e88d169 | ||
|
|
84c5a76e90 | ||
|
|
b6dd109fcb | ||
|
|
0dfe4b1ae2 | ||
|
|
c9e7ec07f5 | ||
|
|
369ebf48b2 | ||
|
|
c2e0c1be95 | ||
|
|
4ab827da06 | ||
|
|
b965e5bf56 | ||
|
|
b41c633a54 | ||
|
|
1823305a5e | ||
|
|
c4f9515c09 | ||
|
|
de81b135f4 | ||
|
|
0d773a9a61 | ||
|
|
13e4adf18f | ||
|
|
da9ab111bc | ||
|
|
adbb176697 | ||
|
|
347f2ab1e1 | ||
|
|
8e89d2bf92 | ||
|
|
9a168aae45 | ||
|
|
b03daa3c71 | ||
|
|
cd875bd0f9 | ||
|
|
743a5d69b2 | ||
|
|
74b82ea990 | ||
|
|
220f562242 | ||
|
|
46dd16caf9 | ||
|
|
325981b9c9 | ||
|
|
85dcb39f5e | ||
|
|
fea5d234ae | ||
|
|
aa4574cfde | ||
|
|
2e56228323 | ||
|
|
617f075c19 | ||
|
|
ba512f004e | ||
|
|
45d10f2667 | ||
|
|
47d532733b | ||
|
|
aad75b9eea | ||
|
|
73740bc920 | ||
|
|
33cff4a08c | ||
|
|
c16044ee2c | ||
|
|
2cbeb50927 | ||
|
|
3167c24f75 | ||
|
|
c7fd603933 | ||
|
|
c2510d302a | ||
|
|
d8203dce77 | ||
|
|
803b1fa505 | ||
|
|
ca46f4c9ac | ||
|
|
51f760332b | ||
|
|
a2caa53664 | ||
|
|
88a9628e7e | ||
|
|
34a33bbda1 | ||
|
|
261432a7fd | ||
|
|
06d1c082fb | ||
|
|
7f07794ce4 | ||
|
|
dc7cd0993a | ||
|
|
7b31c40676 | ||
|
|
81e9d8ce0f | ||
|
|
0315aa67d7 | ||
|
|
f57e73d87b | ||
|
|
3e2ab9c30a | ||
|
|
ad7d1a53d9 | ||
|
|
46abd002f2 | ||
|
|
8e7595cd34 | ||
|
|
8e150c6862 | ||
|
|
b619d91aab | ||
|
|
0060b5e5af | ||
|
|
ada64d97a6 | ||
|
|
14c0aa1ec1 | ||
|
|
b2705dfb2d | ||
|
|
8638698d61 | ||
|
|
d4328f4d54 | ||
|
|
8eecffd3d5 | ||
|
|
bf2646978a | ||
|
|
f5eb522e39 | ||
|
|
e449b5a436 | ||
|
|
318d816a37 | ||
|
|
684000074e | ||
|
|
5e252ed4af | ||
|
|
eb936d579c | ||
|
|
a278036f1d | ||
|
|
0cf84612e9 | ||
|
|
c2b17ea7a8 | ||
|
|
f4ce5618bf | ||
|
|
8290c9e1f4 | ||
|
|
abee010291 | ||
|
|
004f714924 | ||
|
|
9fcd23d63a | ||
|
|
9a317a2c23 | ||
|
|
e303405ee0 | ||
|
|
5c0062d9df | ||
|
|
bd9a411106 | ||
|
|
6a17e4d141 | ||
|
|
ad169fd6e7 | ||
|
|
f23ead56ad | ||
|
|
98f4d973e7 | ||
|
|
cb2b0aabf3 | ||
|
|
b5fcdea62f | ||
|
|
0eec924c02 | ||
|
|
e27ef39dcf | ||
|
|
37587fd12a | ||
|
|
d930853edf | ||
|
|
e9e525ec63 | ||
|
|
13aee13f18 | ||
|
|
f6df7e5f6f | ||
|
|
cfa80be4c7 | ||
|
|
cde51a3059 | ||
|
|
d767f2ff6b | ||
|
|
074e9fc0eb | ||
|
|
ee75eea939 | ||
|
|
1026f65280 | ||
|
|
b101831a1b | ||
|
|
ba555c0a7c | ||
|
|
74169e6caf | ||
|
|
70ca58f98e | ||
|
|
fd397de6b4 | ||
|
|
6cd672f9e6 | ||
|
|
097e9e446e | ||
|
|
546ded3bdd | ||
|
|
c60d7ed75e | ||
|
|
a3b01e4eae | ||
|
|
818c288ba0 | ||
|
|
ad2fe00af0 | ||
|
|
f8340d770c | ||
|
|
14e21a1009 | ||
|
|
950b8ecb55 | ||
|
|
5525462f11 | ||
|
|
35fa384a5b | ||
|
|
fee0c92280 | ||
|
|
7481b42499 | ||
|
|
7001fd2ef1 | ||
|
|
6aa4737b6e | ||
|
|
5eca6016fa | ||
|
|
585f2a2483 | ||
|
|
26b9659140 | ||
|
|
b2147d42a8 | ||
|
|
08a76dc420 | ||
|
|
f9ec4e37e9 | ||
|
|
d331ece0be | ||
|
|
57cc4f698f | ||
|
|
0b4416d9eb | ||
|
|
4c9aa11b9a | ||
|
|
3c9380c2a2 | ||
|
|
ca8f571978 | ||
|
|
70a39eeea4 | ||
|
|
329abfa617 | ||
|
|
739b59c65d | ||
|
|
06de0d61fa | ||
|
|
557c6ad05a | ||
|
|
9807d3e9c7 | ||
|
|
761fb7dafe | ||
|
|
5b577eb148 | ||
|
|
f1a7524acf | ||
|
|
8eca458e38 | ||
|
|
67c608e76f | ||
|
|
3a203d249e | ||
|
|
bd2f00d5f7 | ||
|
|
b74ca3962b | ||
|
|
77f9618e0c | ||
|
|
e5cd72657e | ||
|
|
98d754f605 | ||
|
|
c2ba3d0ab3 | ||
|
|
d682898828 | ||
|
|
d94c81ed23 | ||
|
|
73599be5b8 | ||
|
|
82ee4cca0f | ||
|
|
870b80998e | ||
|
|
2f9f4c2bc0 | ||
|
|
1ad0c195ad | ||
|
|
cc0bfef0ad | ||
|
|
6f2798e8c4 | ||
|
|
819f338c68 | ||
|
|
5d2d5819cf | ||
|
|
393c131104 | ||
|
|
af149b09a9 | ||
|
|
3087e05212 | ||
|
|
0f7d1dd801 | ||
|
|
461483ea36 | ||
|
|
7bf43cfe9a | ||
|
|
30d24de4d3 | ||
|
|
e2f430dd09 | ||
|
|
5369d48eae | ||
|
|
1bff019a64 | ||
|
|
60dfec3375 | ||
|
|
da47098ccd | ||
|
|
f1ee7bb430 | ||
|
|
be6d75c1ff | ||
|
|
9bf065e1f0 | ||
|
|
8543647cbc | ||
|
|
5c3d18b0e2 | ||
|
|
f6826b60b0 | ||
|
|
5bf86ca7bf | ||
|
|
a8cbc4fbdb | ||
|
|
67bbce0b07 | ||
|
|
dd2a8b682c | ||
|
|
cfba798b89 | ||
|
|
d226c52630 | ||
|
|
202d7eb047 | ||
|
|
891e5d795f | ||
|
|
744538eec6 | ||
|
|
97b2d2d561 | ||
|
|
1900d1ae52 | ||
|
|
3dfeefd174 | ||
|
|
7acb19756b | ||
|
|
dc4c68903f | ||
|
|
2cfe8964f3 | ||
|
|
402be52681 | ||
|
|
8f12cc527b | ||
|
|
0a78310fdd | ||
|
|
012f3e695e | ||
|
|
aad16e2359 | ||
|
|
d3998f5d13 | ||
|
|
0b6d2381dc | ||
|
|
b5070cf647 | ||
|
|
97a2aa5015 | ||
|
|
938cff5381 | ||
|
|
ca5890e6e3 | ||
|
|
b5e569a80d | ||
|
|
e868fb4c22 | ||
|
|
16d3404476 | ||
|
|
f156b21ead | ||
|
|
daef6ab06b | ||
|
|
2f92efc44b | ||
|
|
94bb1665a9 | ||
|
|
1d1c30a483 | ||
|
|
306456c3c7 | ||
|
|
bf56d5c3d7 | ||
|
|
6e6795a6f4 | ||
|
|
befb3f69bd | ||
|
|
65d90495eb | ||
|
|
8f9d991a6c | ||
|
|
fd6f6d6c9a | ||
|
|
20fbb84118 | ||
|
|
85fd0254e8 | ||
|
|
1b4c38d82b | ||
|
|
c20649e14a | ||
|
|
e20910c920 | ||
|
|
efc32f8e3a | ||
|
|
d5a679731c | ||
|
|
95ccaf9478 | ||
|
|
50bc218ccd | ||
|
|
765755a041 | ||
|
|
54455bf625 | ||
|
|
69f1bff0a1 | ||
|
|
96fff242f4 | ||
|
|
ae9d840567 | ||
|
|
b65cab0508 | ||
|
|
d0d62faf57 | ||
|
|
379bbee4ab | ||
|
|
82e3c8395c | ||
|
|
95016dab4d | ||
|
|
9da0d47208 | ||
|
|
667b87cd92 | ||
|
|
3a9971ce8b | ||
|
|
691d87d4ff | ||
|
|
505f2c7385 | ||
|
|
c9988f1144 | ||
|
|
57ada81c62 | ||
|
|
9cf4a7ed55 | ||
|
|
f798fa1af1 | ||
|
|
2a25e4a2d9 | ||
|
|
aae0a6f9d4 | ||
|
|
145cb771b7 | ||
|
|
17f7e618ec | ||
|
|
4b47769f61 | ||
|
|
e87f804070 | ||
|
|
7b413dcbb2 | ||
|
|
ff7a12b323 | ||
|
|
9cdf0596c0 | ||
|
|
909ad68444 | ||
|
|
0404dfd53f | ||
|
|
ddd5269122 | ||
|
|
ffa70d6762 | ||
|
|
a6821209d1 | ||
|
|
55a56c3e04 | ||
|
|
3f55bfea22 | ||
|
|
5346041cd1 | ||
|
|
6ae8001b79 | ||
|
|
7a6ae31864 | ||
|
|
70bab877fb | ||
|
|
d0757ec17b | ||
|
|
fa58e786d2 | ||
|
|
ef5c6fddec | ||
|
|
1b53b3c993 | ||
|
|
c4c6a07aeb | ||
|
|
d922de564d | ||
|
|
662dcf430a | ||
|
|
63eb6cd110 | ||
|
|
c924be4dc9 | ||
|
|
991f155aca | ||
|
|
9ed48b1bfc | ||
|
|
83c3a35ce4 | ||
|
|
d130af130b | ||
|
|
ff0b661f26 | ||
|
|
62095b2847 | ||
|
|
347ae87b12 | ||
|
|
b4c3edcd59 | ||
|
|
50b5f8ca21 | ||
|
|
705a3c39da | ||
|
|
6750a5e875 | ||
|
|
cd29ce1ca3 | ||
|
|
d79921f2d6 | ||
|
|
f5d820c511 | ||
|
|
1b4767cd38 | ||
|
|
58d3699a06 | ||
|
|
49af7dd65d | ||
|
|
4ceb7f3b47 | ||
|
|
a6b166588f | ||
|
|
1b4ae3ab3c | ||
|
|
8c0321f2cc | ||
|
|
a3f60477ec | ||
|
|
69258587bc | ||
|
|
9742129547 | ||
|
|
db1a3a4062 | ||
|
|
3007c8db0e | ||
|
|
a0bbffaf91 | ||
|
|
a984d75638 | ||
|
|
9b9638cc6e | ||
|
|
3091d86186 | ||
|
|
0072cdb17b | ||
|
|
6b88719cf6 | ||
|
|
4079419fa8 | ||
|
|
dc33bc33d6 | ||
|
|
d94c2bdf95 | ||
|
|
43f658f5e9 | ||
|
|
db4ddb0b8b | ||
|
|
073717cd1f | ||
|
|
29cb68ac31 | ||
|
|
b53495a0d2 | ||
|
|
363cc1da69 | ||
|
|
3d72a674e6 | ||
|
|
06f160d8ab | ||
|
|
8c235f6fcc | ||
|
|
6df7eed22a | ||
|
|
6f07fbc536 | ||
|
|
463a48f33a | ||
|
|
f449d05a38 | ||
|
|
580a9d9eba | ||
|
|
094a2fb2a0 | ||
|
|
4dca4081c1 | ||
|
|
c9ccc409a3 | ||
|
|
d5d33f0e9b | ||
|
|
dfd8d84e85 | ||
|
|
f5794f1fc3 | ||
|
|
c67fb34588 | ||
|
|
af1ae4346f | ||
|
|
9c132c5089 | ||
|
|
4e732b6367 | ||
|
|
3720681f84 | ||
|
|
2b16872936 | ||
|
|
dadbf8026e | ||
|
|
fd6071f8d6 | ||
|
|
1148803671 | ||
|
|
4379660b01 | ||
|
|
51efb9a5c1 | ||
|
|
abfbba2ad0 | ||
|
|
7e041e5b49 | ||
|
|
d7dec47de7 | ||
|
|
71527cc4b1 | ||
|
|
5fbe580c08 | ||
|
|
c59372dd62 | ||
|
|
ab0631ff63 | ||
|
|
db8c2e76e5 | ||
|
|
11c099c3dc | ||
|
|
9d0e479b90 | ||
|
|
cd36e2b64b | ||
|
|
930c0bc6a3 | ||
|
|
5a5118a7b0 | ||
|
|
5e3c8a3b15 | ||
|
|
a0e63e7326 | ||
|
|
27a6741d3e | ||
|
|
6850414a27 |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json,ts,tsx}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[package.json]
|
||||
indent_size = 2
|
||||
32
.eslintrc.js
Normal file
32
.eslintrc.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: false,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
// TODO: Remove warn rules when not needed anymore
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"indent": ["warn", 4, { "SwitchCase": 1 }],
|
||||
"no-multiple-empty-lines": ["error", { max: 2, maxEOF: 0 }],
|
||||
"no-self-assign": "off",
|
||||
"no-trailing-spaces": "warn",
|
||||
"object-curly-spacing": ["warn", "always"],
|
||||
"prefer-template": "warn",
|
||||
"quotes": ["warn", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"require-await": "warn",
|
||||
"semi": "warn",
|
||||
"no-console": "warn"
|
||||
},
|
||||
};
|
||||
21
.github/workflows/ci.yml
vendored
Normal file
21
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: SQLite CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests with SQLite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Initialization
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Run Tests
|
||||
timeout-minutes: 5
|
||||
run: npm test
|
||||
18
.github/workflows/db-backup.yml
vendored
Normal file
18
.github/workflows/db-backup.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Docker image builds
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- containers/backup-db/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
backup-db:
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
with:
|
||||
name: "db-backup"
|
||||
username: "ajayyy"
|
||||
folder: "./containers/backup-db"
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
46
.github/workflows/docker-build.yml
vendored
Normal file
46
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# Based on https://github.com/ajayyy/sb-mirror/blob/main/.github/workflows/docker-build.yml
|
||||
name: multi-build-docker
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
name:
|
||||
required: true
|
||||
type: string
|
||||
username:
|
||||
required: true
|
||||
type: string
|
||||
folder:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
GH_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build_container:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ inputs.username }}/${{ inputs.name }}
|
||||
tags: |
|
||||
type-raw,value=alpine
|
||||
flavor: |
|
||||
latest=true
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
- name: push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ${{ inputs.folder }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
21
.github/workflows/eslint.yml
vendored
Normal file
21
.github/workflows/eslint.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Linting
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint with ESLint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Initialization
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Run Tests
|
||||
timeout-minutes: 5
|
||||
run: npm run lint
|
||||
28
.github/workflows/generate-sqlite-base.yml
vendored
Normal file
28
.github/workflows/generate-sqlite-base.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: create-sqlite-base
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- databases/**
|
||||
|
||||
jobs:
|
||||
make-base-db:
|
||||
name: Generate SQLite base .db
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Set config
|
||||
run: |
|
||||
echo '{"mode": "init-db-and-exit"}' > config.json
|
||||
- name: Run Server
|
||||
timeout-minutes: 10
|
||||
run: npm start
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: SponsorTimesDB.db
|
||||
path: databases/sponsorTimes.db
|
||||
29
.github/workflows/postgres-redis-ci.yml
vendored
Normal file
29
.github/workflows/postgres-redis-ci.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: PostgreSQL + Redis CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests with PostgreSQL and Redis
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the docker-compose stack
|
||||
env:
|
||||
PG_USER: ci_db_user
|
||||
PG_PASS: ci_db_pass
|
||||
run: docker-compose -f docker/docker-compose-ci.yml up -d
|
||||
- name: Check running containers
|
||||
run: docker ps
|
||||
- uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Run Tests
|
||||
env:
|
||||
TEST_POSTGRES: true
|
||||
timeout-minutes: 5
|
||||
run: npm test
|
||||
25
.github/workflows/sb-server.yml
vendored
Normal file
25
.github/workflows/sb-server.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Docker image builds
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sb-server:
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
with:
|
||||
name: "sb-server"
|
||||
username: "ajayyy"
|
||||
folder: "."
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
rsync-host:
|
||||
needs: sb-server
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
with:
|
||||
name: "rsync-host"
|
||||
username: "ajayyy"
|
||||
folder: "./containers/rsync"
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
12
.github/workflows/take-action.yml
vendored
Normal file
12
.github/workflows/take-action.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Assign issue to contributor
|
||||
on: [issue_comment]
|
||||
|
||||
jobs:
|
||||
assign:
|
||||
name: Take an issue
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: take the issue
|
||||
uses: bdougie/take-action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
94
.gitignore
vendored
94
.gitignore
vendored
@@ -2,47 +2,9 @@
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
@@ -53,39 +15,33 @@ typings/
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Databases
|
||||
databases
|
||||
databases/sponsorTimes.db
|
||||
databases/sponsorTimes.db-shm
|
||||
databases/sponsorTimes.db-wal
|
||||
databases/private.db
|
||||
databases/private.db-shm
|
||||
databases/private.db-wal
|
||||
databases/sponsorTimesReal.db
|
||||
test/databases/sponsorTimes.db
|
||||
test/databases/sponsorTimes.db-shm
|
||||
test/databases/sponsorTimes.db-wal
|
||||
test/databases/private.db
|
||||
databases/cache
|
||||
docker/database-export
|
||||
|
||||
# Config files
|
||||
config.json
|
||||
docker/database.env
|
||||
|
||||
# Other
|
||||
working
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
/.idea/
|
||||
/dist/
|
||||
|
||||
283
DatabaseSchema.md
Normal file
283
DatabaseSchema.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# SponsorTimesDB
|
||||
|
||||
[vipUsers](#vipUsers)
|
||||
[sponsorTimes](#sponsorTimes)
|
||||
[userNames](#userNames)
|
||||
[categoryVotes](#categoryVotes)
|
||||
[lockCategories](#lockCategories)
|
||||
[warnings](#warnings)
|
||||
[shadowBannedUsers](#shadowBannedUsers)
|
||||
[unlistedVideos](#unlistedVideos)
|
||||
[config](#config)
|
||||
[archivedSponsorTimes](#archivedSponsorTimes)
|
||||
|
||||
### vipUsers
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| userID | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| vipUsers_index | userID |
|
||||
|
||||
### sponsorTimes
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| startTime | REAL | not null |
|
||||
| endTime | REAL | not null |
|
||||
| votes | INTEGER | not null |
|
||||
| locked | INTEGER | not null, default '0' |
|
||||
| incorrectVotes | INTEGER | not null, default 1 |
|
||||
| UUID | TEXT | not null, unique |
|
||||
| userID | TEXT | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
| views | INTEGER | not null |
|
||||
| category | TEXT | not null, default 'sponsor' |
|
||||
| actionType | TEXT | not null, default 'skip' |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
| videoDuration | INTEGER | not null, default '0' |
|
||||
| hidden | INTEGER | not null, default '0' |
|
||||
| reputation | REAL | not null, default '0' |
|
||||
| shadowHidden | INTEGER | not null |
|
||||
| hashedVideoID | TEXT | not null, default '', sha256 |
|
||||
| userAgent | TEXT | not null, default '' |
|
||||
| description | TEXT | not null, default '' |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| sponsorTime_timeSubmitted | timeSubmitted |
|
||||
| sponsorTime_userID | userID |
|
||||
| sponsorTimes_UUID | UUID |
|
||||
| sponsorTimes_hashedVideoID | hashedVideoID, category |
|
||||
| sponsorTimes_videoID | videoID, service, category, timeSubmitted |
|
||||
|
||||
### userNames
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| userID | TEXT | not null |
|
||||
| userName | TEXT | not null |
|
||||
| locked | INTEGER | not nul, default '0' |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| userNames_userID | userID |
|
||||
|
||||
### categoryVotes
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| UUID | TEXT | not null |
|
||||
| category | TEXT | not null |
|
||||
| votes | INTEGER | not null, default 0 |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| categoryVotes_UUID_public | UUID, category |
|
||||
|
||||
### lockCategories
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| userID | TEXT | not null |
|
||||
| actionType | TEXT | not null, default 'skip' |
|
||||
| category | TEXT | not null |
|
||||
| hashedVideoID | TEXT | not null, default '' |
|
||||
| reason | TEXT | not null, default '' |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| lockCategories_videoID | videoID, service, category |
|
||||
|
||||
### warnings
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| userID | TEXT | not null |
|
||||
| issueTime | INTEGER | not null |
|
||||
| issuerUserID | TEXT | not null |
|
||||
| enabled | INTEGER | not null |
|
||||
| reason | TEXT | not null, default '' |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| warnings_index | userID |
|
||||
| warnings_issueTime | issueTime |
|
||||
|
||||
### shadowBannedUsers
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| userID | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| shadowBannedUsers_index | userID |
|
||||
|
||||
### videoInfo
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| channelID | TEXT | not null |
|
||||
| title | TEXT | not null |
|
||||
| published | REAL | not null |
|
||||
| genreUrl | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| videoInfo_videoID | timeSubmitted |
|
||||
| videoInfo_channelID | userID |
|
||||
|
||||
### unlistedVideos
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| year | TEXT | not null |
|
||||
| views | TEXT | not null |
|
||||
| channelID | TEXT | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
|
||||
### config
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| key | TEXT | not null, unique |
|
||||
| value | TEXT | not null |
|
||||
|
||||
### archivedSponsorTimes
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| startTime | REAL | not null |
|
||||
| endTime | REAL | not null |
|
||||
| votes | INTEGER | not null |
|
||||
| locked | INTEGER | not null, default '0' |
|
||||
| incorrectVotes | INTEGER | not null, default 1 |
|
||||
| UUID | TEXT | not null, unique |
|
||||
| userID | TEXT | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
| views | INTEGER | not null |
|
||||
| category | TEXT | not null, default 'sponsor' |
|
||||
| actionType | TEXT | not null, default 'skip' |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
| videoDuration | INTEGER | not null, default '0' |
|
||||
| hidden | INTEGER | not null, default '0' |
|
||||
| reputation | REAL | not null, default '0' |
|
||||
| shadowHidden | INTEGER | not null |
|
||||
| hashedVideoID | TEXT | not null, default '', sha256 |
|
||||
| userAgent | TEXT | not null, default '' |
|
||||
|
||||
### ratings
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
| type | INTEGER | not null |
|
||||
| count | INTEGER | not null |
|
||||
| hashedVideoID | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| ratings_hashedVideoID_gin | hashedVideoID |
|
||||
| ratings_hashedVideoID | hashedVideoID, service |
|
||||
| ratings_videoID | videoID, service |
|
||||
|
||||
# Private
|
||||
|
||||
[votes](#votes)
|
||||
[categoryVotes](#categoryVotes)
|
||||
[sponsorTimes](#sponsorTimes)
|
||||
[config](#config)
|
||||
[ratings](#ratings)
|
||||
[tempVipLog](#tempVipLog)
|
||||
[userNameLogs](#userNameLogs)
|
||||
|
||||
### votes
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| UUID | TEXT | not null |
|
||||
| userID | TEXT | not null |
|
||||
| hashedIP | TEXT | not null |
|
||||
| type | INTEGER | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| votes_userID | UUID |
|
||||
|
||||
### categoryVotes
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| UUID | TEXT | not null |
|
||||
| userID | TEXT | not null |
|
||||
| hashedIP | TEXT | not null |
|
||||
| category | TEXT | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| categoryVotes_UUID | UUID, userID, hasedIP, category |
|
||||
|
||||
### sponsorTimes
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| hashedIP | TEXT | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| sponsorTimes_hashedIP | hashedIP |
|
||||
| privateDB_sponsorTimes_videoID_v2 | videoID, service |
|
||||
|
||||
### config
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| key | TEXT | not null |
|
||||
| value | TEXT | not null |
|
||||
|
||||
### ratings
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| service | TEXT | not null, default 'YouTube' |
|
||||
| userID | TEXT | not null |
|
||||
| type | INTEGER | not null |
|
||||
| timeSubmitted | INTEGER | not null |
|
||||
| hashedIP | TEXT | not null |
|
||||
|
||||
| index | field |
|
||||
| -- | :--: |
|
||||
| ratings_videoID | videoID, service, userID, timeSubmitted |
|
||||
|
||||
### tempVipLog
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| issuerUserID | TEXT | not null |
|
||||
| targetUserID | TEXT | not null |
|
||||
| enabled | BOOLEAN | not null |
|
||||
| updatedAt | INTEGER | not null |
|
||||
|
||||
### userNameLogs
|
||||
|
||||
| Name | Type | |
|
||||
| -- | :--: | -- |
|
||||
| userID | TEXT | not null |
|
||||
| newUserName | TEXT | not null |
|
||||
| oldUserName | TEXT | not null |
|
||||
| updatedByAdmin | BOOLEAN | not null |
|
||||
| updatedAt | INTEGER | not null |
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM node:16-alpine as builder
|
||||
RUN apk add --no-cache --virtual .build-deps python3 make g++
|
||||
COPY package.json package-lock.json tsconfig.json entrypoint.sh ./
|
||||
COPY src src
|
||||
RUN npm ci && npm run tsc
|
||||
|
||||
FROM node:16-alpine as app
|
||||
WORKDIR /usr/src/app
|
||||
RUN apk add git postgresql-client
|
||||
COPY --from=builder ./node_modules ./node_modules
|
||||
COPY --from=builder ./dist ./dist
|
||||
COPY ./.git ./.git
|
||||
COPY entrypoint.sh .
|
||||
COPY databases/*.sql databases/
|
||||
EXPOSE 8080
|
||||
CMD ./entrypoint.sh
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ajay Ramachandran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
30
README.MD
30
README.MD
@@ -1,17 +1,37 @@
|
||||
# SponsorBlocker Server
|
||||
# SponsorBlock Server
|
||||
|
||||
SponsorBlocker is an extension that will skip over sponsored segments of YouTube videos. SponsorBlocker is a crowdsourced browser extension that let's anyone submit the start and end time's of sponsored segments of YouTube videos. Once one person submits this information, everyone else with this extension will skip right over the sponsored segment.
|
||||
SponsorBlock is an extension that will skip over sponsored segments of YouTube videos. SponsorBlock is a crowdsourced browser extension that let's anyone submit the start and end time's of sponsored segments of YouTube videos. Once one person submits this information, everyone else with this extension will skip right over the sponsored segment.
|
||||
|
||||
This is the server backend for it
|
||||
|
||||
# Server
|
||||
|
||||
This is a simple Sqlite database that will hold all the timing data.
|
||||
This uses a Postgres or Sqlite database to hold all the timing data.
|
||||
|
||||
To make sure that this project doesn't die, I have made the database publicly downloadable at https://sponsor.ajay.app/database.db. So, you can download a backup or get archive.org to take a backup if you do desire.
|
||||
To make sure that this project doesn't die, I have made the database publicly downloadable at https://sponsor.ajay.app/database. You can download a backup or get archive.org to take a backup if you do desire. The database is under [this license](https://creativecommons.org/licenses/by-nc-sa/4.0/) unless you get explicit permission from me.
|
||||
|
||||
Hopefully this project can be combined with projects like [this](https://github.com/Sponsoff/sponsorship_remover) and use this data to create a neural network to predict when sponsored segments happen. That project is sadly abandoned now, so I have decided to attempt to revive this idea.
|
||||
|
||||
# Client
|
||||
|
||||
The client web browser extension is available here: https://github.com/ajayyy/SponsorBlocker
|
||||
The client web browser extension is available here: https://github.com/ajayyy/SponsorBlock
|
||||
|
||||
# Build Yourself
|
||||
|
||||
This is a node.js server, so clone this repo and run `npm install` to install all dependencies.
|
||||
|
||||
Make sure to put the database files in the `./databases` folder if you want to use a pre-existing database. Otherwise, a fresh database will be created.
|
||||
|
||||
Rename `config.json.example` to `config.json` and fill the parameters inside. Make sure to remove the comments as comments are not supported in JSON.
|
||||
|
||||
Ensure all the tests pass with `npm test`
|
||||
|
||||
Run the server with `npm start`.
|
||||
|
||||
# Developing
|
||||
|
||||
If you want to make changes, run `npm run dev` to automatically reload the server and run tests whenever a file is saved.
|
||||
|
||||
# API Docs
|
||||
|
||||
Available [here](https://wiki.sponsor.ajay.app/index.php/API_Docs)
|
||||
|
||||
71
ci.json
Normal file
71
ci.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"port": 8080,
|
||||
"mockPort": 8081,
|
||||
"globalSalt": "testSalt",
|
||||
"adminUserID": "4bdfdc9cddf2c7d07a8a87b57bf6d25389fb75d1399674ee0e0938a6a60f4c3b",
|
||||
"newLeafURLs": ["placeholder"],
|
||||
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook",
|
||||
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook",
|
||||
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook",
|
||||
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook",
|
||||
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
|
||||
"behindProxy": true,
|
||||
"postgres": {
|
||||
"user": "ci_db_user",
|
||||
"password": "ci_db_pass",
|
||||
"host": "localhost",
|
||||
"port": 5432
|
||||
},
|
||||
"redis": {
|
||||
"socket": {
|
||||
"host": "localhost",
|
||||
"port": 6379
|
||||
}
|
||||
},
|
||||
"createDatabaseIfNotExist": true,
|
||||
"schemaFolder": "./databases",
|
||||
"dbSchema": "./databases/_sponsorTimes.db.sql",
|
||||
"privateDBSchema": "./databases/_private.db.sql",
|
||||
"categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"],
|
||||
"mode": "test",
|
||||
"readOnly": false,
|
||||
"webhooks": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8081/CustomWebhook",
|
||||
"key": "superSecretKey",
|
||||
"scopes": [
|
||||
"vote.up",
|
||||
"vote.down"
|
||||
]
|
||||
}, {
|
||||
"url": "http://127.0.0.1:8081/FailedWebhook",
|
||||
"key": "superSecretKey",
|
||||
"scopes": [
|
||||
"vote.up",
|
||||
"vote.down"
|
||||
]
|
||||
}, {
|
||||
"url": "http://127.0.0.1:8099/WrongPort",
|
||||
"key": "superSecretKey",
|
||||
"scopes": [
|
||||
"vote.up",
|
||||
"vote.down"
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxNumberOfActiveWarnings": 3,
|
||||
"hoursAfterWarningExpires": 24,
|
||||
"rateLimit": {
|
||||
"vote": {
|
||||
"windowMs": 900000,
|
||||
"max": 20,
|
||||
"message": "Too many votes, please try again later",
|
||||
"statusCode": 429
|
||||
},
|
||||
"view": {
|
||||
"windowMs": 900000,
|
||||
"max": 20,
|
||||
"statusCode": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
70
config.json.example
Normal file
70
config.json.example
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
// Remove all comments from the config when using it!
|
||||
|
||||
"port": 80,
|
||||
"globalSalt": "[global salt (pepper) that is added to every ip before hashing to make it even harder for someone to decode the ip]",
|
||||
"adminUserID": "[the hashed id of the user who can perform admin actions]",
|
||||
"newLeafURLs": ["http://localhost:3241"],
|
||||
"discordReportChannelWebhookURL": null, //URL from discord if you would like notifications when someone makes a report [optional]
|
||||
"discordFirstTimeSubmissionsWebhookURL": null, //URL from discord if you would like notifications when someone makes a first time submission [optional]
|
||||
"discordCompletelyIncorrectReportWebhookURL": null, //URL from discord if you would like notifications when someone reports a submission as completely incorrect [optional]
|
||||
"neuralBlockURL": null, // URL to check submissions against neural block. Ex. https://ai.neuralblock.app
|
||||
"discordNeuralBlockRejectWebhookURL": null, //URL from discord if you would like notifications when NeuralBlock rejects a submission [optional]
|
||||
"userCounterURL": null, // For user counting. URL to instance of https://github.com/ajayyy/PrivacyUserCount
|
||||
"proxySubmission": null, // Base url to proxy submissions to persist // e.g. https://sponsor.ajay.app (no trailing slash)
|
||||
"behindProxy": "X-Forwarded-For", //Options: "X-Forwarded-For", "Cloudflare", "X-Real-IP", anything else will mean it is not behind a proxy. True defaults to "X-Forwarded-For"
|
||||
"db": "./databases/sponsorTimes.db",
|
||||
"privateDB": "./databases/private.db",
|
||||
"createDatabaseIfNotExist": true, //This will run on startup every time (unless readOnly is true) - so ensure "create table if not exists" is used in the schema
|
||||
"schemaFolder": "./databases",
|
||||
"dbSchema": "./databases/_sponsorTimes.db.sql",
|
||||
"privateDBSchema": "./databases/_private.db.sql",
|
||||
// when using redis, add `"enable_offline_queue": false` to force commands to fail instead of timing out
|
||||
"mode": "development",
|
||||
"readOnly": false,
|
||||
"webhooks": [],
|
||||
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "poi_highlight"], // List of supported categories any other category will be rejected
|
||||
"getTopUsersCacheTimeMinutes": 5, // cacheTime for getTopUsers result in minutes
|
||||
"maxNumberOfActiveWarnings": 3, // Users with this number of warnings will be blocked until warnings expire
|
||||
"hoursAfterWarningExpire": 24,
|
||||
"rateLimit": {
|
||||
"vote": {
|
||||
"windowMs": 900000, // 15 minutes
|
||||
"max": 20, // 20 requests in 15min time window
|
||||
"message": "Too many votes, please try again later",
|
||||
"statusCode": 429
|
||||
},
|
||||
"view": {
|
||||
"windowMs": 900000, // 15 minutes
|
||||
"max": 20, // 20 requests in 15min time window
|
||||
"statusCode": 200
|
||||
}
|
||||
},
|
||||
"maxRewardTimePerSegmentInSeconds": 86400, // maximum time a user get rewarded in the leaderboard for a single segment
|
||||
"dumpDatabase": {
|
||||
"enabled": true,
|
||||
"minTimeBetweenMs": 60000, // 1 minute between dumps
|
||||
"appExportPath": "./docker/database-export",
|
||||
"postgresExportPath": "/opt/exports",
|
||||
"tables": [{
|
||||
"name": "sponsorTimes",
|
||||
"order": "timeSubmitted"
|
||||
},
|
||||
{
|
||||
"name": "userNames"
|
||||
},
|
||||
{
|
||||
"name": "categoryVotes"
|
||||
},
|
||||
{
|
||||
"name": "lockCategories"
|
||||
},
|
||||
{
|
||||
"name": "warnings",
|
||||
"order": "issueTime"
|
||||
},
|
||||
{
|
||||
"name": "vipUsers"
|
||||
}]
|
||||
}
|
||||
}
|
||||
13
containers/backup-db/Dockerfile
Normal file
13
containers/backup-db/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM alpine
|
||||
RUN apk add postgresql-client
|
||||
RUN apk add restic --repository http://dl-cdn.alpinelinux.org/alpine/latest-stable/community/
|
||||
|
||||
COPY ./backup.sh /usr/src/app/backup.sh
|
||||
RUN chmod +x /usr/src/app/backup.sh
|
||||
COPY ./backup.sh /usr/src/app/forget.sh
|
||||
RUN chmod +x /usr/src/app/forget.sh
|
||||
|
||||
RUN echo '30 * * * * /usr/src/app/backup.sh' >> /etc/crontabs/root
|
||||
RUN echo '10 0 * * 1 /usr/src/app/forget.sh' >> /etc/crontabs/root
|
||||
|
||||
CMD crond -l 2 -f
|
||||
6
containers/backup-db/backup.sh
Normal file
6
containers/backup-db/backup.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
mkdir ./dump
|
||||
|
||||
pg_dump -f ./dump/sponsorTimes.dump sponsorTimes
|
||||
pg_dump -f ./dump/privateDB.dump privateDB
|
||||
|
||||
restic backup ./dump
|
||||
1
containers/backup-db/forget.sh
Normal file
1
containers/backup-db/forget.sh
Normal file
@@ -0,0 +1 @@
|
||||
restic forget --prune --keep-last 48 --keep-daily 7 --keep-weekly 8
|
||||
6
containers/rsync/Dockerfile
Normal file
6
containers/rsync/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM ghcr.io/ajayyy/sb-server:latest
|
||||
EXPOSE 873/tcp
|
||||
RUN apk add rsync>3.2.4-r0
|
||||
RUN mkdir /usr/src/app/database-export
|
||||
|
||||
CMD rsync --no-detach --daemon & ./entrypoint.sh
|
||||
29
databases/_private.db.sql
Normal file
29
databases/_private.db.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "votes" (
|
||||
"UUID" TEXT NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"hashedIP" TEXT NOT NULL,
|
||||
"type" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "categoryVotes" (
|
||||
"UUID" TEXT NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"hashedIP" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "sponsorTimes" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"hashedIP" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "config" (
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
26
databases/_private_indexes.sql
Normal file
26
databases/_private_indexes.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- sponsorTimes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "privateDB_sponsorTimes_v4"
|
||||
ON public."sponsorTimes" USING btree
|
||||
("videoID" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST, "timeSubmitted" ASC NULLS LAST);
|
||||
|
||||
-- votes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "votes_userID"
|
||||
ON public.votes USING btree
|
||||
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- categoryVotes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "categoryVotes_UUID"
|
||||
ON public."categoryVotes" USING btree
|
||||
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" ASC NULLS LAST, "hashedIP" COLLATE pg_catalog."default" ASC NULLS LAST, category COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- ratings
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_videoID"
|
||||
ON public."ratings" USING btree
|
||||
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST, "userID" COLLATE pg_catalog."default" ASC NULLS LAST, "timeSubmitted" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
43
databases/_sponsorTimes.db.sql
Normal file
43
databases/_sponsorTimes.db.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "vipUsers" (
|
||||
"userID" TEXT NOT NULL
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "sponsorTimes" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"shadowHidden" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "userNames" (
|
||||
"userID" TEXT NOT NULL,
|
||||
"userName" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "categoryVotes" (
|
||||
"UUID" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"votes" INTEGER NOT NULL default 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "config" (
|
||||
"key" TEXT NOT NULL UNIQUE,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto; --!sqlite-ignore
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm; --!sqlite-ignore
|
||||
|
||||
COMMIT;
|
||||
106
databases/_sponsorTimes_indexes.sql
Normal file
106
databases/_sponsorTimes_indexes.sql
Normal file
@@ -0,0 +1,106 @@
|
||||
-- sponsorTimes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "sponsorTime_timeSubmitted"
|
||||
ON public."sponsorTimes" USING btree
|
||||
("timeSubmitted" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "sponsorTime_userID"
|
||||
ON public."sponsorTimes" USING btree
|
||||
("userID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "sponsorTimes_UUID"
|
||||
ON public."sponsorTimes" USING btree
|
||||
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "sponsorTimes_hashedVideoID"
|
||||
ON public."sponsorTimes" USING btree
|
||||
(service COLLATE pg_catalog."default" ASC NULLS LAST, "hashedVideoID" text_pattern_ops ASC NULLS LAST, "startTime" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "sponsorTimes_videoID"
|
||||
ON public."sponsorTimes" USING btree
|
||||
(service COLLATE pg_catalog."default" ASC NULLS LAST, "videoID" COLLATE pg_catalog."default" ASC NULLS LAST, "startTime" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "sponsorTimes_description_gin"
|
||||
ON public."sponsorTimes" USING gin
|
||||
("description" COLLATE pg_catalog."default" gin_trgm_ops, category COLLATE pg_catalog."default" gin_trgm_ops)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- userNames
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "userNames_userID"
|
||||
ON public."userNames" USING btree
|
||||
("userID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- vipUsers
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "vipUsers_index"
|
||||
ON public."vipUsers" USING btree
|
||||
("userID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- warnings
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "warnings_index"
|
||||
ON public.warnings USING btree
|
||||
("userID" COLLATE pg_catalog."default" ASC NULLS LAST, "issueTime" DESC NULLS LAST, enabled DESC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "warnings_issueTime"
|
||||
ON public.warnings USING btree
|
||||
("issueTime" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- lockCategories
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "lockCategories_videoID"
|
||||
ON public."lockCategories" USING btree
|
||||
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST, category COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- categoryVotes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "categoryVotes_UUID_public"
|
||||
ON public."categoryVotes" USING btree
|
||||
("UUID" COLLATE pg_catalog."default" ASC NULLS LAST, category COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- shadowBannedUsers
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "shadowBannedUsers_index"
|
||||
ON public."shadowBannedUsers" USING btree
|
||||
("userID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- videoInfo
|
||||
CREATE INDEX IF NOT EXISTS "videoInfo_videoID"
|
||||
ON public."videoInfo" USING btree
|
||||
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "videoInfo_channelID"
|
||||
ON public."videoInfo" USING btree
|
||||
("channelID" COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
-- ratings
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_hashedVideoID_gin"
|
||||
ON public."ratings" USING gin
|
||||
("hashedVideoID" COLLATE pg_catalog."default" gin_trgm_ops)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_hashedVideoID"
|
||||
ON public."ratings" USING btree
|
||||
("hashedVideoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ratings_videoID"
|
||||
ON public."ratings" USING btree
|
||||
("videoID" COLLATE pg_catalog."default" ASC NULLS LAST, service COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
8
databases/_upgrade_private_1.sql
Normal file
8
databases/_upgrade_private_1.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* for testing the db upgrade, don't remove because it looks empty */
|
||||
|
||||
/* Add version to config */
|
||||
INSERT INTO config (key, value) VALUES('version', 1);
|
||||
|
||||
COMMIT;
|
||||
13
databases/_upgrade_private_2.sql
Normal file
13
databases/_upgrade_private_2.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "userNameLogs" (
|
||||
"userID" TEXT NOT NULL,
|
||||
"newUserName" TEXT NOT NULL,
|
||||
"oldUserName" TEXT NOT NULL,
|
||||
"updatedByAdmin" BOOLEAN NOT NULL,
|
||||
"updatedAt" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 2 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
10
databases/_upgrade_private_3.sql
Normal file
10
databases/_upgrade_private_3.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE "sponsorTimes" ADD "service" TEXT NOT NULL default 'YouTube';
|
||||
-- UPDATE "sponsorTimes" SET "service" = "YouTube";
|
||||
|
||||
DROP INDEX IF EXISTS "privateDB_sponsorTimes_videoID";
|
||||
|
||||
UPDATE "config" SET value = 3 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
14
databases/_upgrade_private_4.sql
Normal file
14
databases/_upgrade_private_4.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ratings" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"service" TEXT NOT NULL default 'YouTube',
|
||||
"type" INTEGER NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"hashedIP" TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 4 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
12
databases/_upgrade_private_5.sql
Normal file
12
databases/_upgrade_private_5.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "tempVipLog" (
|
||||
"issuerUserID" TEXT NOT NULL,
|
||||
"targetUserID" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL,
|
||||
"updatedAt" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 5 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
7
databases/_upgrade_private_6.sql
Normal file
7
databases/_upgrade_private_6.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DROP INDEX IF EXISTS "sponsorTimes_hashedIP", "privateDB_sponsorTimes_videoID_v2"; --!sqlite-ignore
|
||||
|
||||
UPDATE "config" SET value = 6 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
7
databases/_upgrade_private_7.sql
Normal file
7
databases/_upgrade_private_7.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE "votes" ADD "normalUserID" TEXT NOT NULL default '';
|
||||
|
||||
UPDATE "config" SET value = 7 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
15
databases/_upgrade_private_8.sql
Normal file
15
databases/_upgrade_private_8.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Add primary keys
|
||||
|
||||
ALTER TABLE "userNameLogs" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "categoryVotes" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "sponsorTimes" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "config" ADD PRIMARY KEY ("key"); --!sqlite-ignore
|
||||
ALTER TABLE "ratings" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "tempVipLog" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "votes" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
|
||||
UPDATE "config" SET value = 8 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
9
databases/_upgrade_private_9.sql
Normal file
9
databases/_upgrade_private_9.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Add primary keys
|
||||
|
||||
DROP INDEX IF EXISTS "privateDB_sponsorTimes_v3"; --!sqlite-ignore
|
||||
|
||||
UPDATE "config" SET value = 9 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
25
databases/_upgrade_sponsorTimes_1.sql
Normal file
25
databases/_upgrade_sponsorTimes_1.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add incorrectVotes field */
|
||||
CREATE TABLE "sqlb_temp_table_1" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"incorrectVotes" INTEGER NOT NULL default 1,
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"shadowHidden" INTEGER NOT NULL
|
||||
);
|
||||
INSERT INTO sqlb_temp_table_1 SELECT "videoID","startTime","endTime","votes",'1',"UUID","userID","timeSubmitted","views","category","shadowHidden" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_1 RENAME TO "sponsorTimes";
|
||||
|
||||
/* Add version to config */
|
||||
INSERT INTO config (key, value) VALUES('version', 1);
|
||||
|
||||
COMMIT;
|
||||
30
databases/_upgrade_sponsorTimes_10.sql
Normal file
30
databases/_upgrade_sponsorTimes_10.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add Hidden field */
|
||||
CREATE TABLE "sqlb_temp_table_10" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"service" TEXT NOT NULL DEFAULT 'YouTube',
|
||||
"videoDuration" REAL NOT NULL DEFAULT '0',
|
||||
"hidden" INTEGER NOT NULL DEFAULT '0',
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_10 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service","videoDuration",0,"shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_10 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 10 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
8
databases/_upgrade_sponsorTimes_11.sql
Normal file
8
databases/_upgrade_sponsorTimes_11.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Rename table: noSegments to lockCategories */
|
||||
ALTER TABLE "noSegments" RENAME TO "lockCategories";
|
||||
|
||||
UPDATE "config" SET value = 11 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
31
databases/_upgrade_sponsorTimes_12.sql
Normal file
31
databases/_upgrade_sponsorTimes_12.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add reputation field */
|
||||
CREATE TABLE "sqlb_temp_table_12" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"service" TEXT NOT NULL DEFAULT 'YouTube',
|
||||
"videoDuration" REAL NOT NULL DEFAULT '0',
|
||||
"hidden" INTEGER NOT NULL DEFAULT '0',
|
||||
"reputation" REAL NOT NULL DEFAULT 0,
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_12 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service","videoDuration","hidden",0,"shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_12 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 12 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
17
databases/_upgrade_sponsorTimes_13.sql
Normal file
17
databases/_upgrade_sponsorTimes_13.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add locked field */
|
||||
CREATE TABLE "sqlb_temp_table_13" (
|
||||
"userID" TEXT NOT NULL,
|
||||
"userName" TEXT NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0'
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_13 SELECT "userID", "userName", 0 FROM "userNames";
|
||||
|
||||
DROP TABLE "userNames";
|
||||
ALTER TABLE sqlb_temp_table_13 RENAME TO "userNames";
|
||||
|
||||
UPDATE "config" SET value = 13 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
9
databases/_upgrade_sponsorTimes_14.sql
Normal file
9
databases/_upgrade_sponsorTimes_14.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "shadowBannedUsers" (
|
||||
"userID" TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 14 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
10
databases/_upgrade_sponsorTimes_15.sql
Normal file
10
databases/_upgrade_sponsorTimes_15.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "unlistedVideos" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 15 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
15
databases/_upgrade_sponsorTimes_16.sql
Normal file
15
databases/_upgrade_sponsorTimes_16.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DROP TABLE "unlistedVideos";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "unlistedVideos" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"year" TEXT NOT NULL,
|
||||
"views" TEXT NOT NULL,
|
||||
"channelID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 16 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
19
databases/_upgrade_sponsorTimes_17.sql
Normal file
19
databases/_upgrade_sponsorTimes_17.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add reason field */
|
||||
CREATE TABLE "sqlb_temp_table_17" (
|
||||
"userID" TEXT NOT NULL,
|
||||
"issueTime" INTEGER NOT NULL,
|
||||
"issuerUserID" TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL,
|
||||
"reason" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_17 SELECT "userID","issueTime","issuerUserID","enabled", '' FROM "warnings";
|
||||
|
||||
DROP TABLE warnings;
|
||||
ALTER TABLE sqlb_temp_table_17 RENAME TO "warnings";
|
||||
|
||||
UPDATE "config" SET value = 17 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
9
databases/_upgrade_sponsorTimes_18.sql
Normal file
9
databases/_upgrade_sponsorTimes_18.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add hash field */
|
||||
ALTER TABLE "lockCategories" ADD "hashedVideoID" TEXT NOT NULL default '';
|
||||
UPDATE "lockCategories" SET "hashedVideoID" = sha256("videoID");
|
||||
|
||||
UPDATE "config" SET value = 18 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
32
databases/_upgrade_sponsorTimes_19.sql
Normal file
32
databases/_upgrade_sponsorTimes_19.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add actionType field */
|
||||
CREATE TABLE "sqlb_temp_table_19" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"actionType" TEXT NOT NULL DEFAULT 'skip',
|
||||
"service" TEXT NOT NULL DEFAULT 'YouTube',
|
||||
"videoDuration" REAL NOT NULL DEFAULT '0',
|
||||
"hidden" INTEGER NOT NULL DEFAULT '0',
|
||||
"reputation" REAL NOT NULL DEFAULT 0,
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_19 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category",'skip',"service","videoDuration","hidden","reputation","shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_19 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 19 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
13
databases/_upgrade_sponsorTimes_2.sql
Normal file
13
databases/_upgrade_sponsorTimes_2.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add new table: noSegments */
|
||||
CREATE TABLE "noSegments" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL
|
||||
);
|
||||
|
||||
/* Add version to config */
|
||||
UPDATE "config" SET value = 2 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
8
databases/_upgrade_sponsorTimes_20.sql
Normal file
8
databases/_upgrade_sponsorTimes_20.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add hash field */
|
||||
ALTER TABLE "lockCategories" ADD "reason" TEXT NOT NULL default '';
|
||||
|
||||
UPDATE "config" SET value = 20 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
26
databases/_upgrade_sponsorTimes_21.sql
Normal file
26
databases/_upgrade_sponsorTimes_21.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "archivedSponsorTimes" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL DEFAULT '0',
|
||||
"incorrectVotes" INTEGER NOT NULL DEFAULT 1,
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"service" TEXT NOT NULL DEFAULT 'Youtube',
|
||||
"actionType" TEXT NOT NULL DEFAULT 'skip',
|
||||
"videoDuration" INTEGER NOT NULL DEFAULT '0',
|
||||
"hidden" INTEGER NOT NULL DEFAULT '0',
|
||||
"reputation" REAL NOT NULL DEFAULT '0',
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 21 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
10
databases/_upgrade_sponsorTimes_22.sql
Normal file
10
databases/_upgrade_sponsorTimes_22.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add hash field */
|
||||
ALTER TABLE "sponsorTimes" ADD "userAgent" TEXT NOT NULL default '';
|
||||
|
||||
ALTER TABLE "archivedSponsorTimes" ADD "userAgent" TEXT NOT NULL default '';
|
||||
|
||||
UPDATE "config" SET value = 22 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
12
databases/_upgrade_sponsorTimes_23.sql
Normal file
12
databases/_upgrade_sponsorTimes_23.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DELETE FROM "userNames" WHERE ctid NOT IN ( --!sqlite-ignore
|
||||
SELECT MIN(ctid) FROM "userNames" --!sqlite-ignore
|
||||
GROUP BY "userID" --!sqlite-ignore
|
||||
); --!sqlite-ignore
|
||||
|
||||
ALTER TABLE "userNames" ADD UNIQUE("userID"); --!sqlite-ignore
|
||||
|
||||
UPDATE "config" SET value = 23 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
21
databases/_upgrade_sponsorTimes_24.sql
Normal file
21
databases/_upgrade_sponsorTimes_24.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE "lockCategories" ADD "service" TEXT NOT NULL default 'YouTube';
|
||||
|
||||
UPDATE "lockCategories"
|
||||
SET "service" = "sponsorTimes"."service"
|
||||
FROM "sponsorTimes"
|
||||
WHERE "lockCategories"."videoID" = "sponsorTimes"."videoID";
|
||||
|
||||
ALTER TABLE "unlistedVideos" ADD "service" TEXT NOT NULL default 'YouTube';
|
||||
|
||||
UPDATE "unlistedVideos"
|
||||
SET "service" = "sponsorTimes"."service"
|
||||
FROM "sponsorTimes"
|
||||
WHERE "unlistedVideos"."videoID" = "sponsorTimes"."videoID";
|
||||
|
||||
DROP INDEX IF EXISTS "noSegments_videoID";
|
||||
|
||||
UPDATE "config" SET value = 24 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
13
databases/_upgrade_sponsorTimes_25.sql
Normal file
13
databases/_upgrade_sponsorTimes_25.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "videoInfo" (
|
||||
"videoID" TEXT PRIMARY KEY NOT NULL,
|
||||
"channelID" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"published" REAL NOT NULL,
|
||||
"genreUrl" REAL NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 25 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
18
databases/_upgrade_sponsorTimes_26.sql
Normal file
18
databases/_upgrade_sponsorTimes_26.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "sqlb_temp_table_26" (
|
||||
"videoID" TEXT PRIMARY KEY NOT NULL,
|
||||
"channelID" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"published" REAL NOT NULL,
|
||||
"genreUrl" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_26 SELECT "videoID", "channelID", "title", "published", '' FROM "videoInfo";
|
||||
|
||||
DROP TABLE "videoInfo";
|
||||
ALTER TABLE sqlb_temp_table_26 RENAME TO "videoInfo";
|
||||
|
||||
UPDATE "config" SET value = 26 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
8
databases/_upgrade_sponsorTimes_27.sql
Normal file
8
databases/_upgrade_sponsorTimes_27.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE "sponsorTimes" ADD "description" TEXT NOT NULL default '';
|
||||
ALTER TABLE "archivedSponsorTimes" ADD "description" TEXT NOT NULL default '';
|
||||
|
||||
UPDATE "config" SET value = 27 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
13
databases/_upgrade_sponsorTimes_28.sql
Normal file
13
databases/_upgrade_sponsorTimes_28.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ratings" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"service" TEXT NOT NULL default 'YouTube',
|
||||
"type" INTEGER NOT NULL,
|
||||
"count" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 28 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
21
databases/_upgrade_sponsorTimes_29.sql
Normal file
21
databases/_upgrade_sponsorTimes_29.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "sqlb_temp_table_29" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"actionType" TEXT NOT NULL DEFAULT 'skip',
|
||||
"category" TEXT NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default '',
|
||||
"reason" TEXT NOT NULL default '',
|
||||
"service" TEXT NOT NULL default 'YouTube'
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_29 SELECT "videoID","userID",'skip',"category","hashedVideoID","reason","service" FROM "lockCategories";
|
||||
INSERT INTO sqlb_temp_table_29 SELECT "videoID","userID",'mute',"category","hashedVideoID","reason","service" FROM "lockCategories";
|
||||
|
||||
DROP TABLE "lockCategories";
|
||||
ALTER TABLE sqlb_temp_table_29 RENAME TO "lockCategories";
|
||||
|
||||
UPDATE "config" SET value = 29 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
11
databases/_upgrade_sponsorTimes_3.sql
Normal file
11
databases/_upgrade_sponsorTimes_3.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* hash upgrade test sha256('vid') = '1ff838dc6ca9680d88455341118157d59a055fe6d0e3870f9c002847bebe4663' */
|
||||
/* Add hash field */
|
||||
ALTER TABLE "sponsorTimes" ADD "hashedVideoID" TEXT NOT NULL default '';
|
||||
UPDATE "sponsorTimes" SET "hashedVideoID" = sha256("videoID");
|
||||
|
||||
/* Bump version in config */
|
||||
UPDATE "config" SET value = 3 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
7
databases/_upgrade_sponsorTimes_30.sql
Normal file
7
databases/_upgrade_sponsorTimes_30.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
UPDATE "sponsorTimes" SET "actionType" = 'poi' WHERE "category" = 'poi_highlight';
|
||||
|
||||
UPDATE "config" SET value = 30 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
25
databases/_upgrade_sponsorTimes_31.sql
Normal file
25
databases/_upgrade_sponsorTimes_31.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* START lockCategory migrations
|
||||
no sponsor migrations
|
||||
no selfpromo migrations */
|
||||
|
||||
/* exclusive_access migrations */
|
||||
DELETE FROM "lockCategories" WHERE "category" = 'exclusive_access' AND "actionType" != 'full';
|
||||
/* delete all full locks on categories without full */
|
||||
DELETE FROM "lockCategories" WHERE "actionType" = 'full' AND "category" in ('interaction', 'intro', 'outro', 'preview', 'filler', 'music_offtopic', 'poi_highlight');
|
||||
/* delete all non-skip music_offtopic locks */
|
||||
DELETE FROM "lockCategories" WHERE "category" = 'music_offtopic' AND "actionType" != 'skip';
|
||||
/* convert all poi_highlight to actionType poi */
|
||||
UPDATE "lockCategories" SET "actionType" = 'poi' WHERE "category" = 'poi_highlight' AND "actionType" = 'skip';
|
||||
/* delete all non-skip poi_highlight locks */
|
||||
DELETE FROM "lockCategories" WHERE "category" = 'poi_highlight' AND "actionType" != 'poi';
|
||||
|
||||
/* END lockCategory migrations */
|
||||
|
||||
/* delete all redundant userName entries */
|
||||
DELETE FROM "userNames" WHERE "userName" = "userID" AND "locked" = 0;
|
||||
|
||||
UPDATE "config" SET value = 31 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
19
databases/_upgrade_sponsorTimes_32.sql
Normal file
19
databases/_upgrade_sponsorTimes_32.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Add primary keys
|
||||
|
||||
ALTER TABLE "sponsorTimes" ADD PRIMARY KEY ("UUID"); --!sqlite-ignore
|
||||
ALTER TABLE "vipUsers" ADD PRIMARY KEY ("userID"); --!sqlite-ignore
|
||||
ALTER TABLE "userNames" ADD PRIMARY KEY ("userID"); --!sqlite-ignore
|
||||
ALTER TABLE "categoryVotes" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "lockCategories" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "warnings" ADD PRIMARY KEY ("userID", "issueTime"); --!sqlite-ignore
|
||||
ALTER TABLE "shadowBannedUsers" ADD PRIMARY KEY ("userID"); --!sqlite-ignore
|
||||
ALTER TABLE "unlistedVideos" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
ALTER TABLE "config" ADD PRIMARY KEY ("key"); --!sqlite-ignore
|
||||
ALTER TABLE "archivedSponsorTimes" ADD PRIMARY KEY ("UUID"); --!sqlite-ignore
|
||||
ALTER TABLE "ratings" ADD "id" SERIAL PRIMARY KEY; --!sqlite-ignore
|
||||
|
||||
UPDATE "config" SET value = 32 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
12
databases/_upgrade_sponsorTimes_4.sql
Normal file
12
databases/_upgrade_sponsorTimes_4.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Create warnings table */
|
||||
CREATE TABLE "warnings" (
|
||||
"userID" TEXT NOT NULL,
|
||||
"issueTime" INTEGER NOT NULL,
|
||||
"issuerUserID" TEXT NOT NULL
|
||||
);
|
||||
|
||||
UPDATE "config" SET value = 4 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
17
databases/_upgrade_sponsorTimes_5.sql
Normal file
17
databases/_upgrade_sponsorTimes_5.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add enabled field */
|
||||
CREATE TABLE "sqlb_temp_table_5" (
|
||||
"userID" TEXT NOT NULL,
|
||||
"issueTime" INTEGER NOT NULL,
|
||||
"issuerUserID" TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL
|
||||
);
|
||||
INSERT INTO sqlb_temp_table_5 SELECT "userID","issueTime","issuerUserID",1 FROM "warnings";
|
||||
|
||||
DROP TABLE warnings;
|
||||
ALTER TABLE sqlb_temp_table_5 RENAME TO "warnings";;
|
||||
|
||||
UPDATE "config" SET value = 5 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
27
databases/_upgrade_sponsorTimes_6.sql
Normal file
27
databases/_upgrade_sponsorTimes_6.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add new voting field */
|
||||
CREATE TABLE "sqlb_temp_table_6" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_6 SELECT "videoID","startTime","endTime","votes",'0',"incorrectVotes","UUID","userID","timeSubmitted","views","category","shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_6 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 6 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
28
databases/_upgrade_sponsorTimes_7.sql
Normal file
28
databases/_upgrade_sponsorTimes_7.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add Service field */
|
||||
CREATE TABLE "sqlb_temp_table_7" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"service" TEXT NOT NULL DEFAULT 'YouTube',
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_7 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category",'YouTube', "shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_7 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 7 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
29
databases/_upgrade_sponsorTimes_8.sql
Normal file
29
databases/_upgrade_sponsorTimes_8.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add Service field */
|
||||
CREATE TABLE "sqlb_temp_table_8" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"service" TEXT NOT NULL DEFAULT 'YouTube',
|
||||
"videoDuration" INTEGER NOT NULL DEFAULT '0',
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_8 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service",'0', "shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_8 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 8 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
29
databases/_upgrade_sponsorTimes_9.sql
Normal file
29
databases/_upgrade_sponsorTimes_9.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* Add Service field */
|
||||
CREATE TABLE "sqlb_temp_table_9" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"startTime" REAL NOT NULL,
|
||||
"endTime" REAL NOT NULL,
|
||||
"votes" INTEGER NOT NULL,
|
||||
"locked" INTEGER NOT NULL default '0',
|
||||
"incorrectVotes" INTEGER NOT NULL default '1',
|
||||
"UUID" TEXT NOT NULL UNIQUE,
|
||||
"userID" TEXT NOT NULL,
|
||||
"timeSubmitted" INTEGER NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'sponsor',
|
||||
"service" TEXT NOT NULL DEFAULT 'YouTube',
|
||||
"videoDuration" REAL NOT NULL DEFAULT '0',
|
||||
"shadowHidden" INTEGER NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default ''
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_9 SELECT "videoID","startTime","endTime","votes","locked","incorrectVotes","UUID","userID","timeSubmitted","views","category","service",'0', "shadowHidden","hashedVideoID" FROM "sponsorTimes";
|
||||
|
||||
DROP TABLE "sponsorTimes";
|
||||
ALTER TABLE sqlb_temp_table_9 RENAME TO "sponsorTimes";
|
||||
|
||||
UPDATE "config" SET value = 9 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
3
docker/database.env.example
Normal file
3
docker/database.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
POSTGRES_USER=sponsorblock
|
||||
POSTGRES_PASSWORD=<choose-a-password>
|
||||
POSTGRES_DB=sponsorTimes
|
||||
13
docker/docker-compose-ci.yml
Normal file
13
docker/docker-compose-ci.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:alpine
|
||||
environment:
|
||||
- POSTGRES_USER=${PG_USER}
|
||||
- POSTGRES_PASSWORD=${PG_PASS}
|
||||
ports:
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
41
docker/docker-compose.yml
Normal file
41
docker/docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
version: '3'
|
||||
services:
|
||||
database:
|
||||
container_name: database
|
||||
image: postgres:13
|
||||
env_file:
|
||||
- database.env
|
||||
volumes:
|
||||
- database-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
restart: always
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis:6.0
|
||||
command: /usr/local/etc/redis/redis.conf
|
||||
volumes:
|
||||
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
ports:
|
||||
- 32773:6379
|
||||
restart: always
|
||||
newleaf:
|
||||
image: abeltramo/newleaf:latest
|
||||
container_name: newleaf
|
||||
restart: always
|
||||
ports:
|
||||
- 3241:3000
|
||||
volumes:
|
||||
- ./newleaf/configuration.py:/workdir/configuration.py
|
||||
rsync:
|
||||
image: mchangrh/rsync:latest
|
||||
container_name: rsync
|
||||
restart: always
|
||||
ports:
|
||||
- 873:873
|
||||
volumes:
|
||||
- ./rsync/rsyncd.conf:/etc/rsyncd.conf
|
||||
- ./database-export/:/mirror
|
||||
|
||||
volumes:
|
||||
database-data:
|
||||
5
docker/migrate/database.pgload.txt
Normal file
5
docker/migrate/database.pgload.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
load database
|
||||
from ./database.db
|
||||
into postgresql://sponsorblock:pw@127.0.0.1:5432/sponsorTimes
|
||||
|
||||
with quote identifiers;
|
||||
17
docker/newleaf/configuration.py
Normal file
17
docker/newleaf/configuration.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# ==============================
|
||||
# You MUST set these settings.
|
||||
# ==============================
|
||||
|
||||
# A URL that this site can be accessed on. Do not include a trailing slash.
|
||||
website_origin = "http://newleaf:3000"
|
||||
|
||||
|
||||
# ==============================
|
||||
# These settings are optional.
|
||||
# ==============================
|
||||
|
||||
# The address of the interface to bind to.
|
||||
#bind_host = "0.0.0.0"
|
||||
|
||||
# The port to bind to.
|
||||
#bind_port = 3000
|
||||
4
docker/redis/redis.conf
Normal file
4
docker/redis/redis.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
maxmemory-policy allkeys-lru
|
||||
maxmemory 6500mb
|
||||
|
||||
appendonly no
|
||||
15
docker/rsync/rsyncd.conf
Normal file
15
docker/rsync/rsyncd.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
pid file = /var/run/rsyncd.pid
|
||||
lock file = /var/run/rsync.lock
|
||||
log file = /var/log/rsync.log
|
||||
# replace with user accessing the files
|
||||
|
||||
[sponsorblock]
|
||||
use chroot = no
|
||||
max connections = 10
|
||||
# path to mirrored files
|
||||
path = /mirror
|
||||
comment = sponsorblock-database
|
||||
read only = true
|
||||
refuse options = c delete zl
|
||||
# disallow checksumming and compression level to reduce CPU/IO load
|
||||
# disallow deleting files clientside
|
||||
12
entrypoint.sh
Executable file
12
entrypoint.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
echo 'Entrypoint script'
|
||||
cd /usr/src/app
|
||||
|
||||
# blank config, use defaults
|
||||
cat <<EOF > config.json
|
||||
{
|
||||
}
|
||||
EOF
|
||||
|
||||
node dist/src/index.js
|
||||
359
index.js
359
index.js
@@ -1,359 +0,0 @@
|
||||
var express = require('express');
|
||||
var http = require('http');
|
||||
|
||||
// Create a service (the app object is just a callback).
|
||||
var app = express();
|
||||
|
||||
//uuid service
|
||||
var uuidv1 = require('uuid/v1');
|
||||
|
||||
//hashing service
|
||||
var crypto = require('crypto');
|
||||
|
||||
//load database
|
||||
var sqlite3 = require('sqlite3').verbose();
|
||||
var db = new sqlite3.Database('./databases/sponsorTimes.db');
|
||||
|
||||
// Create an HTTP service.
|
||||
http.createServer(app).listen(80);
|
||||
|
||||
//global salt that is added to every ip before hashing to
|
||||
// make it even harder for someone to decode the ip
|
||||
var globalSalt = "49cb0d52-1aec-4b89-85fc-fab2c53062fb";
|
||||
|
||||
//setup CORS correctly
|
||||
app.use(function(req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
next();
|
||||
});
|
||||
|
||||
//add the get function
|
||||
app.get('/api/getVideoSponsorTimes', function (req, res) {
|
||||
let videoID = req.query.videoID;
|
||||
|
||||
let sponsorTimes = [];
|
||||
let votes = []
|
||||
let UUIDs = [];
|
||||
|
||||
db.prepare("SELECT startTime, endTime, votes, UUID FROM sponsorTimes WHERE videoID = ?").all(videoID, function(err, rows) {
|
||||
if (err) console.log(err);
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
//check if votes are above -2
|
||||
if (rows[i].votes < -2) {
|
||||
//too untrustworthy, just ignore it
|
||||
continue;
|
||||
}
|
||||
sponsorTimes.push([]);
|
||||
|
||||
let index = sponsorTimes.length - 1;
|
||||
|
||||
sponsorTimes[index][0] = rows[i].startTime;
|
||||
sponsorTimes[index][1] = rows[i].endTime;
|
||||
|
||||
votes[index] = rows[i].votes;
|
||||
UUIDs[index] = rows[i].UUID;
|
||||
}
|
||||
|
||||
if (sponsorTimes.length == 0) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
organisedData = getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs);
|
||||
sponsorTimes = organisedData.sponsorTimes;
|
||||
UUIDs = organisedData.UUIDs;
|
||||
|
||||
if (sponsorTimes.length == 0) {
|
||||
res.sendStatus(404);
|
||||
} else {
|
||||
//send result
|
||||
res.send({
|
||||
sponsorTimes: sponsorTimes,
|
||||
UUIDs: UUIDs
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//add the post function
|
||||
app.get('/api/postVideoSponsorTimes', function (req, res) {
|
||||
let videoID = req.query.videoID;
|
||||
let startTime = req.query.startTime;
|
||||
let endTime = req.query.endTime;
|
||||
let userID = req.query.userID;
|
||||
|
||||
if (typeof videoID != 'string' || startTime == undefined || endTime == undefined || userID == undefined) {
|
||||
//invalid request
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
//x-forwarded-for if this server is behind a proxy
|
||||
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
|
||||
//hash the ip so no one can get it from the database
|
||||
let hashCreator = crypto.createHash('sha256');
|
||||
let hashedIP = hashCreator.update(ip + globalSalt).digest('hex');
|
||||
|
||||
startTime = parseFloat(startTime);
|
||||
endTime = parseFloat(endTime);
|
||||
|
||||
let UUID = uuidv1();
|
||||
|
||||
//get current time
|
||||
let timeSubmitted = Date.now();
|
||||
|
||||
//check to see if the user has already submitted sponsors for this video
|
||||
db.prepare("SELECT UUID FROM sponsorTimes WHERE userID = ? and videoID = ?").all([userID, videoID], function(err, rows) {
|
||||
if (rows.length >= 4) {
|
||||
//too many sponsors for the same video from the same user
|
||||
res.sendStatus(429);
|
||||
} else {
|
||||
//check if this info has already been submitted first
|
||||
db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID], function(err, row) {
|
||||
if (err) console.log(err);
|
||||
|
||||
if (row == null) {
|
||||
//not a duplicate, execute query
|
||||
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, startTime, endTime, 0, UUID, userID, hashedIP, timeSubmitted);
|
||||
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(409);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//voting endpoint
|
||||
app.get('/api/voteOnSponsorTime', function (req, res) {
|
||||
let UUID = req.query.UUID;
|
||||
let userID = req.query.userID;
|
||||
let type = req.query.type;
|
||||
|
||||
if (UUID == undefined || userID == undefined || type == undefined) {
|
||||
//invalid request
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
|
||||
let incrementAmount = 0;
|
||||
|
||||
//don't use userID for now, and just add the vote
|
||||
if (type == 1) {
|
||||
//upvote
|
||||
incrementAmount = 1;
|
||||
} else if (type == 0) {
|
||||
//downvote
|
||||
incrementAmount = -1;
|
||||
} else {
|
||||
//unrecongnised type of vote
|
||||
req.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare("UPDATE sponsorTimes SET votes = votes + ? WHERE UUID = ?").run(incrementAmount, UUID);
|
||||
|
||||
//added to db
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.get('/database.db', function (req, res) {
|
||||
res.sendFile("./databases/sponsorTimes.db", { root: __dirname });
|
||||
});
|
||||
|
||||
|
||||
//This function will find sponsor times that are contained inside of eachother, called similar sponsor times
|
||||
//Only one similar time will be returned, randomly generated based on the sqrt of votes.
|
||||
//This allows new less voted items to still sometimes appear to give them a chance at getting votes.
|
||||
//Sponsor times with less than -2 votes are already ignored before this function is called
|
||||
function getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs) {
|
||||
//list of sponsors that are contained inside eachother
|
||||
let similarSponsors = [];
|
||||
|
||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||
//see if the start time is located between the start and end time of the other sponsor time.
|
||||
for (let j = 0; j < sponsorTimes.length; j++) {
|
||||
if (sponsorTimes[j][0] > sponsorTimes[i][0] && sponsorTimes[j][0] < sponsorTimes[i][1]) {
|
||||
//sponsor j is contained in sponsor i
|
||||
similarSponsors.push([i, j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let similarSponsorsGroups = [];
|
||||
//once they have been added to a group, they don't need to be dealt with anymore
|
||||
let dealtWithSimilarSponsors = [];
|
||||
|
||||
//create lists of all the similar groups (if 1 and 2 are similar, and 2 and 3 are similar, the group is 1, 2, 3)
|
||||
for (let i = 0; i < similarSponsors.length; i++) {
|
||||
if (dealtWithSimilarSponsors.includes(i)) {
|
||||
//dealt with already
|
||||
continue;
|
||||
}
|
||||
|
||||
//this is the group of indexes that are similar
|
||||
let group = similarSponsors[i];
|
||||
for (let j = 0; j < similarSponsors.length; j++) {
|
||||
if (group.includes(similarSponsors[j][0]) || group.includes(similarSponsors[j][1])) {
|
||||
//this is a similar group
|
||||
group.push(similarSponsors[j][0]);
|
||||
group.push(similarSponsors[j][1]);
|
||||
dealtWithSimilarSponsors.push(j);
|
||||
}
|
||||
}
|
||||
similarSponsorsGroups.push(group);
|
||||
}
|
||||
|
||||
//remove duplicate indexes in group arrays
|
||||
for (let i = 0; i < similarSponsorsGroups.length; i++) {
|
||||
uniqueArray = similarSponsorsGroups[i].filter(function(item, pos, self) {
|
||||
return self.indexOf(item) == pos;
|
||||
});
|
||||
|
||||
similarSponsorsGroups[i] = uniqueArray;
|
||||
}
|
||||
|
||||
let weightedRandomIndexes = getWeightedRandomChoiceForArray(similarSponsorsGroups, votes);
|
||||
|
||||
let finalSponsorTimeIndexes = weightedRandomIndexes.finalChoices;
|
||||
//the sponsor times either chosen to be added to finalSponsorTimeIndexes or chosen not to be added
|
||||
let finalSponsorTimeIndexesDealtWith = weightedRandomIndexes.choicesDealtWith;
|
||||
|
||||
let voteSums = weightedRandomIndexes.weightSums;
|
||||
//convert these into the votes
|
||||
for (let i = 0; i < voteSums.length; i++) {
|
||||
if (voteSums[i] != undefined) {
|
||||
//it should use the sum of votes, since anyone upvoting a similar sponsor is upvoting the existence of that sponsor.
|
||||
votes[finalSponsorTimeIndexes[i]] = voteSums;
|
||||
}
|
||||
}
|
||||
|
||||
//find the indexes never dealt with and add them
|
||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||
if (!finalSponsorTimeIndexesDealtWith.includes(i)) {
|
||||
finalSponsorTimeIndexes.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
//if there are too many indexes, find the best 4
|
||||
if (finalSponsorTimeIndexes.length > 4) {
|
||||
finalSponsorTimeIndexes = getWeightedRandomChoice(finalSponsorTimeIndexes, votes, 4).finalChoices;
|
||||
}
|
||||
|
||||
//convert this to a final array to return
|
||||
let finalSponsorTimes = [];
|
||||
for (let i = 0; i < finalSponsorTimeIndexes.length; i++) {
|
||||
finalSponsorTimes.push(sponsorTimes[finalSponsorTimeIndexes[i]]);
|
||||
}
|
||||
|
||||
//convert this to a final array of UUIDs as well
|
||||
let finalUUIDs = [];
|
||||
for (let i = 0; i < finalSponsorTimeIndexes.length; i++) {
|
||||
finalUUIDs.push(UUIDs[finalSponsorTimeIndexes[i]]);
|
||||
}
|
||||
|
||||
return {
|
||||
sponsorTimes: finalSponsorTimes,
|
||||
UUIDs: finalUUIDs
|
||||
};
|
||||
}
|
||||
|
||||
//gets the getWeightedRandomChoice for each group in an array of groups
|
||||
function getWeightedRandomChoiceForArray(choiceGroups, weights) {
|
||||
let finalChoices = [];
|
||||
//the indexes either chosen to be added to final indexes or chosen not to be added
|
||||
let choicesDealtWith = [];
|
||||
//for each choice group, what are the sums of the weights
|
||||
let weightSums = [];
|
||||
|
||||
for (let i = 0; i < choiceGroups.length; i++) {
|
||||
//find weight sums for this group
|
||||
weightSums.push(0);
|
||||
for (let j = 0; j < choiceGroups[i].length; j++) {
|
||||
//only if it is a positive vote, otherwise it is probably just a sponsor time with slightly wrong time
|
||||
if (weights[choiceGroups[i][j]] > 0) {
|
||||
weightSums[weightSums.length - 1] += weights[choiceGroups[i][j]];
|
||||
}
|
||||
}
|
||||
|
||||
//create a random choice for this group
|
||||
let randomChoice = getWeightedRandomChoice(choiceGroups[i], weights, 1)
|
||||
finalChoices.push(randomChoice.finalChoices);
|
||||
|
||||
for (let j = 0; j < randomChoice.choicesDealtWith.length; j++) {
|
||||
choicesDealtWith.push(randomChoice.choicesDealtWith[j])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
finalChoices: finalChoices,
|
||||
choicesDealtWith: choicesDealtWith,
|
||||
weightSums: weightSums
|
||||
};
|
||||
}
|
||||
|
||||
//gets a weighted random choice from the indexes array based on the weights.
|
||||
//amountOfChoices speicifies the amount of choices to return, 1 or more.
|
||||
//choices are unique
|
||||
function getWeightedRandomChoice(choices, weights, amountOfChoices) {
|
||||
if (amountOfChoices > choices.length) {
|
||||
//not possible, since all choices must be unique
|
||||
return null;
|
||||
}
|
||||
|
||||
let finalChoices = [];
|
||||
let choicesDealtWith = [];
|
||||
|
||||
let sqrtWeightsList = [];
|
||||
//the total of all the weights run through the cutom sqrt function
|
||||
let totalSqrtWeights = 0;
|
||||
for (let j = 0; j < choices.length; j++) {
|
||||
//multiplying by 10 makes around 13 votes the point where it the votes start not mattering as much (10 + 3)
|
||||
//The 3 makes -2 the minimum votes before being ignored completely
|
||||
//https://www.desmos.com/calculator/ljftxolg9j
|
||||
//this can be changed if this system increases in popularity.
|
||||
let sqrtVote = Math.sqrt((weights[choices[j]] + 3) * 10);
|
||||
sqrtWeightsList.push(sqrtVote)
|
||||
totalSqrtWeights += sqrtVote;
|
||||
|
||||
//this index has now been deat with
|
||||
choicesDealtWith.push(choices[j]);
|
||||
}
|
||||
|
||||
//iterate and find amountOfChoices choices
|
||||
let randomNumber = Math.random();
|
||||
//this array will keep adding to this variable each time one sqrt vote has been dealt with
|
||||
//this is the sum of all the sqrtVotes under this index
|
||||
let currentVoteNumber = 0;
|
||||
for (let j = 0; j < sqrtWeightsList.length; j++) {
|
||||
if (randomNumber > currentVoteNumber / totalSqrtWeights && randomNumber < (currentVoteNumber + sqrtWeightsList[j]) / totalSqrtWeights) {
|
||||
//this one was randomly generated
|
||||
finalChoices.push(choices[j]);
|
||||
//remove that from original array, for next recursion pass if it happens
|
||||
choices.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
//add on to the count
|
||||
currentVoteNumber += sqrtWeightsList[j];
|
||||
}
|
||||
|
||||
//add on the other choices as well using recursion
|
||||
if (amountOfChoices > 1) {
|
||||
let otherChoices = getWeightedRandomChoice(choices, weights, amountOfChoices - 1).finalChoices;
|
||||
//add all these choices to the finalChoices array being returned
|
||||
for (let i = 0; i < otherChoices.length; i++) {
|
||||
finalChoices.push(otherChoices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
finalChoices: finalChoices,
|
||||
choicesDealtWith: choicesDealtWith
|
||||
};
|
||||
}
|
||||
11
nginx/cors.conf
Normal file
11
nginx/cors.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE';
|
||||
add_header 'Access-Control-Allow-Headers' 'Content-Type';
|
||||
# cache CORS for 24 hours
|
||||
add_header 'Access-Control-Max-Age' 86400;
|
||||
# return empty response for preflight
|
||||
add_header 'Content-Type' 'text/plain; charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
7
nginx/error.conf
Normal file
7
nginx/error.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 421 422 423 424 425 426 428 429 431 451 500 501 502 503 504 505 506 507 508 510 511 /error.html;
|
||||
|
||||
location = /error.html {
|
||||
ssi on;
|
||||
internal;
|
||||
root /etc/nginx/error;
|
||||
}
|
||||
1
nginx/error/error.html
Normal file
1
nginx/error/error.html
Normal file
@@ -0,0 +1 @@
|
||||
<!--# echo var="status"--> <!--# echo var="status_text"--> https://status.sponsor.ajay.app
|
||||
15
nginx/error_map.conf
Normal file
15
nginx/error_map.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
map $status $status_text {
|
||||
400 'Bad Request';
|
||||
401 'Unauthorized';
|
||||
403 'Forbidden';
|
||||
404 'Not Found';
|
||||
405 'Method Not Allowed';
|
||||
408 'Request Timeout';
|
||||
409 'Conflict';
|
||||
429 'Too Many Requests';
|
||||
500 'Internal Server Error';
|
||||
502 'Bad Gateway';
|
||||
503 'Service Unavailable';
|
||||
504 'Gateway Timeout';
|
||||
505 'HTTP Version Not Supported';
|
||||
}
|
||||
317
nginx/nginx.conf
Normal file
317
nginx/nginx.conf
Normal file
@@ -0,0 +1,317 @@
|
||||
worker_processes 2;
|
||||
worker_rlimit_nofile 500000;
|
||||
worker_shutdown_timeout 10;
|
||||
|
||||
events {
|
||||
worker_connections 100000; # Default: 1024
|
||||
#use epoll;
|
||||
#multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format no_ip '$remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
|
||||
|
||||
log_format user_agent '[$time_local] '
|
||||
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
|
||||
|
||||
#limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
|
||||
limit_req_log_level warn;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
include /etc/nginx/proxy.conf;
|
||||
|
||||
# error_map has to be at http level
|
||||
include /etc/nginx/error_map.conf;
|
||||
# Custom MIME definition
|
||||
types {
|
||||
text/csv csv;
|
||||
}
|
||||
# keepalive settings
|
||||
#keepalive_requests 10;
|
||||
keepalive_timeout 10s;
|
||||
http2_idle_timeout 20s; # replaced by keepalive_timeout in 1.19.7
|
||||
|
||||
access_log off;
|
||||
#error_log /etc/nginx/logs/error.log warn;
|
||||
error_log /dev/null crit;
|
||||
|
||||
upstream backend_GET {
|
||||
least_conn;
|
||||
|
||||
#keepalive 5;
|
||||
#server localhost:4441;
|
||||
#server localhost:4442;
|
||||
#server localhost:4443;
|
||||
#server localhost:4444;
|
||||
#server localhost:4445;
|
||||
#server localhost:4446;
|
||||
#server localhost:4447;
|
||||
#server localhost:4448;
|
||||
#server 10.0.0.4:4441 max_fails=25 fail_timeout=20s;
|
||||
|
||||
#server 10.0.0.3:4441 max_fails=25 fail_timeout=20s;
|
||||
#server 10.0.0.3:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.5:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.5:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.6:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.6:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.9:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.9:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.12:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.12:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.10:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.10:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.13:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.13:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.14:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.14:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.11:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.11:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.16:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.16:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
server 10.0.0.17:4441 max_fails=25 fail_timeout=20s;
|
||||
server 10.0.0.17:4442 max_fails=25 fail_timeout=20s;
|
||||
|
||||
#server 134.209.69.251:80 backup;
|
||||
|
||||
#server 116.203.32.253:80 backup;
|
||||
#server 116.203.32.253:80;
|
||||
}
|
||||
upstream backend_POST {
|
||||
#server localhost:4441;
|
||||
#server localhost:4442;
|
||||
server 10.0.0.3:4441 max_fails=25 fail_timeout=15s;
|
||||
server 10.0.0.4:4441 max_fails=25 fail_timeout=15s;
|
||||
#server 10.0.0.3:4442;
|
||||
}
|
||||
upstream backend_db {
|
||||
server 10.0.0.4:4441 max_fails=1 fail_timeout=3s;
|
||||
#server 10.0.0.3:4441;
|
||||
#server 10.0.0.4;
|
||||
}
|
||||
upstream backend_db_dl {
|
||||
server 10.0.0.4;
|
||||
}
|
||||
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHEZONE:10m inactive=60m max_size=400m;
|
||||
proxy_cache_key "$scheme$request_method$host$request_uri";
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
|
||||
server {
|
||||
server_name sponsor.ajay.app api.sponsor.ajay.app;
|
||||
|
||||
include /etc/nginx/error.conf;
|
||||
set_real_ip_from 10.0.0.0/24;
|
||||
real_ip_header proxy_protocol;
|
||||
|
||||
location /news {
|
||||
return 301 https://blog.ajay.app/sponsorblock;
|
||||
}
|
||||
|
||||
location /viewer {
|
||||
return 301 https://sb.ltn.fi;
|
||||
}
|
||||
|
||||
location /test/ {
|
||||
# return 404 "";
|
||||
proxy_pass http://10.0.0.4:4445/;
|
||||
#proxy_pass https://sbtest.etcinit.com/;
|
||||
}
|
||||
|
||||
#access_log /etc/nginx/logs/requests.log no_ip buffer=64k;
|
||||
|
||||
location /api/skipSegments {
|
||||
include /etc/nginx/cors.conf;
|
||||
#return 200 "[]";
|
||||
proxy_pass http://backend_$request_method;
|
||||
#proxy_cache CACHEZONE;
|
||||
#proxy_cache_valid 10s;
|
||||
#limit_req zone=mylimit;
|
||||
|
||||
#access_log /etc/nginx/logs/download.log no_ip;
|
||||
gzip on;
|
||||
if ($request_method = POST) {
|
||||
access_log /etc/nginx/logs/submissions.log user_agent buffer=64k;
|
||||
}
|
||||
|
||||
#proxy_read_timeout 6s;
|
||||
#proxy_next_upstream error timeout http_500 http_502;
|
||||
}
|
||||
|
||||
location /api/getTopUsers {
|
||||
include /etc/nginx/cors.conf;
|
||||
proxy_pass http://backend_GET;
|
||||
proxy_cache CACHEZONE;
|
||||
proxy_cache_valid 20m;
|
||||
}
|
||||
|
||||
location /api/getTotalStats {
|
||||
include /etc/nginx/cors.conf;
|
||||
proxy_pass http://backend_POST;
|
||||
proxy_cache CACHEZONE;
|
||||
proxy_cache_valid 20m;
|
||||
#return 204;
|
||||
}
|
||||
|
||||
location /api/getTopCategoryUsers {
|
||||
include /etc/nginx/cors.conf;
|
||||
proxy_pass http://backend_POST;
|
||||
proxy_cache CACHEZONE;
|
||||
proxy_cache_valid 20m;
|
||||
}
|
||||
|
||||
location /api/getVideoSponsorTimes {
|
||||
include /etc/nginx/cors.conf;
|
||||
proxy_pass http://backend_GET;
|
||||
}
|
||||
|
||||
location /api/isUserVIP {
|
||||
include /etc/nginx/cors.conf;
|
||||
proxy_pass http://backend_GET;
|
||||
}
|
||||
|
||||
location /download/ {
|
||||
#access_log /etc/nginx/logs/download.log no_ip buffer=64k;
|
||||
gzip on;
|
||||
proxy_max_temp_file_size 0;
|
||||
#proxy_cache CACHEZONE;
|
||||
#proxy_cache_valid 20m;
|
||||
#proxy_http_version 1.0;
|
||||
#gzip_types text/csv;
|
||||
#gzip_comp_level 1;
|
||||
#proxy_buffering off;
|
||||
|
||||
|
||||
proxy_pass http://backend_db;
|
||||
#alias /home/sbadmin/sponsor/docker/database-export/;
|
||||
#return 307 https://rsync.sponsor.ajay.app$request_uri;
|
||||
}
|
||||
|
||||
location /database {
|
||||
proxy_pass http://backend_db;
|
||||
#return 200 "Disabled for load reasons";
|
||||
}
|
||||
|
||||
location = /database.db {
|
||||
return 404 "Sqlite database has been replaced with csv exports at https://sponsor.ajay.app/database. Sqlite exports might come back soon, but exported at longer intervals.";
|
||||
#alias /home/sbadmin/sponsor/databases/sponsorTimes.db;
|
||||
#alias /home/sbadmin/test-db/database.db;
|
||||
}
|
||||
|
||||
#location = /database/sponsorTimes.csv {
|
||||
# alias /home/sbadmin/sponsorTimes.csv;
|
||||
#}
|
||||
|
||||
#location /api/voteOnSponsorTime {
|
||||
# return 200 "Success";
|
||||
#}
|
||||
|
||||
#location /api/viewedVideoSponsorTime {
|
||||
# return 200 "Success";
|
||||
#}
|
||||
|
||||
location /api {
|
||||
include /etc/nginx/cors.conf;
|
||||
proxy_pass http://backend_POST;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /home/sbadmin/SponsorBlockSite/public-prod;
|
||||
error_page 404 /404.html;
|
||||
}
|
||||
|
||||
listen [::]:443 default_server ssl http2 ipv6only=on backlog=323999;
|
||||
listen 443 default_server ssl http2 reuseport backlog=3000999; # managed by Certbot
|
||||
listen 4443 default_server ssl http2 proxy_protocol reuseport backlog=3000999;
|
||||
#listen 443 http3 reuseport;
|
||||
#ssl_protocols TLSv1.2 TLSv1.3;
|
||||
listen 8081 proxy_protocol;
|
||||
port_in_redirect off;
|
||||
ssl_certificate /home/sbadmin/certs/cert.pem;
|
||||
ssl_certificate_key /home/sbadmin/certs/key.pem;
|
||||
#ssl_certificate /etc/letsencrypt/live/sponsor.ajay.app-0001/fullchain.pem; # managed by Certbot
|
||||
#ssl_certificate_key /etc/letsencrypt/live/sponsor.ajay.app-0001/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
server_name cdnsponsor.ajay.app;
|
||||
error_page 404 /404.html;
|
||||
|
||||
#location /database/ {
|
||||
# alias /home/sbadmin/sponsor/docker/database-export/;
|
||||
#}
|
||||
|
||||
#location /download/ {
|
||||
# alias /home/sbadmin/sponsor/docker/database-export/;
|
||||
#}
|
||||
|
||||
location / {
|
||||
root /home/sbadmin/SponsorBlockSite/public-prod;
|
||||
}
|
||||
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /home/sbadmin/certs/cert.pem;
|
||||
ssl_certificate_key /home/sbadmin/certs/key.pem;
|
||||
#ssl_certificate /etc/letsencrypt/live/sponsor.ajay.app-0001/fullchain.pem; # managed by Certbot
|
||||
#ssl_certificate_key /etc/letsencrypt/live/sponsor.ajay.app-0001/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
access_log off;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
|
||||
listen [::]:80 ipv6only=on;
|
||||
listen 8080 proxy_protocol;
|
||||
listen 80;
|
||||
server_name sponsor.ajay.app api.sponsor.ajay.app, cdnsponsor.ajay.app, wiki.sponsor.ajay.app;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
server_name wiki.sponsor.ajay.app; # managed by Certbot
|
||||
|
||||
location /.well-known/ {
|
||||
root /home/sbadmin/SponsorBlockSite/public-prod;
|
||||
}
|
||||
|
||||
location ~* ^/index.php/(?<pagename>.*)$ {
|
||||
return 301 /w/$pagename;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://10.0.0.3:8080;
|
||||
}
|
||||
|
||||
port_in_redirect off;
|
||||
listen [::]:443 ssl http2;
|
||||
listen 443 ssl http2; # managed by Certbot
|
||||
listen 8081 proxy_protocol;
|
||||
#listen 443 http3 reuseport;
|
||||
#ssl_protocols TLSv1.2 TLSv1.3;
|
||||
#listen 80;
|
||||
ssl_certificate /home/sbadmin/certs/cert.pem;
|
||||
ssl_certificate_key /home/sbadmin/certs/key.pem;
|
||||
#ssl_certificate /etc/letsencrypt/live/sponsor.ajay.app-0001/fullchain.pem; # managed by Certbot
|
||||
#ssl_certificate_key /etc/letsencrypt/live/sponsor.ajay.app-0001/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
}
|
||||
12
nginx/proxy.conf
Normal file
12
nginx/proxy.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Connection "";
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 128k;
|
||||
proxy_connect_timeout 5s;
|
||||
#proxy_send_timeout 10;
|
||||
proxy_read_timeout 30s;
|
||||
proxy_buffers 32 4k;
|
||||
proxy_http_version 1.1;
|
||||
5
nodemon.json
Normal file
5
nodemon.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts,json",
|
||||
"exec": "(npm test || echo test failed) && npm start"
|
||||
}
|
||||
8573
package-lock.json
generated
8573
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -2,17 +2,52 @@
|
||||
"name": "sponsor_block_server",
|
||||
"version": "0.1.0",
|
||||
"description": "Server that holds the SponsorBlock database",
|
||||
"main": "index.js",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
"test": "npm run tsc && ts-node test/test.ts",
|
||||
"dev": "nodemon",
|
||||
"dev:bash": "nodemon -x 'npm test ; npm start'",
|
||||
"postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:alpine",
|
||||
"redis:docker": "docker run --rm -p 6379:6379 redis:alpine",
|
||||
"start": "ts-node src/index.ts",
|
||||
"tsc": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src test",
|
||||
"lint:fix": "eslint src test --fix"
|
||||
},
|
||||
"author": "Ajay Ramachandran",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0",
|
||||
"better-sqlite3": "^7.4.5",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.17.1",
|
||||
"http": "0.0.0",
|
||||
"sqlite3": "^4.0.9",
|
||||
"uuid": "^3.3.2"
|
||||
"express-promise-router": "^4.1.1",
|
||||
"express-rate-limit": "^6.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pg": "^8.7.1",
|
||||
"rate-limit-redis": "^3.0.1",
|
||||
"redis": "^4.0.6",
|
||||
"sync-mysql": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.4.1",
|
||||
"@types/cron": "^1.7.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.11.11",
|
||||
"@types/pg": "^8.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||
"@typescript-eslint/parser": "^5.5.0",
|
||||
"eslint": "^8.3.0",
|
||||
"mocha": "^9.1.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"sinon": "^12.0.1",
|
||||
"ts-mock-imports": "^1.3.8",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
}
|
||||
|
||||
215
src/app.ts
Normal file
215
src/app.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import express, { Request, RequestHandler, Response, Router } from "express";
|
||||
import { config } from "./config";
|
||||
import { oldSubmitSponsorTimes } from "./routes/oldSubmitSponsorTimes";
|
||||
import { oldGetVideoSponsorTimes } from "./routes/oldGetVideoSponsorTimes";
|
||||
import { postSegmentShift } from "./routes/postSegmentShift";
|
||||
import { postWarning } from "./routes/postWarning";
|
||||
import { getIsUserVIP } from "./routes/getIsUserVIP";
|
||||
import { deleteLockCategoriesEndpoint } from "./routes/deleteLockCategories";
|
||||
import { postLockCategories } from "./routes/postLockCategories";
|
||||
import { endpoint as getUserInfo } from "./routes/getUserInfo";
|
||||
import { getDaysSavedFormatted } from "./routes/getDaysSavedFormatted";
|
||||
import { getTotalStats } from "./routes/getTotalStats";
|
||||
import { getTopUsers } from "./routes/getTopUsers";
|
||||
import { getViewsForUser } from "./routes/getViewsForUser";
|
||||
import { getSavedTimeForUser } from "./routes/getSavedTimeForUser";
|
||||
import { addUserAsVIP } from "./routes/addUserAsVIP";
|
||||
import { shadowBanUser } from "./routes/shadowBanUser";
|
||||
import { getUsername } from "./routes/getUsername";
|
||||
import { setUsername } from "./routes/setUsername";
|
||||
import { viewedVideoSponsorTime } from "./routes/viewedVideoSponsorTime";
|
||||
import { voteOnSponsorTime, getUserID as voteGetUserID } from "./routes/voteOnSponsorTime";
|
||||
import { getSkipSegmentsByHash } from "./routes/getSkipSegmentsByHash";
|
||||
import { postSkipSegments } from "./routes/postSkipSegments";
|
||||
import { endpoint as getSkipSegments } from "./routes/getSkipSegments";
|
||||
import { userCounter } from "./middleware/userCounter";
|
||||
import { loggerMiddleware } from "./middleware/logger";
|
||||
import { corsMiddleware } from "./middleware/cors";
|
||||
import { apiCspMiddleware } from "./middleware/apiCsp";
|
||||
import { rateLimitMiddleware } from "./middleware/requestRateLimit";
|
||||
import dumpDatabase, { appExportPath, downloadFile } from "./routes/dumpDatabase";
|
||||
import { endpoint as getSegmentInfo } from "./routes/getSegmentInfo";
|
||||
import { postClearCache } from "./routes/postClearCache";
|
||||
import { addUnlistedVideo } from "./routes/addUnlistedVideo";
|
||||
import { postPurgeAllSegments } from "./routes/postPurgeAllSegments";
|
||||
import { getUserID } from "./routes/getUserID";
|
||||
import { getLockCategories } from "./routes/getLockCategories";
|
||||
import { getLockCategoriesByHash } from "./routes/getLockCategoriesByHash";
|
||||
import { endpoint as getSearchSegments } from "./routes/getSearchSegments";
|
||||
import { getStatus } from "./routes/getStatus";
|
||||
import { getLockReason } from "./routes/getLockReason";
|
||||
import { getUserStats } from "./routes/getUserStats";
|
||||
import ExpressPromiseRouter from "express-promise-router";
|
||||
import { Server } from "http";
|
||||
import { youtubeApiProxy } from "./routes/youtubeApiProxy";
|
||||
import { getChapterNames } from "./routes/getChapterNames";
|
||||
import { postRating } from "./routes/ratings/postRating";
|
||||
import { getRating } from "./routes/ratings/getRating";
|
||||
import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache";
|
||||
import { getTopCategoryUsers } from "./routes/getTopCategoryUsers";
|
||||
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
|
||||
|
||||
export function createServer(callback: () => void): Server {
|
||||
// Create a service (the app object is just a callback).
|
||||
const app = express();
|
||||
|
||||
const router = ExpressPromiseRouter();
|
||||
app.use(router);
|
||||
|
||||
//setup CORS correctly
|
||||
router.use(corsMiddleware);
|
||||
router.use(loggerMiddleware);
|
||||
router.use("/api/", apiCspMiddleware);
|
||||
router.use(express.json());
|
||||
|
||||
if (config.userCounterURL) router.use(userCounter);
|
||||
|
||||
// Setup pretty JSON
|
||||
if (config.mode === "development") app.set("json spaces", 2);
|
||||
|
||||
// Set production mode
|
||||
app.set("env", config.mode || "production");
|
||||
|
||||
setupRoutes(router);
|
||||
|
||||
return app.listen(config.port, callback);
|
||||
}
|
||||
|
||||
function setupRoutes(router: Router) {
|
||||
// Rate limit endpoint lists
|
||||
const voteEndpoints: RequestHandler[] = [voteOnSponsorTime];
|
||||
const viewEndpoints: RequestHandler[] = [viewedVideoSponsorTime];
|
||||
const postRateEndpoints: RequestHandler[] = [postRating];
|
||||
if (config.rateLimit) {
|
||||
if (config.rateLimit.vote) voteEndpoints.unshift(rateLimitMiddleware(config.rateLimit.vote, voteGetUserID));
|
||||
if (config.rateLimit.view) viewEndpoints.unshift(rateLimitMiddleware(config.rateLimit.view));
|
||||
if (config.rateLimit.rate) postRateEndpoints.unshift(rateLimitMiddleware(config.rateLimit.rate));
|
||||
}
|
||||
|
||||
//add the get function
|
||||
router.get("/api/getVideoSponsorTimes", oldGetVideoSponsorTimes);
|
||||
|
||||
//add the oldpost function
|
||||
router.get("/api/postVideoSponsorTimes", oldSubmitSponsorTimes);
|
||||
router.post("/api/postVideoSponsorTimes", oldSubmitSponsorTimes);
|
||||
|
||||
//add the skip segments functions
|
||||
router.get("/api/skipSegments", getSkipSegments);
|
||||
router.post("/api/skipSegments", postSkipSegments);
|
||||
|
||||
// add the privacy protecting skip segments functions
|
||||
router.get("/api/skipSegments/:prefix", getSkipSegmentsByHash);
|
||||
|
||||
//voting endpoint
|
||||
router.get("/api/voteOnSponsorTime", ...voteEndpoints);
|
||||
router.post("/api/voteOnSponsorTime", ...voteEndpoints);
|
||||
|
||||
//Endpoint when a submission is skipped
|
||||
router.get("/api/viewedVideoSponsorTime", ...viewEndpoints);
|
||||
router.post("/api/viewedVideoSponsorTime", ...viewEndpoints);
|
||||
|
||||
//To set your username for the stats view
|
||||
router.post("/api/setUsername", setUsername);
|
||||
|
||||
//get what username this user has
|
||||
router.get("/api/getUsername", getUsername);
|
||||
|
||||
//Endpoint used to hide a certain user's data
|
||||
router.post("/api/shadowBanUser", shadowBanUser);
|
||||
|
||||
//Endpoint used to make a user a VIP user with special privileges
|
||||
router.post("/api/addUserAsVIP", addUserAsVIP);
|
||||
//Endpoint to add a user as a temporary VIP
|
||||
router.post("/api/addUserAsTempVIP", addUserAsTempVIP);
|
||||
|
||||
//Gets all the views added up for one userID
|
||||
//Useful to see how much one user has contributed
|
||||
router.get("/api/getViewsForUser", getViewsForUser);
|
||||
|
||||
//Gets all the saved time added up (views * sponsor length) for one userID
|
||||
//Useful to see how much one user has contributed
|
||||
//In minutes
|
||||
router.get("/api/getSavedTimeForUser", getSavedTimeForUser);
|
||||
|
||||
router.get("/api/getTopUsers", getTopUsers);
|
||||
router.get("/api/getTopCategoryUsers", getTopCategoryUsers);
|
||||
|
||||
//send out totals
|
||||
//send the total submissions, total views and total minutes saved
|
||||
router.get("/api/getTotalStats", getTotalStats);
|
||||
|
||||
router.get("/api/getUserInfo", getUserInfo);
|
||||
router.get("/api/userInfo", getUserInfo);
|
||||
|
||||
//send out a formatted time saved total
|
||||
router.get("/api/getDaysSavedFormatted", getDaysSavedFormatted);
|
||||
|
||||
//submit video to lock categories
|
||||
router.post("/api/noSegments", postLockCategories);
|
||||
router.post("/api/lockCategories", postLockCategories);
|
||||
|
||||
router.delete("/api/noSegments", deleteLockCategoriesEndpoint);
|
||||
router.delete("/api/lockCategories", deleteLockCategoriesEndpoint);
|
||||
|
||||
//get if user is a vip
|
||||
router.get("/api/isUserVIP", getIsUserVIP);
|
||||
|
||||
//sent user a warning
|
||||
router.post("/api/warnUser", postWarning);
|
||||
|
||||
//get if user is a vip
|
||||
router.post("/api/segmentShift", postSegmentShift);
|
||||
|
||||
//get segment info
|
||||
router.get("/api/segmentInfo", getSegmentInfo);
|
||||
|
||||
//clear cache as VIP
|
||||
router.post("/api/clearCache", postClearCache);
|
||||
|
||||
//purge all segments for VIP
|
||||
router.post("/api/purgeAllSegments", postPurgeAllSegments);
|
||||
|
||||
router.post("/api/unlistedVideo", addUnlistedVideo);
|
||||
|
||||
// get userID from username
|
||||
router.get("/api/userID", getUserID);
|
||||
|
||||
// get lock categores from userID
|
||||
router.get("/api/lockCategories", getLockCategories);
|
||||
|
||||
// get privacy protecting lock categories functions
|
||||
router.get("/api/lockCategories/:prefix", getLockCategoriesByHash);
|
||||
|
||||
// get all segments that match a search
|
||||
router.get("/api/searchSegments", getSearchSegments);
|
||||
|
||||
// autocomplete chapter names
|
||||
router.get("/api/chapterNames", getChapterNames);
|
||||
|
||||
// get status
|
||||
router.get("/api/status/:value", getStatus);
|
||||
router.get("/api/status", getStatus);
|
||||
|
||||
router.get("/api/youtubeApiProxy", youtubeApiProxy);
|
||||
// get user category stats
|
||||
router.get("/api/userStats", getUserStats);
|
||||
|
||||
router.get("/api/lockReason", getLockReason);
|
||||
|
||||
// ratings
|
||||
router.get("/api/ratings/rate/:prefix", getRating);
|
||||
router.get("/api/ratings/rate", getRating);
|
||||
router.post("/api/ratings/rate", postRateEndpoints);
|
||||
router.post("/api/ratings/clearCache", ratingPostClearCache);
|
||||
|
||||
if (config.postgres?.enabled) {
|
||||
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
||||
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
||||
router.get("/database/*", downloadFile);
|
||||
router.use("/download", express.static(appExportPath));
|
||||
} else {
|
||||
router.get("/database.db", function (req: Request, res: Response) {
|
||||
res.sendFile("./databases/sponsorTimes.db", { root: "./" });
|
||||
});
|
||||
}
|
||||
}
|
||||
185
src/config.ts
Normal file
185
src/config.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import fs from "fs";
|
||||
import { SBSConfig } from "./types/config.model";
|
||||
import packageJson from "../package.json";
|
||||
import { isBoolean, isNumber } from "lodash";
|
||||
|
||||
const isTestMode = process.env.npm_lifecycle_script === packageJson.scripts.test;
|
||||
const configFile = process.env.TEST_POSTGRES ? "ci.json"
|
||||
: isTestMode ? "test.json"
|
||||
: "config.json";
|
||||
export const config: SBSConfig = JSON.parse(fs.readFileSync(configFile).toString("utf8"));
|
||||
|
||||
addDefaults(config, {
|
||||
port: 8080,
|
||||
behindProxy: "X-Forwarded-For",
|
||||
db: "./databases/sponsorTimes.db",
|
||||
privateDB: "./databases/private.db",
|
||||
createDatabaseIfNotExist: true,
|
||||
schemaFolder: "./databases",
|
||||
dbSchema: "./databases/_sponsorTimes.db.sql",
|
||||
privateDBSchema: "./databases/_private.db.sql",
|
||||
readOnly: false,
|
||||
webhooks: [],
|
||||
categoryList: ["sponsor", "selfpromo", "exclusive_access", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight"],
|
||||
categorySupport: {
|
||||
sponsor: ["skip", "mute", "full"],
|
||||
selfpromo: ["skip", "mute", "full"],
|
||||
exclusive_access: ["full"],
|
||||
interaction: ["skip", "mute"],
|
||||
intro: ["skip", "mute"],
|
||||
outro: ["skip", "mute"],
|
||||
preview: ["skip", "mute"],
|
||||
filler: ["skip", "mute"],
|
||||
music_offtopic: ["skip"],
|
||||
poi_highlight: ["poi"],
|
||||
chapter: ["chapter"]
|
||||
},
|
||||
maxNumberOfActiveWarnings: 1,
|
||||
hoursAfterWarningExpires: 16300000,
|
||||
adminUserID: "",
|
||||
discordCompletelyIncorrectReportWebhookURL: null,
|
||||
discordFirstTimeSubmissionsWebhookURL: null,
|
||||
discordNeuralBlockRejectWebhookURL: null,
|
||||
discordFailedReportChannelWebhookURL: null,
|
||||
discordReportChannelWebhookURL: null,
|
||||
getTopUsersCacheTimeMinutes: 240,
|
||||
globalSalt: null,
|
||||
mode: "",
|
||||
neuralBlockURL: null,
|
||||
proxySubmission: null,
|
||||
rateLimit: {
|
||||
vote: {
|
||||
windowMs: 900000,
|
||||
max: 15,
|
||||
message: "OK",
|
||||
statusCode: 200,
|
||||
},
|
||||
view: {
|
||||
windowMs: 900000,
|
||||
max: 10,
|
||||
statusCode: 200,
|
||||
message: "OK",
|
||||
},
|
||||
rate: {
|
||||
windowMs: 900000,
|
||||
max: 20,
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
}
|
||||
},
|
||||
userCounterURL: null,
|
||||
newLeafURLs: null,
|
||||
maxRewardTimePerSegmentInSeconds: 600,
|
||||
poiMinimumStartTime: 2,
|
||||
postgres: {
|
||||
enabled: false,
|
||||
user: "",
|
||||
host: "",
|
||||
password: "",
|
||||
port: 5432,
|
||||
max: 150,
|
||||
min: 10
|
||||
},
|
||||
dumpDatabase: {
|
||||
enabled: false,
|
||||
minTimeBetweenMs: 180000,
|
||||
appExportPath: "./docker/database-export",
|
||||
tables: [{
|
||||
name: "sponsorTimes",
|
||||
order: "timeSubmitted"
|
||||
},
|
||||
{
|
||||
name: "userNames"
|
||||
},
|
||||
{
|
||||
name: "categoryVotes"
|
||||
},
|
||||
{
|
||||
name: "lockCategories",
|
||||
},
|
||||
{
|
||||
name: "warnings",
|
||||
order: "issueTime"
|
||||
},
|
||||
{
|
||||
name: "vipUsers"
|
||||
},
|
||||
{
|
||||
name: "unlistedVideos"
|
||||
},
|
||||
{
|
||||
name: "videoInfo"
|
||||
},
|
||||
{
|
||||
name: "ratings"
|
||||
}]
|
||||
},
|
||||
diskCacheURL: null,
|
||||
crons: null,
|
||||
redis: {
|
||||
enabled: false,
|
||||
socket: {
|
||||
host: "",
|
||||
port: 0
|
||||
},
|
||||
disableOfflineQueue: true
|
||||
}
|
||||
});
|
||||
loadFromEnv(config);
|
||||
migrate(config);
|
||||
|
||||
// Add defaults
|
||||
function addDefaults(config: SBSConfig, defaults: SBSConfig) {
|
||||
for (const key in defaults) {
|
||||
if (!Object.prototype.hasOwnProperty.call(config, key)) {
|
||||
config[key] = defaults[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrate(config: SBSConfig) {
|
||||
// Redis change
|
||||
if (config.redis) {
|
||||
const redisConfig = config.redis as any;
|
||||
if (redisConfig.host || redisConfig.port) {
|
||||
config.redis.socket = {
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port
|
||||
};
|
||||
}
|
||||
|
||||
if (redisConfig.enable_offline_queue !== undefined) {
|
||||
config.disableOfflineQueue = !redisConfig.enable_offline_queue;
|
||||
}
|
||||
|
||||
if (redisConfig.socket?.host && redisConfig.enabled === undefined) {
|
||||
redisConfig.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.postgres && config.postgres.user && config.postgres.enabled === undefined) {
|
||||
config.postgres.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromEnv(config: SBSConfig, prefix = "") {
|
||||
for (const key in config) {
|
||||
const fullKey = (prefix ? `${prefix}_` : "") + key;
|
||||
const data = config[key];
|
||||
|
||||
if (data && typeof data === "object" && !Array.isArray(data)) {
|
||||
loadFromEnv(data, fullKey);
|
||||
} else if (process.env[fullKey]) {
|
||||
const value = process.env[fullKey];
|
||||
if (isNumber(value)) {
|
||||
config[key] = parseInt(value, 10);
|
||||
} else if (value.toLowerCase() === "true" || value.toLowerCase() === "false") {
|
||||
config[key] = value === "true";
|
||||
} else if (key === "newLeafURLs") {
|
||||
config[key] = [value];
|
||||
} else {
|
||||
config[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/cronjob/downvoteSegmentArchiveJob.ts
Normal file
67
src/cronjob/downvoteSegmentArchiveJob.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { CronJob } from "cron";
|
||||
|
||||
import { config as serverConfig } from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { db } from "../databases/databases";
|
||||
import { DBSegment } from "../types/segments.model";
|
||||
|
||||
const jobConfig = serverConfig?.crons?.downvoteSegmentArchive;
|
||||
|
||||
export const archiveDownvoteSegment = async (dayLimit: number, voteLimit: number, runTime?: number): Promise<number> => {
|
||||
const timeNow = runTime || new Date().getTime();
|
||||
const threshold = dayLimit * 86400000;
|
||||
|
||||
Logger.info(`DownvoteSegmentArchiveJob starts at ${timeNow}`);
|
||||
try {
|
||||
// insert into archive sponsorTime
|
||||
await db.prepare(
|
||||
"run",
|
||||
`INSERT INTO "archivedSponsorTimes"
|
||||
SELECT *
|
||||
FROM "sponsorTimes"
|
||||
WHERE "votes" < ? AND (? - "timeSubmitted") > ?`,
|
||||
[
|
||||
voteLimit,
|
||||
timeNow,
|
||||
threshold
|
||||
]
|
||||
) as DBSegment[];
|
||||
|
||||
} catch (err) {
|
||||
Logger.error("Execption when insert segment in archivedSponsorTimes");
|
||||
Logger.error(err as string);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// remove from sponsorTime
|
||||
try {
|
||||
await db.prepare(
|
||||
"run",
|
||||
'DELETE FROM "sponsorTimes" WHERE "votes" < ? AND (? - "timeSubmitted") > ?',
|
||||
[
|
||||
voteLimit,
|
||||
timeNow,
|
||||
threshold
|
||||
]
|
||||
) as DBSegment[];
|
||||
|
||||
} catch (err) {
|
||||
Logger.error("Execption when deleting segment in sponsorTimes");
|
||||
Logger.error(err as string);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Logger.info("DownvoteSegmentArchiveJob finished");
|
||||
return 0;
|
||||
};
|
||||
|
||||
const DownvoteSegmentArchiveJob = new CronJob(
|
||||
jobConfig?.schedule || "0 0 * * * 0",
|
||||
() => archiveDownvoteSegment(jobConfig?.timeThresholdInDays, jobConfig?.voteThreshold)
|
||||
);
|
||||
|
||||
if (serverConfig?.crons?.enabled && jobConfig && !jobConfig.schedule) {
|
||||
Logger.error("Invalid cron schedule for downvoteSegmentArchive");
|
||||
}
|
||||
|
||||
export default DownvoteSegmentArchiveJob;
|
||||
13
src/cronjob/index.ts
Normal file
13
src/cronjob/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Logger } from "../utils/logger";
|
||||
import { config } from "../config";
|
||||
import DownvoteSegmentArchiveJob from "./downvoteSegmentArchiveJob";
|
||||
|
||||
export function startAllCrons (): void {
|
||||
if (config?.crons?.enabled) {
|
||||
Logger.info("Crons started");
|
||||
|
||||
DownvoteSegmentArchiveJob.start();
|
||||
} else {
|
||||
Logger.info("Crons dissabled");
|
||||
}
|
||||
}
|
||||
7
src/databases/IDatabase.ts
Normal file
7
src/databases/IDatabase.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface IDatabase {
|
||||
init(): Promise<void>;
|
||||
|
||||
prepare(type: QueryType, query: string, params?: any[]): Promise<any | any[] | void>;
|
||||
}
|
||||
|
||||
export type QueryType = "get" | "all" | "run";
|
||||
35
src/databases/Mysql.ts
Normal file
35
src/databases/Mysql.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Logger } from "../utils/logger";
|
||||
import { IDatabase, QueryType } from "./IDatabase";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import MysqlInterface from "sync-mysql";
|
||||
|
||||
export class Mysql implements IDatabase {
|
||||
private connection: any;
|
||||
|
||||
constructor(private config: unknown) {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async init(): Promise<void> {
|
||||
this.connection = new MysqlInterface(this.config);
|
||||
}
|
||||
|
||||
prepare(type: QueryType, query: string, params?: any[]): Promise<any[]> {
|
||||
Logger.debug(`prepare (mysql): type: ${type}, query: ${query}, params: ${params}`);
|
||||
const queryResult = this.connection.query(query, params);
|
||||
|
||||
switch (type) {
|
||||
case "get": {
|
||||
return queryResult[0];
|
||||
}
|
||||
case "all": {
|
||||
return queryResult;
|
||||
}
|
||||
case "run": {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
137
src/databases/Postgres.ts
Normal file
137
src/databases/Postgres.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Logger } from "../utils/logger";
|
||||
import { IDatabase, QueryType } from "./IDatabase";
|
||||
import { Client, Pool, PoolClient, types } from "pg";
|
||||
|
||||
import fs from "fs";
|
||||
|
||||
// return numeric (pg_type oid=1700) as float
|
||||
types.setTypeParser(1700, function(val) {
|
||||
return parseFloat(val);
|
||||
});
|
||||
|
||||
// return int8 (pg_type oid=20) as int
|
||||
types.setTypeParser(20, function(val) {
|
||||
return parseInt(val, 10);
|
||||
});
|
||||
|
||||
export class Postgres implements IDatabase {
|
||||
private pool: Pool;
|
||||
|
||||
constructor(private config: Record<string, any>) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.pool = new Pool(this.config.postgres);
|
||||
|
||||
if (!this.config.readOnly) {
|
||||
if (this.config.createDbIfNotExists) {
|
||||
await this.createDB();
|
||||
}
|
||||
|
||||
if (this.config.createDbIfNotExists && !this.config.readOnly && fs.existsSync(this.config.dbSchemaFileName)) {
|
||||
await this.pool.query(this.processUpgradeQuery(fs.readFileSync(this.config.dbSchemaFileName).toString()));
|
||||
}
|
||||
|
||||
// Upgrade database if required
|
||||
await this.upgradeDB(this.config.fileNamePrefix, this.config.dbSchemaFolder);
|
||||
|
||||
try {
|
||||
await this.applyIndexes(this.config.fileNamePrefix, this.config.dbSchemaFolder);
|
||||
} catch (e) {
|
||||
Logger.warn("Applying indexes failed. See https://github.com/ajayyy/SponsorBlockServer/wiki/Postgres-Extensions for more information.");
|
||||
Logger.warn(e as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async prepare(type: QueryType, query: string, params?: any[]): Promise<any[]> {
|
||||
// Convert query to use numbered parameters
|
||||
let count = 1;
|
||||
for (let char = 0; char < query.length; char++) {
|
||||
if (query.charAt(char) === "?") {
|
||||
query = `${query.slice(0, char)}$${count}${query.slice(char + 1)}`;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug(`prepare (postgres): type: ${type}, query: ${query}, params: ${params}`);
|
||||
|
||||
let client: PoolClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
const queryResult = await client.query({ text: query, values: params });
|
||||
|
||||
switch (type) {
|
||||
case "get": {
|
||||
const value = queryResult.rows[0];
|
||||
Logger.debug(`result (postgres): ${JSON.stringify(value)}`);
|
||||
return value;
|
||||
}
|
||||
case "all": {
|
||||
const values = queryResult.rows;
|
||||
Logger.debug(`result (postgres): ${JSON.stringify(values)}`);
|
||||
return values;
|
||||
}
|
||||
case "run": {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`prepare (postgres): ${err}`);
|
||||
} finally {
|
||||
client?.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async createDB() {
|
||||
const client = new Client({
|
||||
...this.config.postgres,
|
||||
database: "postgres"
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
if ((await client.query(`SELECT * FROM pg_database WHERE datname = '${this.config.postgres.database}'`)).rowCount == 0) {
|
||||
await client.query(`CREATE DATABASE "${this.config.postgres.database}"
|
||||
WITH
|
||||
OWNER = ${this.config.postgres.user}
|
||||
CONNECTION LIMIT = -1;`
|
||||
);
|
||||
}
|
||||
|
||||
client.end();
|
||||
}
|
||||
|
||||
private async upgradeDB(fileNamePrefix: string, schemaFolder: string) {
|
||||
const versionCodeInfo = await this.pool.query("SELECT value FROM config WHERE key = 'version'");
|
||||
let versionCode = versionCodeInfo.rows[0] ? versionCodeInfo.rows[0].value : 0;
|
||||
|
||||
let path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
while (fs.existsSync(path)) {
|
||||
Logger.debug(`db update: updating ${path}`);
|
||||
await this.pool.query(this.processUpgradeQuery(fs.readFileSync(path).toString()));
|
||||
|
||||
versionCode = (await this.pool.query("SELECT value FROM config WHERE key = 'version'"))?.rows[0]?.value;
|
||||
path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
}
|
||||
Logger.debug(`db update: no file ${path}`);
|
||||
}
|
||||
|
||||
private async applyIndexes(fileNamePrefix: string, schemaFolder: string) {
|
||||
const path = `${schemaFolder}/_${fileNamePrefix}_indexes.sql`;
|
||||
if (fs.existsSync(path)) {
|
||||
await this.pool.query(fs.readFileSync(path).toString());
|
||||
} else {
|
||||
Logger.debug(`failed to apply indexes to ${fileNamePrefix}`);
|
||||
}
|
||||
}
|
||||
|
||||
private processUpgradeQuery(query: string): string {
|
||||
let result = query;
|
||||
result = result.replace(/sha256\((.*?)\)/gm, "encode(digest($1, 'sha256'), 'hex')");
|
||||
result = result.replace(/integer/gmi, "NUMERIC");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
108
src/databases/Sqlite.ts
Normal file
108
src/databases/Sqlite.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { IDatabase, QueryType } from "./IDatabase";
|
||||
import Sqlite3, { Database, Database as SQLiteDatabase } from "better-sqlite3";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { getHash } from "../utils/getHash";
|
||||
import { Logger } from "../utils/logger";
|
||||
|
||||
export class Sqlite implements IDatabase {
|
||||
private db: SQLiteDatabase;
|
||||
|
||||
constructor(private config: SqliteConfig)
|
||||
{
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async prepare(type: QueryType, query: string, params: any[] = []): Promise<any[]> {
|
||||
// Logger.debug(`prepare (sqlite): type: ${type}, query: ${query}, params: ${params}`);
|
||||
const preparedQuery = this.db.prepare(Sqlite.processQuery(query));
|
||||
|
||||
switch (type) {
|
||||
case "get": {
|
||||
return preparedQuery.get(...params);
|
||||
}
|
||||
case "all": {
|
||||
return preparedQuery.all(...params);
|
||||
}
|
||||
case "run": {
|
||||
preparedQuery.run(...params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async init(): Promise<void> {
|
||||
// Make dirs if required
|
||||
if (!fs.existsSync(path.join(this.config.dbPath, "../"))) {
|
||||
fs.mkdirSync(path.join(this.config.dbPath, "../"));
|
||||
}
|
||||
|
||||
this.db = new Sqlite3(this.config.dbPath, { readonly: this.config.readOnly, fileMustExist: !this.config.createDbIfNotExists });
|
||||
|
||||
if (this.config.createDbIfNotExists && !this.config.readOnly && fs.existsSync(this.config.dbSchemaFileName)) {
|
||||
this.db.exec(Sqlite.processUpgradeQuery(fs.readFileSync(this.config.dbSchemaFileName).toString()));
|
||||
}
|
||||
|
||||
if (!this.config.readOnly) {
|
||||
this.db.function("sha256", (str: string) => {
|
||||
return getHash(str, 1);
|
||||
});
|
||||
|
||||
// Upgrade database if required
|
||||
Sqlite.upgradeDB(this.db, this.config.fileNamePrefix, this.config.dbSchemaFolder);
|
||||
}
|
||||
|
||||
this.db.function("regexp", { deterministic: true }, (regex: string, str: string) => {
|
||||
return str.match(regex) ? 1 : 0;
|
||||
});
|
||||
|
||||
// Enable WAL mode checkpoint number
|
||||
if (this.config.enableWalCheckpointNumber) {
|
||||
this.db.exec("PRAGMA journal_mode=WAL;");
|
||||
this.db.exec("PRAGMA wal_autocheckpoint=1;");
|
||||
}
|
||||
|
||||
// Enable Memory-Mapped IO
|
||||
this.db.exec("pragma mmap_size= 500000000;");
|
||||
}
|
||||
|
||||
attachDatabase(database: string, attachAs: string): void {
|
||||
this.db.prepare(`ATTACH ? as ${attachAs}`).run(database);
|
||||
}
|
||||
|
||||
private static processQuery(query: string): string {
|
||||
return query.replace(/ ~\* /g, " REGEXP ");
|
||||
}
|
||||
|
||||
private static upgradeDB(db: Database, fileNamePrefix: string, schemaFolder: string) {
|
||||
const versionCodeInfo = db.prepare("SELECT value FROM config WHERE key = ?").get("version");
|
||||
let versionCode = versionCodeInfo ? versionCodeInfo.value : 0;
|
||||
|
||||
let path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
while (fs.existsSync(path)) {
|
||||
Logger.debug(`db update: updating ${path}`);
|
||||
db.exec(this.processUpgradeQuery(fs.readFileSync(path).toString()));
|
||||
|
||||
versionCode = db.prepare("SELECT value FROM config WHERE key = ?").get("version").value;
|
||||
path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
}
|
||||
Logger.debug(`db update: no file ${path}`);
|
||||
}
|
||||
|
||||
private static processUpgradeQuery(query: string): string {
|
||||
return query.replace(/^.*--!sqlite-ignore/gm, "");
|
||||
}
|
||||
}
|
||||
|
||||
export interface SqliteConfig {
|
||||
dbPath: string;
|
||||
dbSchemaFileName: string;
|
||||
dbSchemaFolder: string;
|
||||
fileNamePrefix: string;
|
||||
readOnly: boolean;
|
||||
createDbIfNotExists: boolean;
|
||||
enableWalCheckpointNumber: boolean
|
||||
}
|
||||
89
src/databases/databases.ts
Normal file
89
src/databases/databases.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { config } from "../config";
|
||||
import { Sqlite } from "./Sqlite";
|
||||
import { Mysql } from "./Mysql";
|
||||
import { Postgres } from "./Postgres";
|
||||
import { IDatabase } from "./IDatabase";
|
||||
|
||||
let db: IDatabase;
|
||||
let privateDB: IDatabase;
|
||||
if (config.mysql) {
|
||||
db = new Mysql(config.mysql);
|
||||
privateDB = new Mysql(config.privateMysql);
|
||||
} else if (config.postgres?.enabled) {
|
||||
db = new Postgres({
|
||||
dbSchemaFileName: config.dbSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: "sponsorTimes",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
postgres: {
|
||||
user: config.postgres?.user,
|
||||
host: config.postgres?.host,
|
||||
database: "sponsorTimes",
|
||||
password: config.postgres?.password,
|
||||
port: config.postgres?.port,
|
||||
max: config.postgres?.max,
|
||||
min: config.postgres?.min,
|
||||
}
|
||||
});
|
||||
|
||||
privateDB = new Postgres({
|
||||
dbSchemaFileName: config.privateDBSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: "private",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
postgres: {
|
||||
user: config.postgres?.user,
|
||||
host: config.postgres?.host,
|
||||
database: "privateDB",
|
||||
password: config.postgres?.password,
|
||||
port: config.postgres?.port,
|
||||
max: config.postgres?.max,
|
||||
min: config.postgres?.min,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
db = new Sqlite({
|
||||
dbPath: config.db,
|
||||
dbSchemaFileName: config.dbSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: "sponsorTimes",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
enableWalCheckpointNumber: !config.readOnly && config.mode === "production"
|
||||
});
|
||||
privateDB = new Sqlite({
|
||||
dbPath: config.privateDB,
|
||||
dbSchemaFileName: config.privateDBSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: "private",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
enableWalCheckpointNumber: false
|
||||
});
|
||||
}
|
||||
async function initDb(): Promise<void> {
|
||||
await db.init();
|
||||
await privateDB.init();
|
||||
|
||||
if (db instanceof Sqlite) {
|
||||
// Attach private db to main db
|
||||
(db as Sqlite).attachDatabase(config.privateDB, "privateDB");
|
||||
}
|
||||
|
||||
if (config.mode === "mirror" && db instanceof Postgres) {
|
||||
const tables = config?.dumpDatabase?.tables ?? [];
|
||||
const tableNames = tables.map(table => table.name);
|
||||
for (const table of tableNames) {
|
||||
const filePath = `${config?.dumpDatabase?.appExportPath}/${table}.csv`;
|
||||
await db.prepare("run", `COPY "${table}" FROM '${filePath}' WITH (FORMAT CSV, HEADER true);`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
db,
|
||||
privateDB,
|
||||
initDb,
|
||||
};
|
||||
30
src/index.ts
Normal file
30
src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { config } from "./config";
|
||||
import { initDb } from "./databases/databases";
|
||||
import { createServer } from "./app";
|
||||
import { Logger } from "./utils/logger";
|
||||
import { startAllCrons } from "./cronjob";
|
||||
import { getCommit } from "./utils/getCommit";
|
||||
|
||||
async function init() {
|
||||
process.on("unhandledRejection", (error: any) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.dir(error?.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
await initDb();
|
||||
// edge case clause for creating compatible .db files, do not enable
|
||||
if (config.mode === "init-db-and-exit") process.exit(0);
|
||||
// do not enable init-db-only mode for usage.
|
||||
(global as any).HEADCOMMIT = config.mode === "development" ? "development"
|
||||
: config.mode === "test" ? "test"
|
||||
: getCommit() as string;
|
||||
createServer(() => {
|
||||
Logger.info(`Server started on port ${config.port}.`);
|
||||
|
||||
// ignite cron job after server created
|
||||
startAllCrons();
|
||||
}).setTimeout(15000);
|
||||
}
|
||||
|
||||
init();
|
||||
6
src/middleware/apiCsp.ts
Normal file
6
src/middleware/apiCsp.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function apiCspMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
res.header("Content-Security-Policy", "script-src 'none'; object-src 'none'");
|
||||
next();
|
||||
}
|
||||
8
src/middleware/cors.ts
Normal file
8
src/middleware/cors.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
next();
|
||||
}
|
||||
7
src/middleware/logger.ts
Normal file
7
src/middleware/logger.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Logger } from "../utils/logger";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function loggerMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
Logger.info(`Request received: ${req.method} ${req.url}`);
|
||||
next();
|
||||
}
|
||||
36
src/middleware/requestRateLimit.ts
Normal file
36
src/middleware/requestRateLimit.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { getIP } from "../utils/getIP";
|
||||
import { getHash } from "../utils/getHash";
|
||||
import { getHashCache } from "../utils/getHashCache";
|
||||
import rateLimit, { RateLimitRequestHandler } from "express-rate-limit";
|
||||
import { RateLimitConfig } from "../types/config.model";
|
||||
import { Request } from "express";
|
||||
import { isUserVIP } from "../utils/isUserVIP";
|
||||
import { UserID } from "../types/user.model";
|
||||
import RedisStore, { RedisReply } from "rate-limit-redis";
|
||||
import redis from "../utils/redis";
|
||||
import { config } from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
|
||||
export function rateLimitMiddleware(limitConfig: RateLimitConfig, getUserID?: (req: Request) => UserID): RateLimitRequestHandler {
|
||||
return rateLimit({
|
||||
windowMs: limitConfig.windowMs,
|
||||
max: limitConfig.max,
|
||||
message: limitConfig.message,
|
||||
statusCode: limitConfig.statusCode,
|
||||
legacyHeaders: false,
|
||||
standardHeaders: false,
|
||||
keyGenerator: (req) => {
|
||||
return getHash(getIP(req), 1);
|
||||
},
|
||||
handler: async (req, res, next) => {
|
||||
if (getUserID === undefined || !await isUserVIP(await getHashCache(getUserID(req)))) {
|
||||
return res.status(limitConfig.statusCode).send(limitConfig.message);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
},
|
||||
store: config.redis?.enabled ? new RedisStore({
|
||||
sendCommand: (...args: string[]) => redis.sendCommand(args).catch((err) => Logger.error(err)) as Promise<RedisReply>,
|
||||
}) : null,
|
||||
});
|
||||
}
|
||||
15
src/middleware/userCounter.ts
Normal file
15
src/middleware/userCounter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import axios from "axios";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { config } from "../config";
|
||||
import { getIP } from "../utils/getIP";
|
||||
import { getHash } from "../utils/getHash";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function userCounter(req: Request, res: Response, next: NextFunction): void {
|
||||
if (req.method !== "OPTIONS") {
|
||||
axios.post(`${config.userCounterURL}/api/v1/addIP?hashedIP=${getHash(getIP(req), 1)}`)
|
||||
.catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
42
src/routes/addUnlistedVideo.ts
Normal file
42
src/routes/addUnlistedVideo.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Request, Response } from "express";
|
||||
import { db } from "../databases/databases";
|
||||
import { getService } from "../utils/getService";
|
||||
import { Logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Optional API method that will be used temporarily to help collect
|
||||
* unlisted videos created before 2017
|
||||
*
|
||||
* https://support.google.com/youtube/answer/9230970
|
||||
*/
|
||||
|
||||
export async function addUnlistedVideo(req: Request, res: Response): Promise<Response> {
|
||||
const {
|
||||
body: {
|
||||
videoID = null,
|
||||
year = 0,
|
||||
views = 0,
|
||||
channelID = "Unknown",
|
||||
service
|
||||
}
|
||||
} = req;
|
||||
|
||||
if (typeof(videoID) !== "string" || videoID.length !== 11) {
|
||||
return res.status(400).send("Invalid parameters");
|
||||
}
|
||||
|
||||
try {
|
||||
const timeSubmitted = Date.now();
|
||||
await db.prepare(
|
||||
"run",
|
||||
`INSERT INTO "unlistedVideos" ("videoID", "year", "views", "channelID", "timeSubmitted", "service") values (?, ?, ?, ?, ?, ?)`,
|
||||
[videoID, year, views, channelID, timeSubmitted, getService(service)]
|
||||
);
|
||||
|
||||
return res.sendStatus(200);
|
||||
} catch (err) {
|
||||
Logger.error(err as string);
|
||||
}
|
||||
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user