mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
568 Commits
docker-ubu
...
072210b605
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
072210b605 | ||
|
|
5b0ec39dcd | ||
|
|
1ec0b38e92 | ||
|
|
3857e3cad2 | ||
|
|
d297ea59f9 | ||
|
|
74ef4a322e | ||
|
|
be1a64ffa2 | ||
|
|
04d1ed5b8d | ||
|
|
d5ad026dc8 | ||
|
|
75757ab65b | ||
|
|
5a7b794179 | ||
|
|
f9011bee7c | ||
|
|
3fa7f5fa1d | ||
|
|
976f120692 | ||
|
|
100e6f8174 | ||
|
|
95e73b99f5 | ||
|
|
e82deb6413 | ||
|
|
8e4a1a090f | ||
|
|
639624aee8 | ||
|
|
9e0cdd09cf | ||
|
|
5369f4e6cc | ||
|
|
97703b4142 | ||
|
|
127ddd88ce | ||
|
|
7b3965f624 | ||
|
|
84b3a6a49b | ||
|
|
ea578c1d70 | ||
|
|
4ee50792b2 | ||
|
|
db25e824b7 | ||
|
|
afd20bc06b | ||
|
|
8e85d84ba3 | ||
|
|
9d54ab7ccf | ||
|
|
1f34b735c1 | ||
|
|
f0ed6b396a | ||
|
|
a018f56e44 | ||
|
|
792b0f9896 | ||
|
|
c227a019f5 | ||
|
|
1adb6fe76c | ||
|
|
6fbf314afd | ||
|
|
3ed2499707 | ||
|
|
55c1151f98 | ||
|
|
58f4313e50 | ||
|
|
20bc0a2632 | ||
|
|
208cd837be | ||
|
|
ea90346eef | ||
|
|
185c1fd0fc | ||
|
|
a032dbe84f | ||
|
|
e89bc10725 | ||
|
|
86464fc646 | ||
|
|
387ff4036f | ||
|
|
1fdc4ed983 | ||
|
|
1968ce47d4 | ||
|
|
dfc522a845 | ||
|
|
13e46397e9 | ||
|
|
7f079c56d0 | ||
|
|
e082919cd0 | ||
|
|
a89378b99f | ||
|
|
4dc899439e | ||
|
|
0644b194d0 | ||
|
|
344d959c05 | ||
|
|
cdf82abf3f | ||
|
|
4bf03bfd1a | ||
|
|
75cbe4d5d0 | ||
|
|
6b59446a37 | ||
|
|
4fd25e1e49 | ||
|
|
d30c338189 | ||
|
|
509e996107 | ||
|
|
240e87b453 | ||
|
|
eaefcc5b96 | ||
|
|
85577ac528 | ||
|
|
41050ce923 | ||
|
|
55bc5339f5 | ||
|
|
0e33b2db2b | ||
|
|
1456c25978 | ||
|
|
67c38039b0 | ||
|
|
8f246d905f | ||
|
|
91c2fdc701 | ||
|
|
2c97403027 | ||
|
|
3151200d33 | ||
|
|
c5ed835b09 | ||
|
|
8a588cf858 | ||
|
|
2396c86486 | ||
|
|
2cc2428db2 | ||
|
|
80e83ba817 | ||
|
|
0565cf24a6 | ||
|
|
353c35cd8d | ||
|
|
169a057c37 | ||
|
|
ab6d0f199e | ||
|
|
ae48a4c195 | ||
|
|
241473b99d | ||
|
|
ba98548662 | ||
|
|
72419d7be9 | ||
|
|
50079d2ab7 | ||
|
|
ee21f79fff | ||
|
|
097a3509c1 | ||
|
|
cc0fa03aca | ||
|
|
477cba93cd | ||
|
|
eda3dfcac7 | ||
|
|
188876e383 | ||
|
|
2c70e1367d | ||
|
|
7012524c61 | ||
|
|
cc6dfbf928 | ||
|
|
6ebda81225 | ||
|
|
a50476ac58 | ||
|
|
99c5cf590e | ||
|
|
8ec787c3e3 | ||
|
|
69b5fb50ce | ||
|
|
682c3c98d9 | ||
|
|
5fe2110711 | ||
|
|
3d24b1dc82 | ||
|
|
71086a3bc7 | ||
|
|
9b0cb1a66b | ||
|
|
ace2d83acd | ||
|
|
90f46f0c1c | ||
|
|
609b55754d | ||
|
|
15ca3f27b9 | ||
|
|
3ef8a576b7 | ||
|
|
c807ca2844 | ||
|
|
c823e28a26 | ||
|
|
3170b6aec3 | ||
|
|
57f5d2822a | ||
|
|
9950663326 | ||
|
|
5c8602e1b7 | ||
|
|
d3cb957991 | ||
|
|
098c5a3c25 | ||
|
|
be71a9bd8c | ||
|
|
42c600cea9 | ||
|
|
0427f91cfc | ||
|
|
51552b3092 | ||
|
|
1a7ca0343a | ||
|
|
525e8e04e8 | ||
|
|
5a824cee82 | ||
|
|
13a03a722c | ||
|
|
f7d3835111 | ||
|
|
8212acbac6 | ||
|
|
2a1af69f1f | ||
|
|
1f615a2379 | ||
|
|
f50d3104de | ||
|
|
f23ca61dab | ||
|
|
6eadb37532 | ||
|
|
2c61260e0f | ||
|
|
ba0de7f95c | ||
|
|
3dfdbcb151 | ||
|
|
c207e56855 | ||
|
|
441131e930 | ||
|
|
84c2b2769b | ||
|
|
e145c9c992 | ||
|
|
2adbc0a02c | ||
|
|
fe95f04c18 | ||
|
|
9b3816afce | ||
|
|
07874d9241 | ||
|
|
7447ca038a | ||
|
|
9fa1aab1e5 | ||
|
|
80b41af620 | ||
|
|
ab5d8dc5ca | ||
|
|
4b55c39f39 | ||
|
|
3ca296f195 | ||
|
|
d4fa640f0f | ||
|
|
427eecf214 | ||
|
|
4f54e408a5 | ||
|
|
9e481bbd5f | ||
|
|
78b29a76b8 | ||
|
|
0342d18f76 | ||
|
|
70754c580c | ||
|
|
e58b0b8638 | ||
|
|
df8f8070ca | ||
|
|
0b8ca31594 | ||
|
|
658a76dc1c | ||
|
|
f363ec5db6 | ||
|
|
f36d675abf | ||
|
|
be74377a08 | ||
|
|
808c7e2112 | ||
|
|
d6f39d37b5 | ||
|
|
e573f34cea | ||
|
|
52e32d4f0f | ||
|
|
adb5f2256e | ||
|
|
59bf6ff86d | ||
|
|
5ce2e2a35d | ||
|
|
68fbde8907 | ||
|
|
62bccb3349 | ||
|
|
90d9ac025a | ||
|
|
07903131f9 | ||
|
|
ec3bb3e738 | ||
|
|
18fcf4eb61 | ||
|
|
19f35d6af4 | ||
|
|
3a918b7059 | ||
|
|
7e7da6c0bc | ||
|
|
f9f7204deb | ||
|
|
8827d9f3de | ||
|
|
42bc255d6c | ||
|
|
2df3b9cbfd | ||
|
|
b859d08d86 | ||
|
|
e7325b2dc2 | ||
|
|
21463762ce | ||
|
|
b06f6a81bb | ||
|
|
82c8146032 | ||
|
|
6f13eab550 | ||
|
|
9d2d70b194 | ||
|
|
4e04ceae16 | ||
|
|
5eec5ac082 | ||
|
|
5253ce8793 | ||
|
|
33a99d9c8d | ||
|
|
0e5c78db0d | ||
|
|
9a08fc6140 | ||
|
|
e7b9dfd312 | ||
|
|
1e2922559c | ||
|
|
cfbee6d6f1 | ||
|
|
c75d58efd5 | ||
|
|
efbf395368 | ||
|
|
dab9fc83ba | ||
|
|
e086bbc301 | ||
|
|
0b3a21b383 | ||
|
|
f973426bd2 | ||
|
|
a4c78e3a3d | ||
|
|
50d3bc183b | ||
|
|
5a379a6a2b | ||
|
|
71692f6b13 | ||
|
|
1746b08d4c | ||
|
|
3bc0ec8bb5 | ||
|
|
2df4dc1bfc | ||
|
|
0e190fca2a | ||
|
|
5aea0b7a3d | ||
|
|
d76aaf83f6 | ||
|
|
a996b9f0d2 | ||
|
|
d3b88412c6 | ||
|
|
6cee892e18 | ||
|
|
e2438a236b | ||
|
|
7a4ae052ed | ||
|
|
b65a7b3dd4 | ||
|
|
955c401f0b | ||
|
|
f0a34df7c6 | ||
|
|
e2c68713ba | ||
|
|
24cabc1f02 | ||
|
|
1edcfca6c3 | ||
|
|
e7fa25cf38 | ||
|
|
527b1f1cb9 | ||
|
|
24d8072eb5 | ||
|
|
c81bf980ca | ||
|
|
a91381720f | ||
|
|
edd4a0928c | ||
|
|
770916492e | ||
|
|
6400b807c2 | ||
|
|
3a7e2d9d0f | ||
|
|
ca5381fe0f | ||
|
|
26988bd607 | ||
|
|
bd8d91ebe5 | ||
|
|
27f05dbae3 | ||
|
|
c7bf1d0e27 | ||
|
|
57be0a032e | ||
|
|
6fe4b22efc | ||
|
|
ed492e54c9 | ||
|
|
af2d583924 | ||
|
|
c61d51be76 | ||
|
|
f3a7d198dc | ||
|
|
3c03cd96d9 | ||
|
|
43848792fa | ||
|
|
fb27264d33 | ||
|
|
7593a23c2e | ||
|
|
aedde4b4fc | ||
|
|
cd2a727e23 | ||
|
|
30c7a96540 | ||
|
|
5197a5f1cc | ||
|
|
12e69afa84 | ||
|
|
e720edf9f0 | ||
|
|
3544a2316d | ||
|
|
4b2e5fb636 | ||
|
|
929e01e5eb | ||
|
|
1f2c5a0238 | ||
|
|
9f833d32a2 | ||
|
|
763ce5d28b | ||
|
|
0e15fd7193 | ||
|
|
a9d7f275ba | ||
|
|
b911552c31 | ||
|
|
da17d903e1 | ||
|
|
a4bbc7df3b | ||
|
|
0bdac15ef1 | ||
|
|
07a0ea6d18 | ||
|
|
9c4f903811 | ||
|
|
c1fd8047ea | ||
|
|
77a858effa | ||
|
|
62ad4226d9 | ||
|
|
a2b5484b75 | ||
|
|
c869c84553 | ||
|
|
32b2a02f79 | ||
|
|
cb5651d437 | ||
|
|
105140e674 | ||
|
|
475efc4d9e | ||
|
|
c8a3551402 | ||
|
|
c526457ee0 | ||
|
|
859861fae8 | ||
|
|
c63744fb3a | ||
|
|
bbc5b6d222 | ||
|
|
95c0a4977c | ||
|
|
40eefc2ea3 | ||
|
|
8fb0b17441 | ||
|
|
191f3b3781 | ||
|
|
95342d6d97 | ||
|
|
5c70e71710 | ||
|
|
2d0137db43 | ||
|
|
01b307ddb2 | ||
|
|
9e0d91992d | ||
|
|
4e6b895af3 | ||
|
|
bdaf336712 | ||
|
|
0f7c495595 | ||
|
|
6010d991fb | ||
|
|
e82066b2cd | ||
|
|
970e3834be | ||
|
|
840e12db71 | ||
|
|
54208ce6ce | ||
|
|
c724a8019a | ||
|
|
f20a31ed0f | ||
|
|
6c8b7d0052 | ||
|
|
cebf8c3d36 | ||
|
|
fe06076eba | ||
|
|
9539e78295 | ||
|
|
8bc14a8be8 | ||
|
|
67e13cb23b | ||
|
|
ba438eca02 | ||
|
|
8da050e5b3 | ||
|
|
01e65a9c25 | ||
|
|
cfb28f3d43 | ||
|
|
121f5586a6 | ||
|
|
2a3017972a | ||
|
|
46ffd02b08 | ||
|
|
8c63a78884 | ||
|
|
c382758833 | ||
|
|
9dda608a50 | ||
|
|
d53b1ec742 | ||
|
|
c10b062832 | ||
|
|
61973510f7 | ||
|
|
0161f544aa | ||
|
|
1797772395 | ||
|
|
7d1c5ff5d8 | ||
|
|
f0c9a6122f | ||
|
|
6d881dc812 | ||
|
|
46756a575c | ||
|
|
3edd4ec5a6 | ||
|
|
0cf9f2de7a | ||
|
|
964760a6a8 | ||
|
|
4f26e9ac3a | ||
|
|
bfcc6a0697 | ||
|
|
1d10d36304 | ||
|
|
cc2be46ad8 | ||
|
|
992947fba5 | ||
|
|
2860b45198 | ||
|
|
665bcc04a7 | ||
|
|
c45e0f04be | ||
|
|
2a19e60c85 | ||
|
|
575f7eed4e | ||
|
|
3ba1b05e84 | ||
|
|
52b435b8ae | ||
|
|
20e7ec7c84 | ||
|
|
ac808fcabe | ||
|
|
0efbd11d29 | ||
|
|
b78bb83ec9 | ||
|
|
5a6e17edb6 | ||
|
|
b11a4e006c | ||
|
|
c6ede725e1 | ||
|
|
3795a6564b | ||
|
|
f44be29181 | ||
|
|
b51f45c704 | ||
|
|
4583e3e5d4 | ||
|
|
6d5a108cb6 | ||
|
|
790db77832 | ||
|
|
b1c213f9be | ||
|
|
49ecaee58c | ||
|
|
5e08ca004a | ||
|
|
c5d1b3ffcf | ||
|
|
c64140b605 | ||
|
|
6579f2b59e | ||
|
|
9f5b584593 | ||
|
|
142d708ee3 | ||
|
|
477d2f6672 | ||
|
|
5cf6e1817f | ||
|
|
1d6be1442c | ||
|
|
8c938b635c | ||
|
|
b56eea3b76 | ||
|
|
2aa5d3e91e | ||
|
|
89a16ef555 | ||
|
|
f818ed744b | ||
|
|
2e52ec22e0 | ||
|
|
efdd0dd228 | ||
|
|
48248c7ddf | ||
|
|
49e2458747 | ||
|
|
1f973efe60 | ||
|
|
3847f3e0d3 | ||
|
|
26d3875293 | ||
|
|
55a4e2e1f2 | ||
|
|
f26016d4ec | ||
|
|
cd7adcecdd | ||
|
|
09847f74ae | ||
|
|
8ea78f38ed | ||
|
|
0675ef21c7 | ||
|
|
dfe554d880 | ||
|
|
6f1a40d329 | ||
|
|
9c7416b2eb | ||
|
|
54d8d7844a | ||
|
|
1533bc951b | ||
|
|
31f8827e61 | ||
|
|
5f87356544 | ||
|
|
9c0a77cb6e | ||
|
|
75915c41c7 | ||
|
|
415c97cb09 | ||
|
|
1c6b7815fe | ||
|
|
fc3c179f6a | ||
|
|
f3572d274c | ||
|
|
02447e0285 | ||
|
|
24475386f9 | ||
|
|
55268301f6 | ||
|
|
faa76abbbd | ||
|
|
b827f8f0cc | ||
|
|
b6b61c42d4 | ||
|
|
6af1ce4092 | ||
|
|
303d0015c6 | ||
|
|
56db43da79 | ||
|
|
64b1a9e5c0 | ||
|
|
48f0a700ab | ||
|
|
768798c6b3 | ||
|
|
9d1f93acfb | ||
|
|
077a0d8fdb | ||
|
|
c9359f172e | ||
|
|
d6dc4756a7 | ||
|
|
9bc9b17294 | ||
|
|
80d3580447 | ||
|
|
3f15f3bcaf | ||
|
|
703848e4e5 | ||
|
|
934965720e | ||
|
|
bb4a882d19 | ||
|
|
74315b8c76 | ||
|
|
a9e95c5bb8 | ||
|
|
fe45a889c9 | ||
|
|
e726e991cc | ||
|
|
940267651d | ||
|
|
2dc68139f7 | ||
|
|
301451d021 | ||
|
|
a7f8795e7e | ||
|
|
162094a9b9 | ||
|
|
e843b4c97f | ||
|
|
c784091ad6 | ||
|
|
fb404d3cee | ||
|
|
68c2ee26ff | ||
|
|
742129bf6a | ||
|
|
9a250b5c58 | ||
|
|
86cbfea08f | ||
|
|
19a3ffc118 | ||
|
|
d225e84a03 | ||
|
|
a4a0045475 | ||
|
|
573cca0b2f | ||
|
|
fecefde3ad | ||
|
|
d300a8a3c6 | ||
|
|
c6b7e7bc4c | ||
|
|
0ffd7022d0 | ||
|
|
a0c36bf1a1 | ||
|
|
64b4b5a2b4 | ||
|
|
b1448d95e5 | ||
|
|
a2d1b154a3 | ||
|
|
2ba1dc6333 | ||
|
|
314a5047d6 | ||
|
|
fb6cf8548d | ||
|
|
adbe7f95d5 | ||
|
|
55d4f746c3 | ||
|
|
6d3f5e6c94 | ||
|
|
bec158f65d | ||
|
|
6fa4296edf | ||
|
|
636f7b16a8 | ||
|
|
39abc3efcf | ||
|
|
2edc00c950 | ||
|
|
6fe0cd5649 | ||
|
|
b6de6d08fa | ||
|
|
cddd280206 | ||
|
|
9d1624d569 | ||
|
|
da8c23d3ef | ||
|
|
901e87a681 | ||
|
|
7bfb2976fe | ||
|
|
295781b1f1 | ||
|
|
cbdd1a6253 | ||
|
|
c91e51de15 | ||
|
|
4c6c15d3a3 | ||
|
|
0951e445ac | ||
|
|
e1cb56e8e9 | ||
|
|
05ee48ffb6 | ||
|
|
7f47fb339b | ||
|
|
690cc38899 | ||
|
|
d912e44484 | ||
|
|
93e3dafb03 | ||
|
|
b5ee0d365c | ||
|
|
0dd617b438 | ||
|
|
aca86e0228 | ||
|
|
895c385d6b | ||
|
|
b56c66f705 | ||
|
|
c810d4d878 | ||
|
|
9cf8b87c6e | ||
|
|
53a181e04d | ||
|
|
eee8494c70 | ||
|
|
b50baf6fa7 | ||
|
|
f905d3c321 | ||
|
|
91f5de326d | ||
|
|
dcbd8f0346 | ||
|
|
8a6a578e60 | ||
|
|
01114d9309 | ||
|
|
7f387ce6aa | ||
|
|
523d303766 | ||
|
|
6bd9ddd14c | ||
|
|
f8d4e18fd4 | ||
|
|
56facd320f | ||
|
|
14c9dc482b | ||
|
|
1f47f01fd5 | ||
|
|
09957843ec | ||
|
|
a6ae5d114e | ||
|
|
68c2bc9d3d | ||
|
|
f9f35b27bd | ||
|
|
6a63f7ee1a | ||
|
|
11acd56e1e | ||
|
|
12dd9d45b5 | ||
|
|
d0171d719b | ||
|
|
e298f19534 | ||
|
|
6c875ba667 | ||
|
|
1b4caf4699 | ||
|
|
ca9b1641d8 | ||
|
|
b861e54a51 | ||
|
|
050c40fc19 | ||
|
|
0945a0bbd1 | ||
|
|
9f91fdf221 | ||
|
|
4b89c58c84 | ||
|
|
d0876516a6 | ||
|
|
e8390e3d9d | ||
|
|
306da4ea63 | ||
|
|
5c4c282718 | ||
|
|
bf64d97b72 | ||
|
|
8d8c52e009 | ||
|
|
37107148eb | ||
|
|
29273e2775 | ||
|
|
71d5a64272 | ||
|
|
a2db8ba0fe | ||
|
|
1514952fd1 | ||
|
|
9fc659dc0a | ||
|
|
24659213c2 | ||
|
|
2354749c2f | ||
|
|
5cd3669634 | ||
|
|
2328502c0d | ||
|
|
301d8a6ae3 | ||
|
|
8529fe152c | ||
|
|
8dac9d1806 | ||
|
|
0f6742f11b | ||
|
|
d6aaca9233 | ||
|
|
7333edf6c8 | ||
|
|
5d9cb19bde | ||
|
|
a21dc85d15 | ||
|
|
a57a25133c | ||
|
|
d6f5b87d3f | ||
|
|
2678215e08 | ||
|
|
95203a47d0 | ||
|
|
7f4119febe | ||
|
|
02f758c33d | ||
|
|
554f7c9787 | ||
|
|
d37287541f | ||
|
|
da226df72a | ||
|
|
6199157687 | ||
|
|
d2e1b04326 | ||
|
|
feff8b2461 | ||
|
|
4bff50a5f0 | ||
|
|
6ffa9d1ffd | ||
|
|
5a80b7aafa | ||
|
|
4d00960fcf | ||
|
|
6e8ca9d843 | ||
|
|
9ab15dd5dd | ||
|
|
76e4635338 | ||
|
|
c97b88614f | ||
|
|
8738b13cd1 |
39
.devcontainer/devcontainer.json
Normal file
39
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||||
|
{
|
||||||
|
"name": "Node.js",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/jshint:2": {},
|
||||||
|
"ghcr.io/devcontainers-contrib/features/angular-cli:2": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [4200, 17442],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "npm install && cd backend && npm install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
// Configure properties specific to VS Code.
|
||||||
|
"vscode": {
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"Angular.ng-template",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"waderyan.gitblame",
|
||||||
|
"42Crunch.vscode-openapi",
|
||||||
|
"christian-kohler.npm-intellisense",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"hbenl.vscode-mocha-test-adapter",
|
||||||
|
"DavidAnson.vscode-markdownlint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
18
.github/dependabot.yaml
vendored
Normal file
18
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/.github/workflows"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/backend/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@@ -13,11 +13,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '12'
|
node-version: '16'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
||||||
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
||||||
- name: upload build artifact
|
- name: upload build artifact
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
path: build
|
path: build
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
if: contains(github.ref, '/tags/v')
|
if: contains(github.ref, '/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: create release
|
- name: create release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
- name: download build artifact
|
- name: download build artifact
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
path: ${{runner.temp}}/youtubedl-material
|
path: ${{runner.temp}}/youtubedl-material
|
||||||
|
|||||||
38
.github/workflows/close-issue-if-noresponse.yml
vendored
Normal file
38
.github/workflows/close-issue-if-noresponse.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: No Response
|
||||||
|
|
||||||
|
# Both `issue_comment` and `scheduled` event types are required for this Action
|
||||||
|
# to work properly.
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
schedule:
|
||||||
|
# Schedule for five minutes after the hour, every hour
|
||||||
|
- cron: '5 * * * *'
|
||||||
|
|
||||||
|
# By specifying the access of one of the scopes, all of those that are not
|
||||||
|
# specified are set to 'none'.
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
noResponse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'Tzahi12345/YoutubeDL-Material' }}
|
||||||
|
steps:
|
||||||
|
- uses: lee-dohm/no-response@v0.5.0
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there has been no response
|
||||||
|
to our request for more information from the original author. With only the
|
||||||
|
information that is currently in the issue, we don't have enough information
|
||||||
|
to take action. Please reach out if you have or find the answers we need so
|
||||||
|
that we can investigate further. We will re-open this issue if you provide us
|
||||||
|
with the requested information with a comment under this issue.
|
||||||
|
Thank you for your understanding and for trying to help make this application
|
||||||
|
a better one!
|
||||||
|
# Number of days of inactivity before an issue is closed for lack of response.
|
||||||
|
daysUntilClose: 21
|
||||||
|
# Label requiring a response.
|
||||||
|
responseRequiredLabel: "💬 response-needed"
|
||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -68,4 +68,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
19
.github/workflows/docker-pr.yml
vendored
19
.github/workflows/docker-pr.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
@@ -18,10 +18,21 @@ jobs:
|
|||||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
dir: 'backend/'
|
dir: 'backend/'
|
||||||
- name: Build docker images
|
- name: setup platform emulator
|
||||||
run: docker build . -t tzahi12345/youtubedl-material:nightly-pr
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: setup multi-arch docker build
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: build & push images
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||||
|
#platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
tags: tzahi12345/youtubedl-material:nightly-pr
|
||||||
|
|||||||
57
.github/workflows/docker-release.yml
vendored
57
.github/workflows/docker-release.yml
vendored
@@ -6,40 +6,81 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
description: 'Docker tags'
|
description: 'Docker tags'
|
||||||
required: true
|
required: true
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
|
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
dir: 'backend/'
|
dir: 'backend/'
|
||||||
|
|
||||||
|
- name: Set image tag
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.inputs.tags }}" != "" ]; then
|
||||||
|
echo "::set-output name=tags::${{ github.event.inputs.tags }}"
|
||||||
|
elif [ ${{ github.event.action }} == "release" ]; then
|
||||||
|
echo "::set-output name=tags::${{ github.event.release.tag_name }}"
|
||||||
|
else
|
||||||
|
echo "Unknown workflow trigger: ${{ github.event.action }}! Cannot determine default tag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate Docker image metadata
|
||||||
|
id: docker-meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
||||||
|
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.tags.outputs.tags }}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: setup platform emulator
|
- name: setup platform emulator
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ github.event.inputs.tags }}
|
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||||
|
|||||||
54
.github/workflows/docker.yml
vendored
54
.github/workflows/docker.yml
vendored
@@ -13,44 +13,74 @@ on:
|
|||||||
- '**.pem'
|
- '**.pem'
|
||||||
- '.dockerignore'
|
- '.dockerignore'
|
||||||
- '.gitignore'
|
- '.gitignore'
|
||||||
|
schedule:
|
||||||
|
- cron: '34 4 * * 2'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
|
|
||||||
- name: create-json
|
- name: create-json
|
||||||
id: create-json
|
id: create-json
|
||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@v1.2.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
dir: 'backend/'
|
dir: 'backend/'
|
||||||
|
|
||||||
- name: setup platform emulator
|
- name: setup platform emulator
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Generate Docker image metadata
|
||||||
|
id: docker-meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
# Defaults:
|
||||||
|
# DOCKERHUB_USERNAME : tzahi12345
|
||||||
|
# DOCKERHUB_REPO : youtubedl-material
|
||||||
|
# DOCKERHUB_MASTER_TAG: nightly
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
||||||
|
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
|
||||||
|
tags: |
|
||||||
|
type=raw,${{secrets.DOCKERHUB_MASTER_TAG}}-{{ date 'YYYY-MM-DD' }}
|
||||||
|
type=raw,${{secrets.DOCKERHUB_MASTER_TAG}}
|
||||||
|
type=sha,prefix=sha-,format=short
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
|
||||||
push: true
|
push: true
|
||||||
# Defaults:
|
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||||
# DOCKERHUB_USERNAME : tzahi12345
|
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||||
# DOCKERHUB_REPO : youtubedl-material
|
|
||||||
# DOCKERHUB_MASTER_TAG: nightly
|
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{secrets.DOCKERHUB_MASTER_TAG}}
|
|
||||||
|
|||||||
40
.github/workflows/mocha.yml
vendored
Normal file
40
.github/workflows/mocha.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Tests
|
||||||
|
'on':
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: 'Backend - mocha'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node:
|
||||||
|
- 16
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '${{ matrix.node }}'
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: 'Cache node_modules'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-v${{ matrix.node }}-
|
||||||
|
working-directory: ./backend
|
||||||
|
- uses: FedericoCarboni/setup-ffmpeg@v2
|
||||||
|
id: setup-ffmpeg
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: ./backend
|
||||||
|
- name: Run All Node.js Tests
|
||||||
|
run: npm run test
|
||||||
|
working-directory: ./backend
|
||||||
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"angular.ng-template",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"waderyan.gitblame",
|
||||||
|
"42crunch.vscode-openapi",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"christian-kohler.npm-intellisense",
|
||||||
|
"hbenl.vscode-mocha-test-adapter"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -4,6 +4,20 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Dev: Debug Backend",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run-script",
|
||||||
|
"debug"
|
||||||
|
],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"type": "node",
|
||||||
|
"cwd": "${workspaceFolder}/backend"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mochaExplorer.files": "backend/test/**/*.js",
|
||||||
|
"mochaExplorer.cwd": "backend",
|
||||||
|
"mochaExplorer.globImplementation": "vscode",
|
||||||
|
"mochaExplorer.env": {
|
||||||
|
// "YTDL_MODE": "debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
.vscode/tasks.json
vendored
45
.vscode/tasks.json
vendored
@@ -1,25 +1,60 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
"windows": {
|
||||||
|
"options": {
|
||||||
|
"shell": {
|
||||||
|
"executable": "cmd.exe",
|
||||||
|
"args": [
|
||||||
|
"/d", "/c"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "start",
|
"script": "start",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"label": "Dev: start frontend",
|
"label": "Dev: start frontend",
|
||||||
"detail": "ng serve"
|
"detail": "ng serve",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Dev: start backend",
|
"label": "Dev: start backend",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "set YTDL_MODE=debug && node app.js",
|
"command": "node app.js",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "./backend"
|
"cwd": "./backend",
|
||||||
|
"env": {
|
||||||
|
"YTDL_MODE": "debug"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": [],
|
||||||
|
"dependsOn": ["Dev: post-build"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dev: post-build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "node src/postbuild.mjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dev: run all",
|
||||||
|
"dependsOn": ["Dev: start backend", "Dev: start frontend"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
38
DEVELOPMENT.md
Normal file
38
DEVELOPMENT.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<h1>Development</h1>
|
||||||
|
|
||||||
|
- [First time...](#first-time)
|
||||||
|
- [Setup](#setup)
|
||||||
|
- [Startup](#startup)
|
||||||
|
- [Debugging the backend (VSC)](#debugging-the-backend-vsc)
|
||||||
|
- [Deploy changes](#deploy-changes)
|
||||||
|
- [Frontend](#frontend)
|
||||||
|
- [Backend](#backend)
|
||||||
|
|
||||||
|
# First time...
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Checkout the repository and navigate to the `youtubedl-material` directory.
|
||||||
|
```bash
|
||||||
|
vim ./src/assets/default.json # Edit settings for your local environment. This config file is just the dev config file, if YTDL_MODE is not set to "debug", then ./backend/appdata/default.json will be used
|
||||||
|
npm -g install pm2 # Install pm2
|
||||||
|
npm install # Install dependencies for the frontend
|
||||||
|
cd ./backend
|
||||||
|
npm install # Install dependencies for the backend
|
||||||
|
cd ..
|
||||||
|
npm run build # Build the frontend
|
||||||
|
```
|
||||||
|
This step have to be done only once.
|
||||||
|
|
||||||
|
## Startup
|
||||||
|
Navigate to the `youtubedl-material/backend` directory and run `npm start`.
|
||||||
|
|
||||||
|
# Debugging the backend (VSC)
|
||||||
|
Open the `youtubedl-material` directory in Visual Studio Code and run the launch configuration `Dev: Debug Backend`.
|
||||||
|
|
||||||
|
# Deploy changes
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
Navigate to the `youtubedl-material` directory and run `npm run build`. Restart the backend.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
Simply restart the backend.
|
||||||
150
Dockerfile
150
Dockerfile
@@ -1,75 +1,97 @@
|
|||||||
FROM ubuntu:22.04 AS ffmpeg
|
# Fetching our utils
|
||||||
|
FROM ubuntu:22.04 AS utils
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
# Use script due local build compability
|
||||||
|
COPY docker-utils/*.sh .
|
||||||
|
RUN chmod +x *.sh
|
||||||
|
RUN sh ./ffmpeg-fetch.sh
|
||||||
|
RUN sh ./fetch-twitchdownloader.sh
|
||||||
|
|
||||||
COPY docker-build.sh .
|
|
||||||
RUN sh ./docker-build.sh
|
|
||||||
|
|
||||||
#--------------# Stage 2
|
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
|
||||||
|
# Go to 20.04
|
||||||
FROM ubuntu:22.04 as frontend
|
FROM ubuntu:22.04 AS base
|
||||||
|
ARG TARGETPLATFORM
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt-get update && apt-get -y install \
|
ENV UID=1000
|
||||||
curl \
|
ENV GID=1000
|
||||||
gnupg \
|
ENV USER=youtube
|
||||||
# Ubuntu 22.04 ships Node.JS 12 by default :)
|
ENV NO_UPDATE_NOTIFIER=true
|
||||||
nodejs \
|
|
||||||
# needed on 21.10 and before, maybe not on 22.04 YARN: brings along npm, solves dependency conflicts,
|
|
||||||
# spares us this spaghetti approach: https://stackoverflow.com/a/60547197
|
|
||||||
npm && \
|
|
||||||
apt-get install -f && \
|
|
||||||
npm config set strict-ssl false && \
|
|
||||||
npm install -g @angular/cli
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
COPY [ "package.json", "package-lock.json", "/build/" ]
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
|
||||||
COPY [ "src/", "/build/src/" ]
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
#--------------# Final Stage
|
|
||||||
|
|
||||||
FROM ubuntu:22.04
|
|
||||||
|
|
||||||
ENV UID=1000 \
|
|
||||||
GID=1000 \
|
|
||||||
USER=youtube \
|
|
||||||
NO_UPDATE_NOTIFIER=true
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN groupadd -g $GID $USER && useradd --system -g $USER --uid $UID $USER
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y install \
|
|
||||||
npm \
|
|
||||||
python2 \
|
|
||||||
python3 \
|
|
||||||
gosu \
|
|
||||||
atomicparsley && \
|
|
||||||
apt-get install -f && \
|
|
||||||
apt-get autoremove --purge && \
|
|
||||||
apt-get autoremove && \
|
|
||||||
apt-get clean && \
|
|
||||||
rm -rf /var/lib/apt
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
|
||||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
|
||||||
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
|
||||||
ENV PM2_HOME=/app/pm2
|
ENV PM2_HOME=/app/pm2
|
||||||
|
ENV ALLOW_CONFIG_MUTATIONS=true
|
||||||
|
ENV npm_config_cache=/app/.npm
|
||||||
|
|
||||||
|
# Use NVM to get specific node version
|
||||||
|
ENV NODE_VERSION=16.14.2
|
||||||
|
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
|
||||||
|
apt update && \
|
||||||
|
apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 libatomic1 && \
|
||||||
|
apt clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir /usr/local/nvm
|
||||||
|
ENV PATH="/usr/local/nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
|
||||||
|
ENV NVM_DIR=/usr/local/nvm
|
||||||
|
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
FROM --platform=${BUILDPLATFORM} node:16 as frontend
|
||||||
|
RUN npm install -g @angular/cli
|
||||||
|
WORKDIR /build
|
||||||
|
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
|
||||||
|
COPY [ "src/", "/build/src/" ]
|
||||||
|
RUN npm install && \
|
||||||
|
npm run build && \
|
||||||
|
ls -al /build/backend/public
|
||||||
|
RUN npm uninstall -g @angular/cli
|
||||||
|
RUN rm -rf node_modules
|
||||||
|
|
||||||
|
|
||||||
|
# Install backend deps
|
||||||
|
FROM base as backend
|
||||||
|
WORKDIR /app
|
||||||
|
COPY [ "backend/","/app/" ]
|
||||||
RUN npm config set strict-ssl false && \
|
RUN npm config set strict-ssl false && \
|
||||||
npm install pm2 -g && \
|
npm install --prod && \
|
||||||
npm install && chown -R $UID:$GID ./
|
ls -al
|
||||||
|
|
||||||
# needed for ubuntu, see #596
|
#FROM base as python
|
||||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
# armv7 need build from source
|
||||||
|
#WORKDIR /app
|
||||||
|
#COPY docker-utils/GetTwitchDownloader.py .
|
||||||
|
#RUN apt update && \
|
||||||
|
# apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
|
||||||
|
# apt clean && \
|
||||||
|
# rm -rf /var/lib/apt/lists/*
|
||||||
|
#RUN pip install PyGithub requests
|
||||||
|
#RUN python GetTwitchDownloader.py
|
||||||
|
|
||||||
|
# Final image
|
||||||
|
FROM base
|
||||||
|
RUN npm install -g pm2 && \
|
||||||
|
apt update && \
|
||||||
|
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
|
||||||
|
pip install pycryptodomex && \
|
||||||
|
apt remove -y --purge build-essential && \
|
||||||
|
apt autoremove -y --purge && \
|
||||||
|
apt clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
# User 1000 already exist from base image
|
||||||
|
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
||||||
|
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
||||||
|
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/TwitchDownloaderCLI", "/usr/local/bin/TwitchDownloaderCLI"]
|
||||||
|
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
|
||||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||||
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
#COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
|
||||||
|
RUN chmod +x /app/fix-scripts/*.sh
|
||||||
|
# Add some persistence data
|
||||||
|
#VOLUME ["/app/appdata"]
|
||||||
|
|
||||||
EXPOSE 17442
|
EXPOSE 17442
|
||||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||||
CMD [ "pm2-runtime", "pm2.config.js" ]
|
CMD [ "npm","start" ]
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
FROM tzahi12345/youtubedl-material:nightly
|
FROM tzahi12345/youtubedl-material:latest
|
||||||
CMD [ "pm2-runtime", "pm2.config.js" ]
|
CMD [ "npm", "start" ]
|
||||||
@@ -97,6 +97,11 @@ paths:
|
|||||||
summary: Get all files
|
summary: Get all files
|
||||||
description: Gets all files and playlists stored in the db
|
description: Gets all files and playlists stored in the db
|
||||||
operationId: get-getAllFiles
|
operationId: get-getAllFiles
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GetAllFilesRequest'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: OK
|
description: OK
|
||||||
@@ -106,6 +111,37 @@ paths:
|
|||||||
$ref: '#/components/schemas/GetAllFilesResponse'
|
$ref: '#/components/schemas/GetAllFilesResponse'
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
/api/rss:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- files
|
||||||
|
summary: Generates an RSS feed
|
||||||
|
description: Generates an RSS feed for downloaded files
|
||||||
|
operationId: get-rss
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: params
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/GetAllFilesRequest'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
description: user uid
|
||||||
|
default: null
|
||||||
|
style: form
|
||||||
|
explode: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: RSS feed
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
/api/getFile:
|
/api/getFile:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -129,6 +165,27 @@ paths:
|
|||||||
description: User is not authorized to view the file.
|
description: User is not authorized to view the file.
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
/api/updateFile:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- files
|
||||||
|
summary: Updates file database object
|
||||||
|
description: Updates a file db object using its uid and a change object.
|
||||||
|
operationId: post-updateFile
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdateFileRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
/api/enableSharing:
|
/api/enableSharing:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -236,6 +293,48 @@ paths:
|
|||||||
$ref: '#/components/schemas/UnsubscribeResponse'
|
$ref: '#/components/schemas/UnsubscribeResponse'
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
/api/checkSubscription:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- subscriptions
|
||||||
|
summary: Run a check for videos for a subscription
|
||||||
|
description: Runs a subscription check
|
||||||
|
operationId: post-api-checksubscription
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CheckSubscriptionRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
|
/api/cancelCheckSubscription:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- subscriptions
|
||||||
|
summary: Cancels check for videos for a subscription
|
||||||
|
description: Cancels subscription check
|
||||||
|
operationId: post-api-checksubscription
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CheckSubscriptionRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
/api/deleteSubscriptionFile:
|
/api/deleteSubscriptionFile:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -521,6 +620,69 @@ paths:
|
|||||||
description: If the archive dir is not found, 404 is sent as a response
|
description: If the archive dir is not found, 404 is sent as a response
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
/api/deleteArchiveItems:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- archive
|
||||||
|
summary: Delete item from archive
|
||||||
|
description: 'Deletes an item from the archive'
|
||||||
|
operationId: post-api-deleteArchiveItems
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DeleteArchiveItemsRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
|
/api/importArchive:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- archive
|
||||||
|
summary: Imports archive
|
||||||
|
description: 'Imports an existing archive.txt file'
|
||||||
|
operationId: post-api-importArchive
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ImportArchiveRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
|
/api/uploadCookies:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- downloader
|
||||||
|
summary: Upload cookies
|
||||||
|
description: 'Uploads cookies file to be used during downloading'
|
||||||
|
operationId: post-api-uploadCookies
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UploadCookiesRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
/api/updaterStatus:
|
/api/updaterStatus:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -785,7 +947,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SuccessObject'
|
$ref: '#/components/schemas/RestartDownloadResponse'
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -841,17 +1003,10 @@ paths:
|
|||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
tags:
|
tags:
|
||||||
- downloader
|
- downloader
|
||||||
/api/clearFinishedDownloads:
|
/api/clearDownloads:
|
||||||
post:
|
post:
|
||||||
tags:
|
summary: Clear multiple downloads
|
||||||
- downloader
|
operationId: post-api-clear-downloads
|
||||||
summary: Clear finished downloads
|
|
||||||
operationId: post-api-clear-finished-downloads
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: OK
|
description: OK
|
||||||
@@ -859,8 +1014,17 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SuccessObject'
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ClearDownloadsRequest'
|
||||||
|
description: ''
|
||||||
|
description: "Clears multiple downloads based on a given filter."
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
tags:
|
||||||
|
- downloader
|
||||||
/api/getTask:
|
/api/getTask:
|
||||||
post:
|
post:
|
||||||
summary: Get info for one task
|
summary: Get info for one task
|
||||||
@@ -1507,6 +1671,8 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
FileType:
|
FileType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -1559,6 +1725,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Height of the video, if known
|
description: Height of the video, if known
|
||||||
example: '1080'
|
example: '1080'
|
||||||
|
maxHeight:
|
||||||
|
type: string
|
||||||
|
description: Max height that should be used, useful for playlists. selectedHeight will override this.
|
||||||
|
example: '1080'
|
||||||
maxBitrate:
|
maxBitrate:
|
||||||
type: string
|
type: string
|
||||||
description: Specify ffmpeg/avconv audio quality
|
description: Specify ffmpeg/avconv audio quality
|
||||||
@@ -1567,6 +1737,9 @@ components:
|
|||||||
$ref: '#/components/schemas/FileType'
|
$ref: '#/components/schemas/FileType'
|
||||||
cropFileSettings:
|
cropFileSettings:
|
||||||
$ref: '#/components/schemas/CropFileSettings'
|
$ref: '#/components/schemas/CropFileSettings'
|
||||||
|
ignoreArchive:
|
||||||
|
type: boolean
|
||||||
|
description: If using youtube-dl archive, download will ignore it
|
||||||
DownloadResponse:
|
DownloadResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1591,6 +1764,13 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
download:
|
download:
|
||||||
$ref: '#/components/schemas/Download'
|
$ref: '#/components/schemas/Download'
|
||||||
|
RestartDownloadResponse:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/SuccessObject'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
new_download_uid:
|
||||||
|
type: string
|
||||||
GetAllDownloadsRequest:
|
GetAllDownloadsRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1607,18 +1787,27 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Download'
|
$ref: '#/components/schemas/Download'
|
||||||
|
ClearDownloadsRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
clear_finished:
|
||||||
|
type: boolean
|
||||||
|
clear_paused:
|
||||||
|
type: boolean
|
||||||
|
clear_errors:
|
||||||
|
type: boolean
|
||||||
GetTaskRequest:
|
GetTaskRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
required:
|
required:
|
||||||
- task_key
|
- task_key
|
||||||
UpdateTaskScheduleRequest:
|
UpdateTaskScheduleRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_schedule:
|
new_schedule:
|
||||||
$ref: '#/components/schemas/Schedule'
|
$ref: '#/components/schemas/Schedule'
|
||||||
required:
|
required:
|
||||||
@@ -1628,12 +1817,22 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_data:
|
new_data:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- task_key
|
- task_key
|
||||||
- new_data
|
- new_data
|
||||||
|
UpdateTaskOptionsRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
task_key:
|
||||||
|
$ref: '#/components/schemas/TaskType'
|
||||||
|
new_options:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- task_key
|
||||||
|
- new_options
|
||||||
GetTaskResponse:
|
GetTaskResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1690,6 +1889,51 @@ components:
|
|||||||
description: All video playlists
|
description: All video playlists
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Playlist'
|
$ref: '#/components/schemas/Playlist'
|
||||||
|
GetAllFilesRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sort:
|
||||||
|
$ref: '#/components/schemas/Sort'
|
||||||
|
range:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: number
|
||||||
|
description: Two elements allowed, start index and end index
|
||||||
|
minItems: 2
|
||||||
|
maxItems: 2
|
||||||
|
default: null
|
||||||
|
text_search:
|
||||||
|
type: string
|
||||||
|
description: Filter files by title
|
||||||
|
default: null
|
||||||
|
file_type_filter:
|
||||||
|
$ref: '#/components/schemas/FileTypeFilter'
|
||||||
|
favorite_filter:
|
||||||
|
type: boolean
|
||||||
|
description: If set to true, only gets favorites
|
||||||
|
default: false
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
|
description: Include if you want to filter by subscription
|
||||||
|
default: null
|
||||||
|
Sort:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
by:
|
||||||
|
type: string
|
||||||
|
description: Property to sort by
|
||||||
|
default: registered
|
||||||
|
order:
|
||||||
|
type: number
|
||||||
|
description: 1 for ascending, -1 for descending
|
||||||
|
default: -1
|
||||||
|
FileTypeFilter:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- audio_only
|
||||||
|
- video_only
|
||||||
|
- both
|
||||||
|
default: both
|
||||||
GetAllFilesResponse:
|
GetAllFilesResponse:
|
||||||
required:
|
required:
|
||||||
- files
|
- files
|
||||||
@@ -1727,6 +1971,18 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
file:
|
file:
|
||||||
$ref: '#/components/schemas/DatabaseFile'
|
$ref: '#/components/schemas/DatabaseFile'
|
||||||
|
UpdateFileRequest:
|
||||||
|
required:
|
||||||
|
- uid
|
||||||
|
- change_obj
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uid:
|
||||||
|
type: string
|
||||||
|
description: Video UID
|
||||||
|
change_obj:
|
||||||
|
type: object
|
||||||
|
description: Object with fields to update as keys and their new values
|
||||||
SharingToggle:
|
SharingToggle:
|
||||||
required:
|
required:
|
||||||
- uid
|
- uid
|
||||||
@@ -1740,7 +1996,6 @@ components:
|
|||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- url
|
- url
|
||||||
- streamingOnly
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -1768,11 +2023,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
UnsubscribeRequest:
|
UnsubscribeRequest:
|
||||||
required:
|
required:
|
||||||
- sub
|
- sub_id
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sub:
|
sub_id:
|
||||||
$ref: '#/components/schemas/SubscriptionRequestData'
|
type: string
|
||||||
deleteMode:
|
deleteMode:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Defaults to false
|
description: Defaults to false
|
||||||
@@ -1785,6 +2040,13 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
|
CheckSubscriptionRequest:
|
||||||
|
required:
|
||||||
|
- sub_id
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
DeleteAllFilesResponse:
|
DeleteAllFilesResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1796,16 +2058,11 @@ components:
|
|||||||
description: Number of files removed
|
description: Number of files removed
|
||||||
DeleteSubscriptionFileRequest:
|
DeleteSubscriptionFileRequest:
|
||||||
required:
|
required:
|
||||||
- file
|
- file_uid
|
||||||
- sub
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
file:
|
|
||||||
type: string
|
|
||||||
file_uid:
|
file_uid:
|
||||||
type: string
|
type: string
|
||||||
sub:
|
|
||||||
$ref: '#/components/schemas/SubscriptionRequestData'
|
|
||||||
deleteForever:
|
deleteForever:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.'
|
description: 'If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.'
|
||||||
@@ -1853,7 +2110,6 @@ components:
|
|||||||
- uids
|
- uids
|
||||||
- playlistName
|
- playlistName
|
||||||
- thumbnailURL
|
- thumbnailURL
|
||||||
- type
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
playlistName:
|
playlistName:
|
||||||
@@ -1862,8 +2118,6 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type:
|
|
||||||
$ref: '#/components/schemas/FileType'
|
|
||||||
thumbnailURL:
|
thumbnailURL:
|
||||||
type: string
|
type: string
|
||||||
CreatePlaylistResponse:
|
CreatePlaylistResponse:
|
||||||
@@ -1893,15 +2147,17 @@ components:
|
|||||||
required:
|
required:
|
||||||
- playlist
|
- playlist
|
||||||
- success
|
- success
|
||||||
- type
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
playlist:
|
playlist:
|
||||||
$ref: '#/components/schemas/Playlist'
|
$ref: '#/components/schemas/Playlist'
|
||||||
type:
|
|
||||||
$ref: '#/components/schemas/FileType'
|
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
file_objs:
|
||||||
|
type: array
|
||||||
|
description: File objects for every uid in the playlist's uids property, in the same order
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/DatabaseFile'
|
||||||
GetPlaylistsRequest:
|
GetPlaylistsRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1926,13 +2182,10 @@ components:
|
|||||||
DeletePlaylistRequest:
|
DeletePlaylistRequest:
|
||||||
required:
|
required:
|
||||||
- playlist_id
|
- playlist_id
|
||||||
- type
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
playlist_id:
|
playlist_id:
|
||||||
type: string
|
type: string
|
||||||
type:
|
|
||||||
$ref: '#/components/schemas/FileType'
|
|
||||||
DownloadFileRequest:
|
DownloadFileRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1949,17 +2202,83 @@ components:
|
|||||||
type:
|
type:
|
||||||
$ref: '#/components/schemas/FileType'
|
$ref: '#/components/schemas/FileType'
|
||||||
DownloadArchiveRequest:
|
DownloadArchiveRequest:
|
||||||
required:
|
|
||||||
- sub
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sub:
|
type:
|
||||||
required:
|
$ref: '#/components/schemas/FileType'
|
||||||
- archive_dir
|
sub_id:
|
||||||
type: object
|
type: string
|
||||||
properties:
|
Archive:
|
||||||
archive_dir:
|
required:
|
||||||
type: string
|
- extractor
|
||||||
|
- id
|
||||||
|
- type
|
||||||
|
- title
|
||||||
|
- timestamp
|
||||||
|
- uid
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
extractor:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/FileType'
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
user_uid:
|
||||||
|
type: string
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: number
|
||||||
|
uid:
|
||||||
|
type: string
|
||||||
|
DeleteArchiveItemsRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- archives
|
||||||
|
properties:
|
||||||
|
archives:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Archive'
|
||||||
|
ImportArchiveRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- archive
|
||||||
|
- type
|
||||||
|
properties:
|
||||||
|
archive:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/FileType'
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
|
GetArchivesRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/FileType'
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
|
GetArchivesResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- archives
|
||||||
|
properties:
|
||||||
|
archives:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Archive'
|
||||||
|
UploadCookiesRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cookies
|
||||||
|
properties:
|
||||||
|
cookies:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
UpdaterStatus:
|
UpdaterStatus:
|
||||||
required:
|
required:
|
||||||
- details
|
- details
|
||||||
@@ -1980,8 +2299,6 @@ components:
|
|||||||
tag:
|
tag:
|
||||||
type: string
|
type: string
|
||||||
DBInfoResponse:
|
DBInfoResponse:
|
||||||
required:
|
|
||||||
- db_info
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
using_local_db:
|
using_local_db:
|
||||||
@@ -2003,6 +2320,8 @@ components:
|
|||||||
$ref: '#/components/schemas/TableInfo'
|
$ref: '#/components/schemas/TableInfo'
|
||||||
download_queue:
|
download_queue:
|
||||||
$ref: '#/components/schemas/TableInfo'
|
$ref: '#/components/schemas/TableInfo'
|
||||||
|
archives:
|
||||||
|
$ref: '#/components/schemas/TableInfo'
|
||||||
TransferDBResponse:
|
TransferDBResponse:
|
||||||
required:
|
required:
|
||||||
- success
|
- success
|
||||||
@@ -2153,7 +2472,6 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
result:
|
result:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/file'
|
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
formats:
|
formats:
|
||||||
@@ -2303,6 +2621,7 @@ components:
|
|||||||
- upload_date
|
- upload_date
|
||||||
- uploader
|
- uploader
|
||||||
- url
|
- url
|
||||||
|
- favorite
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -2311,6 +2630,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
thumbnailURL:
|
thumbnailURL:
|
||||||
type: string
|
type: string
|
||||||
|
description: Backup if thumbnailPath is not defined
|
||||||
|
thumbnailPath:
|
||||||
|
type: string
|
||||||
isAudio:
|
isAudio:
|
||||||
type: boolean
|
type: boolean
|
||||||
duration:
|
duration:
|
||||||
@@ -2322,14 +2644,35 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
size:
|
size:
|
||||||
type: number
|
type: number
|
||||||
|
description: In bytes
|
||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
upload_date:
|
upload_date:
|
||||||
type: string
|
type: string
|
||||||
uid:
|
uid:
|
||||||
type: string
|
type: string
|
||||||
|
user_uid:
|
||||||
|
type: string
|
||||||
sharingEnabled:
|
sharingEnabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
category:
|
||||||
|
$ref: '#/components/schemas/Category'
|
||||||
|
view_count:
|
||||||
|
type: number
|
||||||
|
local_view_count:
|
||||||
|
type: number
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
|
registered:
|
||||||
|
type: number
|
||||||
|
height:
|
||||||
|
type: number
|
||||||
|
description: In pixels, only for videos
|
||||||
|
abr:
|
||||||
|
type: number
|
||||||
|
description: In Kbps
|
||||||
|
favorite:
|
||||||
|
type: boolean
|
||||||
Playlist:
|
Playlist:
|
||||||
required:
|
required:
|
||||||
- uids
|
- uids
|
||||||
@@ -2359,6 +2702,10 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
user_uid:
|
user_uid:
|
||||||
type: string
|
type: string
|
||||||
|
auto:
|
||||||
|
type: boolean
|
||||||
|
sharingEnabled:
|
||||||
|
type: boolean
|
||||||
Download:
|
Download:
|
||||||
required:
|
required:
|
||||||
- url
|
- url
|
||||||
@@ -2385,6 +2732,8 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
paused:
|
paused:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
cancelled:
|
||||||
|
type: boolean
|
||||||
finished_step:
|
finished_step:
|
||||||
type: boolean
|
type: boolean
|
||||||
url:
|
url:
|
||||||
@@ -2403,12 +2752,18 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Error text, set if download fails.
|
description: Error text, set if download fails.
|
||||||
nullable: true
|
nullable: true
|
||||||
|
error_type:
|
||||||
|
type: string
|
||||||
|
description: Error type, may or may not be set in case of an error
|
||||||
|
nullable: true
|
||||||
user_uid:
|
user_uid:
|
||||||
type: string
|
type: string
|
||||||
sub_id:
|
sub_id:
|
||||||
type: string
|
type: string
|
||||||
sub_name:
|
sub_name:
|
||||||
type: string
|
type: string
|
||||||
|
prefetched_info:
|
||||||
|
type: object
|
||||||
Task:
|
Task:
|
||||||
required:
|
required:
|
||||||
- key
|
- key
|
||||||
@@ -2422,6 +2777,8 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
|
$ref: '#/components/schemas/TaskType'
|
||||||
|
title:
|
||||||
type: string
|
type: string
|
||||||
last_ran:
|
last_ran:
|
||||||
type: number
|
type: number
|
||||||
@@ -2436,7 +2793,20 @@ components:
|
|||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
schedule:
|
schedule:
|
||||||
|
$ref: '#/components/schemas/Schedule'
|
||||||
|
options:
|
||||||
type: object
|
type: object
|
||||||
|
TaskType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- backup_local_db
|
||||||
|
- missing_files_check
|
||||||
|
- missing_db_records
|
||||||
|
- duplicate_files_check
|
||||||
|
- youtubedl_update_check
|
||||||
|
- delete_old_files
|
||||||
|
- import_legacy_archives
|
||||||
|
- rebuild_database
|
||||||
Schedule:
|
Schedule:
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@@ -2461,6 +2831,8 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
timestamp:
|
timestamp:
|
||||||
type: number
|
type: number
|
||||||
|
tz:
|
||||||
|
type: string
|
||||||
DBBackup:
|
DBBackup:
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
@@ -2503,7 +2875,6 @@ components:
|
|||||||
- url
|
- url
|
||||||
- type
|
- type
|
||||||
- user_uid
|
- user_uid
|
||||||
- streamingOnly
|
|
||||||
- isPlaylist
|
- isPlaylist
|
||||||
- videos
|
- videos
|
||||||
type: object
|
type: object
|
||||||
@@ -2519,10 +2890,10 @@ components:
|
|||||||
user_uid:
|
user_uid:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
streamingOnly:
|
|
||||||
type: boolean
|
|
||||||
isPlaylist:
|
isPlaylist:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
child_process:
|
||||||
|
type: object
|
||||||
archive:
|
archive:
|
||||||
type: string
|
type: string
|
||||||
timerange:
|
timerange:
|
||||||
@@ -2531,6 +2902,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
custom_output:
|
custom_output:
|
||||||
type: string
|
type: string
|
||||||
|
downloading:
|
||||||
|
type: boolean
|
||||||
|
paused:
|
||||||
|
type: boolean
|
||||||
videos:
|
videos:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -2545,28 +2920,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
passhash:
|
passhash:
|
||||||
type: string
|
type: string
|
||||||
files:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
audio:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/file'
|
|
||||||
video:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/file'
|
|
||||||
playlists:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
audio:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/file'
|
|
||||||
video:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/file'
|
|
||||||
subscriptions:
|
subscriptions:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -2592,6 +2945,7 @@ components:
|
|||||||
- sharing
|
- sharing
|
||||||
- advanced_download
|
- advanced_download
|
||||||
- downloads_manager
|
- downloads_manager
|
||||||
|
- tasks_manager
|
||||||
YesNo:
|
YesNo:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -2674,6 +3028,44 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
date:
|
date:
|
||||||
type: string
|
type: string
|
||||||
|
Notification:
|
||||||
|
required:
|
||||||
|
- uid
|
||||||
|
- type
|
||||||
|
- text
|
||||||
|
- read
|
||||||
|
- timestamp
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/NotificationType'
|
||||||
|
uid:
|
||||||
|
type: string
|
||||||
|
user_uid:
|
||||||
|
type: string
|
||||||
|
action:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/NotificationAction'
|
||||||
|
read:
|
||||||
|
type: boolean
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
timestamp:
|
||||||
|
type: number
|
||||||
|
NotificationAction:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- play
|
||||||
|
- retry_download
|
||||||
|
- view_download_error
|
||||||
|
- view_tasks
|
||||||
|
NotificationType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- download_complete
|
||||||
|
- download_error
|
||||||
|
- task_finished
|
||||||
BaseChangePermissionsRequest:
|
BaseChangePermissionsRequest:
|
||||||
required:
|
required:
|
||||||
- permission
|
- permission
|
||||||
@@ -2805,6 +3197,29 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/UserPermission'
|
$ref: '#/components/schemas/UserPermission'
|
||||||
|
DeleteNotificationRequest:
|
||||||
|
required:
|
||||||
|
- uid
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uid:
|
||||||
|
type: string
|
||||||
|
SetNotificationsToReadRequest:
|
||||||
|
required:
|
||||||
|
- uids
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
GetNotificationsResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
notifications:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Notification'
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
Auth query parameter:
|
Auth query parameter:
|
||||||
name: apiKey
|
name: apiKey
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -6,25 +6,15 @@
|
|||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||||
|
|
||||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 13](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 15](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||||
|
|
||||||
Now with [Docker](#Docker) support!
|
Now with [Docker](#Docker) support!
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
### USAGE OF THE NIGHTLY BUILDS IS HIGHLY RECOMMENDED.
|
|
||||||
|
|
||||||
For much better scaling with large datasets please run your YTDL-M instance with a MongoDB backend rather than the json file-based default.
|
|
||||||
It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
|
|
||||||
The (closed) issues as well as the project's Wiki will give you good starting points for your journey!
|
|
||||||
|
|
||||||
For MongoDB specifically there is [this little guide](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Check out the prerequisites, and go to the installation section. Easy as pie!
|
Check out the prerequisites, and go to the [installation](#Installing) section. Easy as pie!
|
||||||
|
|
||||||
Here's an image of what it'll look like once you're done:
|
Here's an image of what it'll look like once you're done:
|
||||||
|
|
||||||
@@ -38,13 +28,28 @@ Dark mode:
|
|||||||
|
|
||||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||||
|
|
||||||
Debian/Ubuntu:
|
Required dependencies:
|
||||||
|
|
||||||
|
* Node.js 16
|
||||||
|
* Python
|
||||||
|
|
||||||
|
Optional dependencies:
|
||||||
|
|
||||||
|
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||||
|
* [Twitch Downloader CLI](https://github.com/lay295/TwitchDownloader) (for downloading Twitch VOD chats)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Debian/Ubuntu</summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||||
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
||||||
```
|
```
|
||||||
|
|
||||||
CentOS 7:
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>CentOS 7</summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo yum install epel-release
|
sudo yum install epel-release
|
||||||
@@ -52,15 +57,16 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
|
|||||||
sudo yum install centos-release-scl-rh
|
sudo yum install centos-release-scl-rh
|
||||||
sudo yum install rh-nodejs12
|
sudo yum install rh-nodejs12
|
||||||
scl enable rh-nodejs12 bash
|
scl enable rh-nodejs12 bash
|
||||||
|
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
|
||||||
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional dependencies:
|
</details>
|
||||||
|
|
||||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
|
||||||
|
|
||||||
### Installing
|
### Installing
|
||||||
|
|
||||||
|
If you are using Docker, skip to the [Docker](#Docker) section. Otherwise, continue:
|
||||||
|
|
||||||
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||||
|
|
||||||
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file.
|
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file.
|
||||||
@@ -79,7 +85,9 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
|
|||||||
|
|
||||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||||
|
|
||||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm run build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
||||||
|
|
||||||
|
Lastly, type `npm -g install pm2` to install pm2 globally.
|
||||||
|
|
||||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
||||||
|
|
||||||
@@ -91,7 +99,7 @@ Alternatively, you can port forward the port specified in the config (defaults t
|
|||||||
|
|
||||||
### Host-specific instructions
|
### Host-specific instructions
|
||||||
|
|
||||||
If you're on a Synology NAS, unRAID or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
|
If you're on a Synology NAS, unRAID, Raspberry Pi 4 or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@@ -102,8 +110,6 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
|
|||||||
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
|
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
|
||||||
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
|
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
|
||||||
|
|
||||||
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
|
|
||||||
|
|
||||||
### Custom UID/GID
|
### Custom UID/GID
|
||||||
|
|
||||||
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
|
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
|
||||||
@@ -114,6 +120,12 @@ environment:
|
|||||||
GID: YOUR_GID
|
GID: YOUR_GID
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## MongoDB
|
||||||
|
|
||||||
|
For much better scaling with large datasets please run your YoutubeDL-Material instance with MongoDB backend rather than the json file-based default. It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
|
||||||
|
|
||||||
|
[Tutorial](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
|
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
|
||||||
@@ -124,7 +136,7 @@ Once you have enabled the API and have the key, you can start sending requests b
|
|||||||
|
|
||||||
## iOS Shortcut
|
## iOS Shortcut
|
||||||
|
|
||||||
If you are using iOS, try YoutubeDL-Material more conveniently with a Shortcut. With this Shorcut, you can easily start downloading YouTube video with just two taps! (Or maybe three?)
|
If you are using iOS, try YoutubeDL-Material more conveniently with a Shortcut. With this Shortcut, you can easily start downloading YouTube video with just two taps! (Or maybe three?)
|
||||||
|
|
||||||
You can download Shortcut [here.](https://routinehub.co/shortcut/10283/)
|
You can download Shortcut [here.](https://routinehub.co/shortcut/10283/)
|
||||||
|
|
||||||
|
|||||||
18
SECURITY.md
18
SECURITY.md
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Currently all work on this project goes into the nightly builds.
|
If you would like to see the latest updates, use the `nightly` tag on Docker.
|
||||||
4.2's RELEASE build is now quite old and should be considered legacy.
|
|
||||||
We urge users to use the nightly releases, because the project
|
|
||||||
constantly sees fixes.
|
|
||||||
|
|
||||||
| Version | Supported |
|
If you'd like to stick with more stable releases, use the `latest` tag on Docker or download the [latest release here](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest).
|
||||||
| ------------- | ------------------ |
|
|
||||||
| 4.2 Nightlies | :white_check_mark: |
|
| Version | Supported |
|
||||||
| 4.2 Release | :x: |
|
| -------------------- | ------------------ |
|
||||||
| < 4.2 | :x: |
|
| 4.3 Docker Nightlies | :white_check_mark: |
|
||||||
|
| 4.3 Release | :white_check_mark: |
|
||||||
|
| 4.2 Release | :x: |
|
||||||
|
| < 4.2 | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|||||||
24
angular.json
24
angular.json
@@ -30,7 +30,8 @@
|
|||||||
"src/backend"
|
"src/backend"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss",
|
||||||
|
"src/bootstrap.min.css"
|
||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
@@ -65,6 +66,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codespaces": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.codespaces.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"localize": ["es"]
|
"localize": ["es"]
|
||||||
}
|
}
|
||||||
@@ -82,6 +91,9 @@
|
|||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"browserTarget": "youtube-dl-material:build:es"
|
"browserTarget": "youtube-dl-material:build:es"
|
||||||
|
},
|
||||||
|
"codespaces": {
|
||||||
|
"browserTarget": "youtube-dl-material:build:codespaces"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,7 +130,8 @@
|
|||||||
"src/backend"
|
"src/backend"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss",
|
||||||
|
"src/bootstrap.min.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -151,7 +164,8 @@
|
|||||||
"tsConfig": "src/tsconfig.spec.json",
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss",
|
||||||
|
"src/bootstrap.min.css"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/assets",
|
"src/assets",
|
||||||
@@ -179,7 +193,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "youtube-dl-material",
|
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
@@ -188,5 +201,8 @@
|
|||||||
"@schematics/angular:directive": {
|
"@schematics/angular:directive": {
|
||||||
"prefix": "app"
|
"prefix": "app"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
587
backend/app.js
587
backend/app.js
@@ -1,8 +1,7 @@
|
|||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const auth_api = require('./authentication/auth');
|
const auth_api = require('./authentication/auth');
|
||||||
const winston = require('winston');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
@@ -18,11 +17,8 @@ const URL = require('url').URL;
|
|||||||
const CONSTS = require('./consts')
|
const CONSTS = require('./consts')
|
||||||
const read_last_lines = require('read-last-lines');
|
const read_last_lines = require('read-last-lines');
|
||||||
const ps = require('ps-node');
|
const ps = require('ps-node');
|
||||||
|
const Feed = require('feed').Feed;
|
||||||
// needed if bin/details somehow gets deleted
|
const session = require('express-session');
|
||||||
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
|
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
@@ -32,6 +28,9 @@ const subscriptions_api = require('./subscriptions');
|
|||||||
const categories_api = require('./categories');
|
const categories_api = require('./categories');
|
||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
const youtubedl_api = require('./youtube-dl');
|
const youtubedl_api = require('./youtube-dl');
|
||||||
|
const archive_api = require('./archive');
|
||||||
|
const files_api = require('./files');
|
||||||
|
const notifications_api = require('./notifications');
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -68,7 +67,9 @@ db.defaults(
|
|||||||
configWriteFlag: false,
|
configWriteFlag: false,
|
||||||
downloads: {},
|
downloads: {},
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
files_to_db_migration_complete: false
|
files_to_db_migration_complete: false,
|
||||||
|
tasks_manager_role_migration_complete: false,
|
||||||
|
archives_migration_complete: false
|
||||||
}).write();
|
}).write();
|
||||||
|
|
||||||
users_db.defaults(
|
users_db.defaults(
|
||||||
@@ -101,7 +102,6 @@ let backendPort = null;
|
|||||||
let useDefaultDownloadingAgent = null;
|
let useDefaultDownloadingAgent = null;
|
||||||
let customDownloadingAgent = null;
|
let customDownloadingAgent = null;
|
||||||
let allowSubscriptions = null;
|
let allowSubscriptions = null;
|
||||||
let archivePath = path.join(__dirname, 'appdata', 'archives');
|
|
||||||
|
|
||||||
// other needed values
|
// other needed values
|
||||||
let url_domain = null;
|
let url_domain = null;
|
||||||
@@ -148,22 +148,19 @@ if (fs.existsSync('version.json')) {
|
|||||||
|
|
||||||
// don't overwrite config if it already happened.. NOT
|
// don't overwrite config if it already happened.. NOT
|
||||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||||
let writeConfigMode = process.env.write_ytdl_config;
|
|
||||||
|
|
||||||
// checks if config exists, if not, a config is auto generated
|
// checks if config exists, if not, a config is auto generated
|
||||||
config_api.configExistsCheck();
|
config_api.configExistsCheck();
|
||||||
|
|
||||||
if (writeConfigMode) {
|
setAndLoadConfig();
|
||||||
setAndLoadConfig();
|
|
||||||
} else {
|
|
||||||
loadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: false }));
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
// use passport
|
// use passport
|
||||||
app.use(auth_api.passport.initialize());
|
app.use(auth_api.passport.initialize());
|
||||||
|
app.use(session({ secret: uuid(), resave: true, saveUninitialized: true }))
|
||||||
|
app.use(auth_api.passport.session());
|
||||||
|
|
||||||
// actual functions
|
// actual functions
|
||||||
|
|
||||||
@@ -174,10 +171,10 @@ async function checkMigrations() {
|
|||||||
if (!simplified_db_migration_complete) {
|
if (!simplified_db_migration_complete) {
|
||||||
logger.info('Beginning migration: 4.1->4.2+')
|
logger.info('Beginning migration: 4.1->4.2+')
|
||||||
let success = await simplifyDBFileStructure();
|
let success = await simplifyDBFileStructure();
|
||||||
success = success && await db_api.addMetadataPropertyToDB('view_count');
|
success = success && await files_api.addMetadataPropertyToDB('view_count');
|
||||||
success = success && await db_api.addMetadataPropertyToDB('description');
|
success = success && await files_api.addMetadataPropertyToDB('description');
|
||||||
success = success && await db_api.addMetadataPropertyToDB('height');
|
success = success && await files_api.addMetadataPropertyToDB('height');
|
||||||
success = success && await db_api.addMetadataPropertyToDB('abr');
|
success = success && await files_api.addMetadataPropertyToDB('abr');
|
||||||
// sets migration to complete
|
// sets migration to complete
|
||||||
db.set('simplified_db_migration_complete', true).write();
|
db.set('simplified_db_migration_complete', true).write();
|
||||||
if (success) { logger.info('4.1->4.2+ migration complete!'); }
|
if (success) { logger.info('4.1->4.2+ migration complete!'); }
|
||||||
@@ -188,13 +185,31 @@ async function checkMigrations() {
|
|||||||
if (!new_db_system_migration_complete) {
|
if (!new_db_system_migration_complete) {
|
||||||
logger.info('Beginning migration: 4.2->4.3+')
|
logger.info('Beginning migration: 4.2->4.3+')
|
||||||
let success = await db_api.importJSONToDB(db.value(), users_db.value());
|
let success = await db_api.importJSONToDB(db.value(), users_db.value());
|
||||||
|
await tasks_api.setupTasks(); // necessary as tasks were not properly initialized at first
|
||||||
// sets migration to complete
|
// sets migration to complete
|
||||||
db.set('new_db_system_migration_complete', true).write();
|
db.set('new_db_system_migration_complete', true).write();
|
||||||
if (success) { logger.info('4.2->4.3+ migration complete!'); }
|
if (success) { logger.info('4.2->4.3+ migration complete!'); }
|
||||||
else { logger.error('Migration failed: 4.2->4.3+'); }
|
else { logger.error('Migration failed: 4.2->4.3+'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tasks_manager_role_migration_complete = db.get('tasks_manager_role_migration_complete').value();
|
||||||
|
if (!tasks_manager_role_migration_complete) {
|
||||||
|
logger.info('Checking if tasks manager role permissions exist for admin user...');
|
||||||
|
const success = await auth_api.changeRolePermissions('admin', 'tasks_manager', 'yes');
|
||||||
|
if (success) logger.info('Task manager permissions check complete!');
|
||||||
|
else logger.error('Failed to auto add tasks manager permissions to admin role!');
|
||||||
|
db.set('tasks_manager_role_migration_complete', true).write();
|
||||||
|
}
|
||||||
|
|
||||||
|
const archives_migration_complete = db.get('archives_migration_complete').value();
|
||||||
|
if (!archives_migration_complete) {
|
||||||
|
logger.info('Checking if archives have been migrated...');
|
||||||
|
const imported_archives = await archive_api.importArchives();
|
||||||
|
if (imported_archives) logger.info('Archives migration complete!');
|
||||||
|
else logger.error('Failed to migrate archives!');
|
||||||
|
db.set('archives_migration_complete', true).write();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,8 +499,9 @@ async function setAndLoadConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setConfigFromEnv() {
|
async function setConfigFromEnv() {
|
||||||
let config_items = getEnvConfigItems();
|
const config_items = getEnvConfigItems();
|
||||||
let success = config_api.setConfigItems(config_items);
|
if (!config_items || config_items.length === 0) return true;
|
||||||
|
const success = config_api.setConfigItems(config_items);
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.info('Config items set using ENV variables.');
|
logger.info('Config items set using ENV variables.');
|
||||||
await utils.wait(100);
|
await utils.wait(100);
|
||||||
@@ -500,13 +516,11 @@ async function loadConfig() {
|
|||||||
loadConfigValues();
|
loadConfigValues();
|
||||||
|
|
||||||
// connect to DB
|
// connect to DB
|
||||||
await db_api.connectToDB();
|
if (!config_api.getConfigItem('ytdl_use_local_db'))
|
||||||
|
await db_api.connectToDB();
|
||||||
db_api.database_initialized = true;
|
db_api.database_initialized = true;
|
||||||
db_api.database_initialized_bs.next(true);
|
db_api.database_initialized_bs.next(true);
|
||||||
|
|
||||||
// creates archive path if missing
|
|
||||||
await fs.ensureDir(archivePath);
|
|
||||||
|
|
||||||
// check migrations
|
// check migrations
|
||||||
await checkMigrations();
|
await checkMigrations();
|
||||||
|
|
||||||
@@ -517,15 +531,10 @@ async function loadConfig() {
|
|||||||
if (allowSubscriptions) {
|
if (allowSubscriptions) {
|
||||||
// set downloading to false
|
// set downloading to false
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
||||||
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
|
||||||
|
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false, child_process: null});
|
||||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||||
const watchSubscriptionsInterval = function() {
|
subscriptions_api.watchSubscriptionsInterval();
|
||||||
watchSubscriptions();
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
setTimeout(watchSubscriptionsInterval, subscriptionsCheckInterval*1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
watchSubscriptionsInterval();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the server here
|
// start the server here
|
||||||
@@ -552,69 +561,11 @@ function loadConfigValues() {
|
|||||||
url_domain = new URL(url);
|
url_domain = new URL(url);
|
||||||
|
|
||||||
let logger_level = config_api.getConfigItem('ytdl_logger_level');
|
let logger_level = config_api.getConfigItem('ytdl_logger_level');
|
||||||
const possible_levels = ['error', 'warn', 'info', 'verbose', 'debug'];
|
utils.updateLoggerLevel(logger_level);
|
||||||
if (!possible_levels.includes(logger_level)) {
|
|
||||||
logger.error(`${logger_level} is not a valid logger level! Choose one of the following: ${possible_levels.join(', ')}.`)
|
|
||||||
logger_level = 'info';
|
|
||||||
}
|
|
||||||
logger.level = logger_level;
|
|
||||||
winston.loggers.get('console').level = logger_level;
|
|
||||||
logger.transports[2].level = logger_level;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateSubcriptionRetrievalDelay(subscriptions_amount) {
|
|
||||||
// frequency is once every 5 mins by default
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
let interval_in_ms = subscriptionsCheckInterval * 1000;
|
|
||||||
const subinterval_in_ms = interval_in_ms/subscriptions_amount;
|
|
||||||
return subinterval_in_ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function watchSubscriptions() {
|
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
|
||||||
|
|
||||||
if (!subscriptions) return;
|
|
||||||
|
|
||||||
const valid_subscriptions = subscriptions.filter(sub => !sub.paused);
|
|
||||||
|
|
||||||
let subscriptions_amount = valid_subscriptions.length;
|
|
||||||
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
|
||||||
|
|
||||||
let current_delay = 0;
|
|
||||||
|
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
|
||||||
for (let i = 0; i < valid_subscriptions.length; i++) {
|
|
||||||
let sub = valid_subscriptions[i];
|
|
||||||
|
|
||||||
// don't check the sub if the last check for the same subscription has not completed
|
|
||||||
if (subscription_timeouts[sub.id]) {
|
|
||||||
logger.verbose(`Subscription: skipped checking ${sub.name} as the last check for ${sub.name} has not completed.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sub.name) {
|
|
||||||
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.verbose('Watching ' + sub.name + ' with delay interval of ' + delay_interval);
|
|
||||||
setTimeout(async () => {
|
|
||||||
const multiUserModeChanged = config_api.getConfigItem('ytdl_multi_user_mode') !== multiUserMode;
|
|
||||||
if (multiUserModeChanged) {
|
|
||||||
logger.verbose(`Skipping subscription ${sub.name} due to multi-user mode change.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await subscriptions_api.getVideosForSub(sub, sub.user_uid);
|
|
||||||
subscription_timeouts[sub.id] = false;
|
|
||||||
}, current_delay);
|
|
||||||
subscription_timeouts[sub.id] = true;
|
|
||||||
current_delay += delay_interval;
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
if (current_delay >= subscriptionsCheckInterval * 1000) current_delay = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrigin() {
|
function getOrigin() {
|
||||||
|
if (process.env.CODESPACES) return `https://${process.env.CODESPACE_NAME}-4200.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
|
||||||
return url_domain.origin;
|
return url_domain.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,38 +590,11 @@ function generateEnvVarConfigItem(key) {
|
|||||||
return {key: key, value: process['env'][key]};
|
return {key: key, value: process['env'][key]};
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently only works for single urls
|
|
||||||
async function getUrlInfos(url) {
|
|
||||||
let startDate = Date.now();
|
|
||||||
let result = [];
|
|
||||||
return new Promise(resolve => {
|
|
||||||
youtubedl.exec(url, ['--dump-json'], {maxBuffer: Infinity}, (err, output) => {
|
|
||||||
let new_date = Date.now();
|
|
||||||
let difference = (new_date - startDate)/1000;
|
|
||||||
logger.debug(`URL info retrieval delay: ${difference} seconds.`);
|
|
||||||
if (err) {
|
|
||||||
logger.error(`Error during retrieving formats for ${url}: ${err}`);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
let try_putput = null;
|
|
||||||
try {
|
|
||||||
try_putput = JSON.parse(output);
|
|
||||||
result = try_putput;
|
|
||||||
} catch(e) {
|
|
||||||
logger.error(`Failed to retrieve available formats for url: ${url}`);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// youtube-dl functions
|
// youtube-dl functions
|
||||||
|
|
||||||
async function startYoutubeDL() {
|
async function startYoutubeDL() {
|
||||||
// auto update youtube-dl
|
// auto update youtube-dl
|
||||||
youtubedl_api.verifyBinaryExistsLinux();
|
await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
|
||||||
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
@@ -690,7 +614,7 @@ app.use(function(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||||
next();
|
next();
|
||||||
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/')) {
|
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss') || req.path.includes('/api/telegramRequest')) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||||
@@ -711,7 +635,7 @@ const optionalJwt = async function (req, res, next) {
|
|||||||
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
||||||
const uid = using_body ? req.body.uid : req.query.uid;
|
const uid = using_body ? req.body.uid : req.query.uid;
|
||||||
const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id;
|
const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id;
|
||||||
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await db_api.getPlaylist(playlist_id, uuid, true);
|
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await files_api.getPlaylist(playlist_id, uuid, true);
|
||||||
if (file) {
|
if (file) {
|
||||||
req.can_watch = true;
|
req.can_watch = true;
|
||||||
return next();
|
return next();
|
||||||
@@ -763,7 +687,7 @@ app.post('/api/restartServer', optionalJwt, (req, res) => {
|
|||||||
|
|
||||||
app.get('/api/getDBInfo', optionalJwt, async (req, res) => {
|
app.get('/api/getDBInfo', optionalJwt, async (req, res) => {
|
||||||
const db_info = await db_api.getDBStats();
|
const db_info = await db_api.getDBStats();
|
||||||
res.send({db_info: db_info});
|
res.send(db_info);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/transferDB', optionalJwt, async (req, res) => {
|
app.post('/api/transferDB', optionalJwt, async (req, res) => {
|
||||||
@@ -803,11 +727,13 @@ app.post('/api/downloadFile', optionalJwt, async function(req, res) {
|
|||||||
additionalArgs: req.body.additionalArgs,
|
additionalArgs: req.body.additionalArgs,
|
||||||
customOutput: req.body.customOutput,
|
customOutput: req.body.customOutput,
|
||||||
selectedHeight: req.body.selectedHeight,
|
selectedHeight: req.body.selectedHeight,
|
||||||
|
maxHeight: req.body.maxHeight,
|
||||||
customQualityConfiguration: req.body.customQualityConfiguration,
|
customQualityConfiguration: req.body.customQualityConfiguration,
|
||||||
youtubeUsername: req.body.youtubeUsername,
|
youtubeUsername: req.body.youtubeUsername,
|
||||||
youtubePassword: req.body.youtubePassword,
|
youtubePassword: req.body.youtubePassword,
|
||||||
ui_uid: req.body.ui_uid,
|
ui_uid: req.body.ui_uid,
|
||||||
cropFileSettings: req.body.cropFileSettings
|
cropFileSettings: req.body.cropFileSettings,
|
||||||
|
ignoreArchive: req.body.ignoreArchive
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = await downloader_api.createDownload(url, type, options, user_uid);
|
const download = await downloader_api.createDownload(url, type, options, user_uid);
|
||||||
@@ -833,6 +759,7 @@ app.post('/api/generateArgs', optionalJwt, async function(req, res) {
|
|||||||
additionalArgs: req.body.additionalArgs,
|
additionalArgs: req.body.additionalArgs,
|
||||||
customOutput: req.body.customOutput,
|
customOutput: req.body.customOutput,
|
||||||
selectedHeight: req.body.selectedHeight,
|
selectedHeight: req.body.selectedHeight,
|
||||||
|
maxHeight: req.body.maxHeight,
|
||||||
customQualityConfiguration: req.body.customQualityConfiguration,
|
customQualityConfiguration: req.body.customQualityConfiguration,
|
||||||
youtubeUsername: req.body.youtubeUsername,
|
youtubeUsername: req.body.youtubeUsername,
|
||||||
youtubePassword: req.body.youtubePassword,
|
youtubePassword: req.body.youtubePassword,
|
||||||
@@ -911,45 +838,40 @@ app.post('/api/getFile', optionalJwt, async function (req, res) {
|
|||||||
|
|
||||||
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||||
// these are returned
|
// these are returned
|
||||||
let files = null;
|
const sort = req.body.sort;
|
||||||
let playlists = null;
|
const range = req.body.range;
|
||||||
let sort = req.body.sort;
|
const text_search = req.body.text_search;
|
||||||
let range = req.body.range;
|
const file_type_filter = req.body.file_type_filter;
|
||||||
let text_search = req.body.text_search;
|
const favorite_filter = req.body.favorite_filter;
|
||||||
let file_type_filter = req.body.file_type_filter;
|
const sub_id = req.body.sub_id;
|
||||||
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
const filter_obj = {user_uid: uuid};
|
const {files, file_count} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
|
||||||
const regex = true;
|
|
||||||
if (text_search) {
|
|
||||||
if (regex) {
|
|
||||||
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
|
|
||||||
} else {
|
|
||||||
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
|
||||||
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
|
||||||
|
|
||||||
files = await db_api.getRecords('files', filter_obj, false, sort, range, text_search);
|
|
||||||
let file_count = await db_api.getRecords('files', filter_obj, true);
|
|
||||||
playlists = await db_api.getRecords('playlists', {user_uid: uuid});
|
|
||||||
|
|
||||||
const categories = await categories_api.getCategoriesAsPlaylists(files);
|
|
||||||
if (categories) {
|
|
||||||
playlists = playlists.concat(categories);
|
|
||||||
}
|
|
||||||
|
|
||||||
files = JSON.parse(JSON.stringify(files));
|
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
files: files,
|
files: files,
|
||||||
file_count: file_count,
|
file_count: file_count,
|
||||||
playlists: playlists
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/updateFile', optionalJwt, async function (req, res) {
|
||||||
|
const uid = req.body.uid;
|
||||||
|
const change_obj = req.body.change_obj;
|
||||||
|
|
||||||
|
const file = await db_api.updateRecord('files', {uid: uid}, change_obj);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
error: 'File could not be found'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.send({
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/checkConcurrentStream', async (req, res) => {
|
app.post('/api/checkConcurrentStream', async (req, res) => {
|
||||||
const uid = req.body.uid;
|
const uid = req.body.uid;
|
||||||
|
|
||||||
@@ -1067,9 +989,6 @@ app.post('/api/disableSharing', optionalJwt, async function(req, res) {
|
|||||||
await db_api.updateRecord('files', {uid: uid}, {sharingEnabled: false})
|
await db_api.updateRecord('files', {uid: uid}, {sharingEnabled: false})
|
||||||
} else if (is_playlist) {
|
} else if (is_playlist) {
|
||||||
await db_api.updateRecord(`playlists`, {id: uid}, {sharingEnabled: false});
|
await db_api.updateRecord(`playlists`, {id: uid}, {sharingEnabled: false});
|
||||||
} else if (type === 'subscription') {
|
|
||||||
// TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every
|
|
||||||
// time they are requested from the subscription directory.
|
|
||||||
} else {
|
} else {
|
||||||
// error
|
// error
|
||||||
success = false;
|
success = false;
|
||||||
@@ -1084,7 +1003,7 @@ app.post('/api/disableSharing', optionalJwt, async function(req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/incrementViewCount', optionalJwt, async (req, res) => {
|
app.post('/api/incrementViewCount', async (req, res) => {
|
||||||
let file_uid = req.body.file_uid;
|
let file_uid = req.body.file_uid;
|
||||||
let sub_id = req.body.sub_id;
|
let sub_id = req.body.sub_id;
|
||||||
let uuid = req.body.uuid;
|
let uuid = req.body.uuid;
|
||||||
@@ -1093,7 +1012,7 @@ app.post('/api/incrementViewCount', optionalJwt, async (req, res) => {
|
|||||||
uuid = req.user.uid;
|
uuid = req.user.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file_obj = await db_api.getVideo(file_uid, uuid, sub_id);
|
const file_obj = await files_api.getVideo(file_uid, uuid, sub_id);
|
||||||
|
|
||||||
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
|
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
|
||||||
const new_view_count = current_view_count + 1;
|
const new_view_count = current_view_count + 1;
|
||||||
@@ -1201,10 +1120,10 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
||||||
let deleteMode = req.body.deleteMode
|
let deleteMode = req.body.deleteMode
|
||||||
let sub = req.body.sub;
|
let sub_id = req.body.sub_id;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode, user_uid);
|
let result_obj = subscriptions_api.unsubscribe(sub_id, deleteMode, user_uid);
|
||||||
if (result_obj.success) {
|
if (result_obj.success) {
|
||||||
res.send({
|
res.send({
|
||||||
success: result_obj.success
|
success: result_obj.success
|
||||||
@@ -1219,12 +1138,9 @@ app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
|
app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
|
||||||
let deleteForever = req.body.deleteForever;
|
let deleteForever = req.body.deleteForever;
|
||||||
let file = req.body.file;
|
|
||||||
let file_uid = req.body.file_uid;
|
let file_uid = req.body.file_uid;
|
||||||
let sub = req.body.sub;
|
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
||||||
|
|
||||||
let success = await subscriptions_api.deleteSubscriptionFile(sub, file, deleteForever, file_uid, user_uid);
|
let success = await files_api.deleteFile(file_uid, deleteForever);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.send({
|
res.send({
|
||||||
@@ -1257,7 +1173,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|||||||
subscription = JSON.parse(JSON.stringify(subscription));
|
subscription = JSON.parse(JSON.stringify(subscription));
|
||||||
|
|
||||||
// get sub videos
|
// get sub videos
|
||||||
if (subscription.name && !subscription.streamingOnly) {
|
if (subscription.name) {
|
||||||
var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos;
|
var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos;
|
||||||
subscription['videos'] = parsed_files;
|
subscription['videos'] = parsed_files;
|
||||||
// loop through files for extra processing
|
// loop through files for extra processing
|
||||||
@@ -1267,19 +1183,6 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|||||||
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
|
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send({
|
|
||||||
subscription: subscription,
|
|
||||||
files: parsed_files
|
|
||||||
});
|
|
||||||
} else if (subscription.name && subscription.streamingOnly) {
|
|
||||||
// return list of videos
|
|
||||||
let parsed_files = [];
|
|
||||||
if (subscription.videos) {
|
|
||||||
for (let i = 0; i < subscription.videos.length; i++) {
|
|
||||||
const video = subscription.videos[i];
|
|
||||||
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date, video.view_count, video.height, video.abr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.send({
|
res.send({
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
files: parsed_files
|
files: parsed_files
|
||||||
@@ -1290,21 +1193,49 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
||||||
let subID = req.body.subID;
|
const subID = req.body.subID;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
||||||
|
|
||||||
let sub = subscriptions_api.getSubscription(subID, user_uid);
|
const sub = subscriptions_api.getSubscription(subID);
|
||||||
subscriptions_api.getVideosForSub(sub, user_uid);
|
subscriptions_api.getVideosForSub(sub.id);
|
||||||
res.send({
|
res.send({
|
||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
||||||
let updated_sub = req.body.subscription;
|
const updated_sub = req.body.subscription;
|
||||||
|
|
||||||
|
const success = subscriptions_api.updateSubscription(updated_sub);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/checkSubscription', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
let success = subscriptions_api.updateSubscription(updated_sub, user_uid);
|
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cancelCheckSubscription', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = subscriptions_api.cancelCheckSubscription(sub_id, user_uid);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cancelSubscriptionCheck', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1324,9 +1255,8 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
||||||
let playlistName = req.body.playlistName;
|
let playlistName = req.body.playlistName;
|
||||||
let uids = req.body.uids;
|
let uids = req.body.uids;
|
||||||
let type = req.body.type;
|
|
||||||
|
|
||||||
const new_playlist = await db_api.createPlaylist(playlistName, uids, type, req.isAuthenticated() ? req.user.uid : null);
|
const new_playlist = await files_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
new_playlist: new_playlist,
|
new_playlist: new_playlist,
|
||||||
@@ -1339,13 +1269,13 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
|
|||||||
let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null);
|
let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null);
|
||||||
let include_file_metadata = req.body.include_file_metadata;
|
let include_file_metadata = req.body.include_file_metadata;
|
||||||
|
|
||||||
const playlist = await db_api.getPlaylist(playlist_id, uuid);
|
const playlist = await files_api.getPlaylist(playlist_id, uuid);
|
||||||
const file_objs = [];
|
const file_objs = [];
|
||||||
|
|
||||||
if (playlist && include_file_metadata) {
|
if (playlist && include_file_metadata) {
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
const uid = playlist['uids'][i];
|
const uid = playlist['uids'][i];
|
||||||
const file_obj = await db_api.getVideo(uid, uuid);
|
const file_obj = await files_api.getVideo(uid, uuid);
|
||||||
if (file_obj) file_objs.push(file_obj);
|
if (file_obj) file_objs.push(file_obj);
|
||||||
// TODO: remove file from playlist if could not be found
|
// TODO: remove file from playlist if could not be found
|
||||||
}
|
}
|
||||||
@@ -1354,7 +1284,6 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
|
|||||||
res.send({
|
res.send({
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
file_objs: file_objs,
|
file_objs: file_objs,
|
||||||
type: playlist && playlist.type,
|
|
||||||
success: !!playlist
|
success: !!playlist
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1365,7 +1294,7 @@ app.post('/api/getPlaylists', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
let playlists = await db_api.getRecords('playlists', {user_uid: uuid});
|
let playlists = await db_api.getRecords('playlists', {user_uid: uuid});
|
||||||
if (include_categories) {
|
if (include_categories) {
|
||||||
const categories = await categories_api.getCategoriesAsPlaylists(files);
|
const categories = await categories_api.getCategoriesAsPlaylists();
|
||||||
if (categories) {
|
if (categories) {
|
||||||
playlists = playlists.concat(categories);
|
playlists = playlists.concat(categories);
|
||||||
}
|
}
|
||||||
@@ -1384,7 +1313,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
playlist.uids.push(file_uid);
|
playlist.uids.push(file_uid);
|
||||||
|
|
||||||
let success = await db_api.updatePlaylist(playlist);
|
let success = await files_api.updatePlaylist(playlist);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1392,7 +1321,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
||||||
let playlist = req.body.playlist;
|
let playlist = req.body.playlist;
|
||||||
let success = await db_api.updatePlaylist(playlist, req.user && req.user.uid);
|
let success = await files_api.updatePlaylist(playlist, req.user && req.user.uid);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1420,10 +1349,9 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
||||||
const uid = req.body.uid;
|
const uid = req.body.uid;
|
||||||
const blacklistMode = req.body.blacklistMode;
|
const blacklistMode = req.body.blacklistMode;
|
||||||
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
|
||||||
|
|
||||||
let wasDeleted = false;
|
let wasDeleted = false;
|
||||||
wasDeleted = await db_api.deleteFile(uid, uuid, blacklistMode);
|
wasDeleted = await files_api.deleteFile(uid, blacklistMode);
|
||||||
res.send(wasDeleted);
|
res.send(wasDeleted);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1455,7 +1383,7 @@ app.post('/api/deleteAllFiles', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let wasDeleted = false;
|
let wasDeleted = false;
|
||||||
wasDeleted = await db_api.deleteFile(files[i].uid, uuid, blacklistMode);
|
wasDeleted = await files_api.deleteFile(files[i].uid, blacklistMode);
|
||||||
if (wasDeleted) {
|
if (wasDeleted) {
|
||||||
delete_count++;
|
delete_count++;
|
||||||
}
|
}
|
||||||
@@ -1481,10 +1409,10 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
|||||||
if (playlist_id) {
|
if (playlist_id) {
|
||||||
zip_file_generated = true;
|
zip_file_generated = true;
|
||||||
const playlist_files_to_download = [];
|
const playlist_files_to_download = [];
|
||||||
const playlist = await db_api.getPlaylist(playlist_id, uuid);
|
const playlist = await files_api.getPlaylist(playlist_id, uuid);
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
const playlist_file_uid = playlist['uids'][i];
|
const playlist_file_uid = playlist['uids'][i];
|
||||||
const file_obj = await db_api.getVideo(playlist_file_uid, uuid);
|
const file_obj = await files_api.getVideo(playlist_file_uid, uuid);
|
||||||
playlist_files_to_download.push(file_obj);
|
playlist_files_to_download.push(file_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1498,7 +1426,7 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
|||||||
// generate zip
|
// generate zip
|
||||||
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
|
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
|
||||||
} else {
|
} else {
|
||||||
const file_obj = await db_api.getVideo(uid, uuid, sub_id)
|
const file_obj = await files_api.getVideo(uid, uuid, sub_id)
|
||||||
file_path_to_download = file_obj.path;
|
file_path_to_download = file_obj.path;
|
||||||
}
|
}
|
||||||
if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download);
|
if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download);
|
||||||
@@ -1516,20 +1444,69 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/getArchives', optionalJwt, async (req, res) => {
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
const sub_id = req.body.sub_id;
|
||||||
|
const filter_obj = {user_uid: uuid, sub_id: sub_id};
|
||||||
|
const type = req.body.type;
|
||||||
|
|
||||||
|
// we do this for file types because if type is null, that means get files of all types
|
||||||
|
if (type) filter_obj['type'] = type;
|
||||||
|
|
||||||
|
const archives = await db_api.getRecords('archives', filter_obj);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
archives: archives
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/downloadArchive', optionalJwt, async (req, res) => {
|
app.post('/api/downloadArchive', optionalJwt, async (req, res) => {
|
||||||
let sub = req.body.sub;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
let archive_dir = sub.archive;
|
const sub_id = req.body.sub_id;
|
||||||
|
const type = req.body.type;
|
||||||
|
|
||||||
let full_archive_path = path.join(archive_dir, 'archive.txt');
|
const archive_text = await archive_api.generateArchive(type, uuid, sub_id);
|
||||||
|
|
||||||
if (await fs.pathExists(full_archive_path)) {
|
if (archive_text !== null && archive_text !== undefined) {
|
||||||
res.sendFile(full_archive_path);
|
res.setHeader('Content-type', "application/octet-stream");
|
||||||
|
res.setHeader('Content-disposition', 'attachment; filename=archive.txt');
|
||||||
|
res.send(archive_text);
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(404);
|
res.sendStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/importArchive', optionalJwt, async (req, res) => {
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
const archive = req.body.archive;
|
||||||
|
const sub_id = req.body.sub_id;
|
||||||
|
const type = req.body.type;
|
||||||
|
|
||||||
|
const archive_text = Buffer.from(archive.split(',')[1], 'base64').toString();
|
||||||
|
|
||||||
|
const imported_count = await archive_api.importArchiveFile(archive_text, type, uuid, sub_id);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
success: !!imported_count,
|
||||||
|
imported_count: imported_count
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/deleteArchiveItems', optionalJwt, async (req, res) => {
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
const archives = req.body.archives;
|
||||||
|
|
||||||
|
let success = true;
|
||||||
|
for (const archive of archives) {
|
||||||
|
success &= await archive_api.removeFromArchive(archive['extractor'], archive['id'], archive['type'], uuid, archive['sub_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var upload_multer = multer({ dest: __dirname + '/appdata/' });
|
var upload_multer = multer({ dest: __dirname + '/appdata/' });
|
||||||
app.post('/api/uploadCookies', upload_multer.single('cookies'), async (req, res) => {
|
app.post('/api/uploadCookies', upload_multer.single('cookies'), async (req, res) => {
|
||||||
const new_path = path.join(__dirname, 'appdata', 'cookies.txt');
|
const new_path = path.join(__dirname, 'appdata', 'cookies.txt');
|
||||||
@@ -1596,12 +1573,13 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
if (!multiUserMode || req.isAuthenticated() || req.can_watch) {
|
if (!multiUserMode || req.isAuthenticated() || req.can_watch) {
|
||||||
file_obj = await db_api.getVideo(uid, uuid, sub_id);
|
file_obj = await files_api.getVideo(uid, uuid, sub_id);
|
||||||
if (file_obj) file_path = file_obj['path'];
|
if (file_obj) file_path = file_obj['path'];
|
||||||
else file_path = null;
|
else file_path = null;
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(file_path)) {
|
if (!fs.existsSync(file_path)) {
|
||||||
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj.id}`);
|
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(file_path);
|
const stat = fs.statSync(file_path);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
@@ -1669,9 +1647,15 @@ app.post('/api/download', optionalJwt, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/clearFinishedDownloads', optionalJwt, async (req, res) => {
|
app.post('/api/clearDownloads', optionalJwt, async (req, res) => {
|
||||||
const user_uid = req.isAuthenticated() ? req.user.uid : null;
|
const user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
const success = db_api.removeAllRecords('download_queue', {finished: true, user_uid: user_uid});
|
const clear_finished = req.body.clear_finished;
|
||||||
|
const clear_paused = req.body.clear_paused;
|
||||||
|
const clear_errors = req.body.clear_errors;
|
||||||
|
let success = true;
|
||||||
|
if (clear_finished) success &= await db_api.removeAllRecords('download_queue', {finished: true, user_uid: user_uid});
|
||||||
|
if (clear_paused) success &= await db_api.removeAllRecords('download_queue', {paused: true, user_uid: user_uid});
|
||||||
|
if (clear_errors) success &= await db_api.removeAllRecords('download_queue', {error: {$ne: null}, user_uid: user_uid});
|
||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1715,8 +1699,8 @@ app.post('/api/resumeAllDownloads', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/restartDownload', optionalJwt, async (req, res) => {
|
app.post('/api/restartDownload', optionalJwt, async (req, res) => {
|
||||||
const download_uid = req.body.download_uid;
|
const download_uid = req.body.download_uid;
|
||||||
const success = await downloader_api.restartDownload(download_uid);
|
const new_download = await downloader_api.restartDownload(download_uid);
|
||||||
res.send({success: success});
|
res.send({success: !!new_download, new_download_uid: new_download ? new_download['uid'] : null});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
|
app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
|
||||||
@@ -1730,6 +1714,10 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
||||||
const tasks = await db_api.getRecords('tasks');
|
const tasks = await db_api.getRecords('tasks');
|
||||||
for (let task of tasks) {
|
for (let task of tasks) {
|
||||||
|
if (!tasks_api.TASKS[task['key']]) {
|
||||||
|
logger.verbose(`Task ${task['key']} does not exist!`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
||||||
}
|
}
|
||||||
res.send({tasks: tasks});
|
res.send({tasks: tasks});
|
||||||
@@ -1793,6 +1781,15 @@ app.post('/api/updateTaskData', optionalJwt, async (req, res) => {
|
|||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/updateTaskOptions', optionalJwt, async (req, res) => {
|
||||||
|
const task_key = req.body.task_key;
|
||||||
|
const new_options = req.body.new_options;
|
||||||
|
|
||||||
|
const success = await db_api.updateRecord('tasks', {key: task_key}, {options: new_options});
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/getDBBackups', optionalJwt, async (req, res) => {
|
app.post('/api/getDBBackups', optionalJwt, async (req, res) => {
|
||||||
const backup_dir = path.join('appdata', 'db_backup');
|
const backup_dir = path.join('appdata', 'db_backup');
|
||||||
fs.ensureDirSync(backup_dir);
|
fs.ensureDirSync(backup_dir);
|
||||||
@@ -1863,19 +1860,48 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getFileFormats', optionalJwt, async (req, res) => {
|
app.post('/api/getFileFormats', optionalJwt, async (req, res) => {
|
||||||
let url = req.body.url;
|
const url = req.body.url;
|
||||||
let result = await getUrlInfos(url);
|
const result = await downloader_api.getVideoInfoByURL(url);
|
||||||
res.send({
|
res.send({
|
||||||
result: result,
|
result: result && result.length === 1 ? result[0] : null,
|
||||||
success: !!result
|
success: result && result.length === 0
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// user authentication
|
// user authentication
|
||||||
|
|
||||||
app.post('/api/auth/register'
|
app.post('/api/auth/register', optionalJwt, async (req, res) => {
|
||||||
, optionalJwt
|
const userid = req.body.userid;
|
||||||
, auth_api.registerUser);
|
const username = req.body.username;
|
||||||
|
const plaintextPassword = req.body.password;
|
||||||
|
|
||||||
|
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
||||||
|
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintextPassword === "") {
|
||||||
|
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userid || !username) {
|
||||||
|
logger.error(`Registration failed for user ${userid}. Username or userid is invalid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
|
||||||
|
|
||||||
|
if (!new_user) {
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
user: new_user
|
||||||
|
});
|
||||||
|
});
|
||||||
app.post('/api/auth/login'
|
app.post('/api/auth/login'
|
||||||
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
||||||
, auth_api.generateJWT
|
, auth_api.generateJWT
|
||||||
@@ -1927,18 +1953,7 @@ app.post('/api/updateUser', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
||||||
let uid = req.body.uid;
|
let uid = req.body.uid;
|
||||||
try {
|
try {
|
||||||
let success = false;
|
const success = await auth_api.deleteUser(uid);
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
||||||
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
|
||||||
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
|
||||||
if (user_db_obj) {
|
|
||||||
// user exists, let's delete
|
|
||||||
await fs.remove(user_folder);
|
|
||||||
await db_api.removeRecord('users', {uid: uid});
|
|
||||||
success = true;
|
|
||||||
} else {
|
|
||||||
logger.error(`Could not find user with uid ${uid}`);
|
|
||||||
}
|
|
||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@@ -1976,6 +1991,112 @@ app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => {
|
|||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
|
||||||
|
app.post('/api/getNotifications', optionalJwt, async (req, res) => {
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const notifications = await db_api.getRecords('notifications', {user_uid: uuid});
|
||||||
|
|
||||||
|
res.send({notifications: notifications});
|
||||||
|
});
|
||||||
|
|
||||||
|
// set notifications to read
|
||||||
|
app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = await db_api.updateRecords('notifications', {user_uid: uuid}, {read: true});
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
|
||||||
|
const uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = await db_api.removeRecord('notifications', {uid: uid});
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = await db_api.removeAllRecords('notifications', {user_uid: uuid});
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/telegramRequest', async (req, res) => {
|
||||||
|
if (!req.body.message && !req.body.message.text) {
|
||||||
|
logger.error('Invalid Telegram request received!');
|
||||||
|
res.sendStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = req.body.message.text;
|
||||||
|
const regex_exp = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
|
||||||
|
const url_regex = new RegExp(regex_exp);
|
||||||
|
if (text.match(url_regex)) {
|
||||||
|
downloader_api.createDownload(text, 'video', {}, req.query.user_uid ? req.query.user_uid : null);
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
logger.error('Invalid Telegram request received! Make sure you only send a valid URL.');
|
||||||
|
notifications_api.sendTelegramNotification({title: 'Invalid Telegram Request', body: 'Make sure you only send a valid URL.', url: text});
|
||||||
|
res.sendStatus(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// rss feed
|
||||||
|
|
||||||
|
app.get('/api/rss', async function (req, res) {
|
||||||
|
if (!config_api.getConfigItem('ytdl_enable_rss_feed')) {
|
||||||
|
logger.error('RSS feed is disabled! It must be enabled in the settings before it can be generated.');
|
||||||
|
res.sendStatus(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// these are returned
|
||||||
|
const sort = req.query.sort ? JSON.parse(decodeURIComponent(req.query.sort)) : {by: 'registered', order: -1};
|
||||||
|
const range = req.query.range ? req.query.range.map(range_num => parseInt(range_num)) : null;
|
||||||
|
const text_search = req.query.text_search ? decodeURIComponent(req.query.text_search) : null;
|
||||||
|
const file_type_filter = req.query.file_type_filter;
|
||||||
|
const favorite_filter = req.query.favorite_filter === 'true';
|
||||||
|
const sub_id = req.query.sub_id ? decodeURIComponent(req.query.sub_id) : null;
|
||||||
|
const uuid = req.query.uuid ? decodeURIComponent(req.query.uuid) : null;
|
||||||
|
|
||||||
|
const {files} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
|
||||||
|
|
||||||
|
const feed = new Feed({
|
||||||
|
title: 'Downloads',
|
||||||
|
description: 'YoutubeDL-Material downloads',
|
||||||
|
id: utils.getBaseURL(),
|
||||||
|
link: utils.getBaseURL(),
|
||||||
|
image: 'https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/src/assets/images/logo_128px.png',
|
||||||
|
favicon: 'https://raw.githubusercontent.com/Tzahi12345/YoutubeDL-Material/master/src/favicon.ico',
|
||||||
|
generator: 'YoutubeDL-Material'
|
||||||
|
});
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
feed.addItem({
|
||||||
|
title: file.title,
|
||||||
|
link: `${utils.getBaseURL()}/#/player;uid=${file.uid}`,
|
||||||
|
description: file.description,
|
||||||
|
author: [
|
||||||
|
{
|
||||||
|
name: file.uploader,
|
||||||
|
link: file.url
|
||||||
|
}
|
||||||
|
],
|
||||||
|
contributor: [],
|
||||||
|
date: file.timestamp,
|
||||||
|
// https://stackoverflow.com/a/45415677/8088021
|
||||||
|
image: file.thumbnailURL.replace('&', '&')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res.send(feed.rss2());
|
||||||
|
});
|
||||||
|
|
||||||
|
// web server
|
||||||
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
//if the request is not html then move along
|
//if the request is not html then move along
|
||||||
var accept = req.accepts('html', 'json', 'xml');
|
var accept = req.accepts('html', 'json', 'xml');
|
||||||
@@ -1991,6 +2112,8 @@ app.use(function(req, res, next) {
|
|||||||
|
|
||||||
let index_path = path.join(__dirname, 'public', 'index.html');
|
let index_path = path.join(__dirname, 'public', 'index.html');
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
fs.createReadStream(index_path).pipe(res);
|
fs.createReadStream(index_path).pipe(res);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,12 @@
|
|||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"allow_autoplay": true,
|
"allow_autoplay": true,
|
||||||
"enable_downloads_manager": true,
|
"enable_downloads_manager": true,
|
||||||
"allow_playlist_categorization": true
|
"allow_playlist_categorization": true,
|
||||||
|
"force_autoplay": false,
|
||||||
|
"enable_notifications": true,
|
||||||
|
"enable_all_notifications": true,
|
||||||
|
"allowed_notification_types": [],
|
||||||
|
"enable_rss_feed": false
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_API_key": false,
|
"use_API_key": false,
|
||||||
@@ -31,10 +36,23 @@
|
|||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": "",
|
"youtube_API_key": "",
|
||||||
"use_twitch_API": false,
|
"use_twitch_API": false,
|
||||||
"twitch_API_key": "",
|
"twitch_client_ID": "",
|
||||||
|
"twitch_client_secret": "",
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false
|
"generate_NFO_files": false,
|
||||||
|
"use_ntfy_API": false,
|
||||||
|
"ntfy_topic_URL": "",
|
||||||
|
"use_gotify_API": false,
|
||||||
|
"gotify_server_URL": "",
|
||||||
|
"gotify_app_token": "",
|
||||||
|
"use_telegram_API": false,
|
||||||
|
"telegram_bot_token": "",
|
||||||
|
"telegram_chat_id": "",
|
||||||
|
"telegram_webhook_proxy": "",
|
||||||
|
"webhook_URL": "",
|
||||||
|
"discord_webhook_URL": "",
|
||||||
|
"slack_webhook_URL": ""
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
@@ -63,7 +81,7 @@
|
|||||||
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||||
},
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
"default_downloader": "youtube-dl",
|
"default_downloader": "yt-dlp",
|
||||||
"use_default_downloading_agent": true,
|
"use_default_downloading_agent": true,
|
||||||
"custom_downloading_agent": "",
|
"custom_downloading_agent": "",
|
||||||
"multi_user_mode": false,
|
"multi_user_mode": false,
|
||||||
|
|||||||
91
backend/archive.js
Normal file
91
backend/archive.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
|
const db_api = require('./db');
|
||||||
|
|
||||||
|
exports.generateArchive = async (type = null, user_uid = null, sub_id = null) => {
|
||||||
|
const filter = {user_uid: user_uid, sub_id: sub_id};
|
||||||
|
if (type) filter['type'] = type;
|
||||||
|
const archive_items = await db_api.getRecords('archives', filter);
|
||||||
|
const archive_item_lines = archive_items.map(archive_item => `${archive_item['extractor']} ${archive_item['id']}`);
|
||||||
|
return archive_item_lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addToArchive = async (extractor, id, type, title, user_uid = null, sub_id = null) => {
|
||||||
|
const archive_item = createArchiveItem(extractor, id, type, title, user_uid, sub_id);
|
||||||
|
const success = await db_api.insertRecordIntoTable('archives', archive_item, {extractor: extractor, id: id, type: type});
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.removeFromArchive = async (extractor, id, type, user_uid = null, sub_id = null) => {
|
||||||
|
const success = await db_api.removeAllRecords('archives', {extractor: extractor, id: id, type: type, user_uid: user_uid, sub_id: sub_id});
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.existsInArchive = async (extractor, id, type, user_uid, sub_id) => {
|
||||||
|
const archive_item = await db_api.getRecord('archives', {extractor: extractor, id: id, type: type, user_uid: user_uid, sub_id: sub_id});
|
||||||
|
return !!archive_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importArchiveFile = async (archive_text, type, user_uid = null, sub_id = null) => {
|
||||||
|
let archive_import_count = 0;
|
||||||
|
const lines = archive_text.split('\n');
|
||||||
|
for (let line of lines) {
|
||||||
|
const archive_line_parts = line.trim().split(' ');
|
||||||
|
// should just be the extractor and the video ID
|
||||||
|
if (archive_line_parts.length !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractor = archive_line_parts[0];
|
||||||
|
const id = archive_line_parts[1];
|
||||||
|
if (!extractor || !id) continue;
|
||||||
|
|
||||||
|
// we can't do a bulk write because we need to avoid duplicate archive items existing in db
|
||||||
|
|
||||||
|
const archive_item = createArchiveItem(extractor, id, type, null, user_uid, sub_id);
|
||||||
|
await db_api.insertRecordIntoTable('archives', archive_item, {extractor: extractor, id: id, type: type, sub_id: sub_id, user_uid: user_uid});
|
||||||
|
archive_import_count++;
|
||||||
|
}
|
||||||
|
return archive_import_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importArchives = async () => {
|
||||||
|
const imported_archives = [];
|
||||||
|
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||||
|
|
||||||
|
// run through check list and check each file to see if it's missing from the db
|
||||||
|
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||||
|
const dir_to_check = dirs_to_check[i];
|
||||||
|
if (!dir_to_check['archive_path']) continue;
|
||||||
|
|
||||||
|
const files_to_import = [
|
||||||
|
path.join(dir_to_check['archive_path'], `archive_${dir_to_check['type']}.txt`),
|
||||||
|
path.join(dir_to_check['archive_path'], `blacklist_${dir_to_check['type']}.txt`)
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const file_to_import of files_to_import) {
|
||||||
|
const file_exists = await fs.pathExists(file_to_import);
|
||||||
|
if (!file_exists) continue;
|
||||||
|
|
||||||
|
const archive_text = await fs.readFile(file_to_import, 'utf8');
|
||||||
|
await exports.importArchiveFile(archive_text, dir_to_check.type, dir_to_check.user_uid, dir_to_check.sub_id);
|
||||||
|
imported_archives.push(file_to_import);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imported_archives;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createArchiveItem = (extractor, id, type, title = null, user_uid = null, sub_id = null) => {
|
||||||
|
return {
|
||||||
|
extractor: extractor,
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
title: title,
|
||||||
|
user_uid: user_uid ? user_uid : null,
|
||||||
|
sub_id: sub_id ? sub_id : null,
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
uid: uuid()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
const config_api = require('../config');
|
const config_api = require('../config');
|
||||||
const consts = require('../consts');
|
const CONSTS = require('../consts');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const db_api = require('../db');
|
const db_api = require('../db');
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
var LocalStrategy = require('passport-local').Strategy;
|
var LocalStrategy = require('passport-local').Strategy;
|
||||||
var LdapStrategy = require('passport-ldapauth');
|
var LdapStrategy = require('passport-ldapauth');
|
||||||
@@ -16,7 +18,7 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
|||||||
let SERVER_SECRET = null;
|
let SERVER_SECRET = null;
|
||||||
let JWT_EXPIRATION = null;
|
let JWT_EXPIRATION = null;
|
||||||
let opts = null;
|
let opts = null;
|
||||||
let saltRounds = null;
|
let saltRounds = 10;
|
||||||
|
|
||||||
exports.initialize = function () {
|
exports.initialize = function () {
|
||||||
/*************************
|
/*************************
|
||||||
@@ -31,9 +33,14 @@ exports.initialize = function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saltRounds = 10;
|
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
|
||||||
|
|
||||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||||
|
if (!(+JWT_EXPIRATION)) {
|
||||||
|
logger.warn(`JWT expiration value improperly set to ${JWT_EXPIRATION}, auto setting to 1 day.`);
|
||||||
|
JWT_EXPIRATION = 86400;
|
||||||
|
} else {
|
||||||
|
JWT_EXPIRATION = +JWT_EXPIRATION;
|
||||||
|
}
|
||||||
|
|
||||||
SERVER_SECRET = null;
|
SERVER_SECRET = null;
|
||||||
if (db_api.users_db.get('jwt_secret').value()) {
|
if (db_api.users_db.get('jwt_secret').value()) {
|
||||||
@@ -61,14 +68,7 @@ exports.initialize = function () {
|
|||||||
const setupRoles = async () => {
|
const setupRoles = async () => {
|
||||||
const required_roles = {
|
const required_roles = {
|
||||||
admin: {
|
admin: {
|
||||||
permissions: [
|
permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||||
'filemanager',
|
|
||||||
'settings',
|
|
||||||
'subscriptions',
|
|
||||||
'sharing',
|
|
||||||
'advanced_download',
|
|
||||||
'downloads_manager'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
permissions: [
|
permissions: [
|
||||||
@@ -106,55 +106,41 @@ exports.passport.deserializeUser(function(user, done) {
|
|||||||
/***************************************
|
/***************************************
|
||||||
* Register user with hashed password
|
* Register user with hashed password
|
||||||
**************************************/
|
**************************************/
|
||||||
exports.registerUser = async function(req, res) {
|
|
||||||
var userid = req.body.userid;
|
|
||||||
var username = req.body.username;
|
|
||||||
var plaintextPassword = req.body.password;
|
|
||||||
|
|
||||||
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
exports.registerUser = async (userid, username, plaintextPassword) => {
|
||||||
res.sendStatus(409);
|
const hash = await bcrypt.hash(plaintextPassword, saltRounds);
|
||||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
const new_user = generateUserObject(userid, username, hash);
|
||||||
return;
|
// check if user exists
|
||||||
|
if (await db_api.getRecord('users', {uid: userid})) {
|
||||||
|
// user id is taken!
|
||||||
|
logger.error('Registration failed: UID is already taken!');
|
||||||
|
return null;
|
||||||
|
} else if (await db_api.getRecord('users', {name: username})) {
|
||||||
|
// user name is taken!
|
||||||
|
logger.error('Registration failed: User name is already taken!');
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// add to db
|
||||||
|
await db_api.insertRecordIntoTable('users', new_user);
|
||||||
|
logger.verbose(`New user created: ${new_user.name}`);
|
||||||
|
return new_user;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (plaintextPassword === "") {
|
exports.deleteUser = async (uid) => {
|
||||||
res.sendStatus(400);
|
let success = false;
|
||||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
return;
|
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
||||||
|
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
||||||
|
if (user_db_obj) {
|
||||||
|
// user exists, let's delete
|
||||||
|
await fs.remove(user_folder);
|
||||||
|
await db_api.removeRecord('users', {uid: uid});
|
||||||
|
success = true;
|
||||||
|
} else {
|
||||||
|
logger.error(`Could not find user with uid ${uid}`);
|
||||||
}
|
}
|
||||||
|
return success;
|
||||||
bcrypt.hash(plaintextPassword, saltRounds)
|
|
||||||
.then(async function(hash) {
|
|
||||||
let new_user = generateUserObject(userid, username, hash);
|
|
||||||
// check if user exists
|
|
||||||
if (await db_api.getRecord('users', {uid: userid})) {
|
|
||||||
// user id is taken!
|
|
||||||
logger.error('Registration failed: UID is already taken!');
|
|
||||||
res.status(409).send('UID is already taken!');
|
|
||||||
} else if (await db_api.getRecord('users', {name: username})) {
|
|
||||||
// user name is taken!
|
|
||||||
logger.error('Registration failed: User name is already taken!');
|
|
||||||
res.status(409).send('User name is already taken!');
|
|
||||||
} else {
|
|
||||||
// add to db
|
|
||||||
await db_api.insertRecordIntoTable('users', new_user);
|
|
||||||
logger.verbose(`New user created: ${new_user.name}`);
|
|
||||||
res.send({
|
|
||||||
user: new_user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(result) {
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
logger.error(err);
|
|
||||||
if( err.code == 'ER_DUP_ENTRY' ) {
|
|
||||||
res.status(409).send('UserId already taken');
|
|
||||||
} else {
|
|
||||||
res.sendStatus(409);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************
|
/***************************************
|
||||||
@@ -171,8 +157,12 @@ exports.registerUser = async function(req, res) {
|
|||||||
|
|
||||||
|
|
||||||
exports.login = async (username, password) => {
|
exports.login = async (username, password) => {
|
||||||
|
// even if we're using LDAP, we still want users to be able to login using internal credentials
|
||||||
const user = await db_api.getRecord('users', {name: username});
|
const user = await db_api.getRecord('users', {name: username});
|
||||||
if (!user) { logger.error(`User ${username} not found`); return false }
|
if (!user) {
|
||||||
|
if (config_api.getConfigItem('ytdl_auth_method') === 'internal') logger.error(`User ${username} not found`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (user.auth_method && user.auth_method !== 'internal') { return false }
|
if (user.auth_method && user.auth_method !== 'internal') { return false }
|
||||||
return await bcrypt.compare(password, user.passhash) ? user : false;
|
return await bcrypt.compare(password, user.passhash) ? user : false;
|
||||||
}
|
}
|
||||||
@@ -231,7 +221,7 @@ exports.returnAuthResponse = async function(req, res) {
|
|||||||
user: req.user,
|
user: req.user,
|
||||||
token: req.token,
|
token: req.token,
|
||||||
permissions: await exports.userPermissions(req.user.uid),
|
permissions: await exports.userPermissions(req.user.uid),
|
||||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
available_permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +305,7 @@ exports.getUserVideos = async function(user_uid, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
||||||
let file = await db_api.getRecord('files', {file_uid: file_uid});
|
let file = await db_api.getRecord('files', {uid: file_uid});
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
// prevent unauthorized users from accessing the file info
|
||||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||||
@@ -357,7 +347,6 @@ exports.userHasPermission = async function(user_uid, permission) {
|
|||||||
logger.error('Invalid role ' + role);
|
logger.error('Invalid role ' + role);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const role_permissions = (await db_api.getRecords('roles'))['permissions'];
|
|
||||||
|
|
||||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||||
@@ -372,7 +361,8 @@ exports.userHasPermission = async function(user_uid, permission) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// no overrides, let's check if the role has the permission
|
// no overrides, let's check if the role has the permission
|
||||||
if (role_permissions.includes(permission)) {
|
const role_has_permission = await exports.roleHasPermissions(role, permission);
|
||||||
|
if (role_has_permission) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
|
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
|
||||||
@@ -380,6 +370,16 @@ exports.userHasPermission = async function(user_uid, permission) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.roleHasPermissions = async function(role, permission) {
|
||||||
|
const role_obj = await db_api.getRecord('roles', {key: role})
|
||||||
|
if (!role) {
|
||||||
|
logger.error(`Role ${role} does not exist!`);
|
||||||
|
}
|
||||||
|
const role_permissions = role_obj['permissions'];
|
||||||
|
if (role_permissions && role_permissions.includes(permission)) return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
exports.userPermissions = async function(user_uid) {
|
exports.userPermissions = async function(user_uid) {
|
||||||
let user_permissions = [];
|
let user_permissions = [];
|
||||||
const user_obj = await db_api.getRecord('users', ({uid: user_uid}));
|
const user_obj = await db_api.getRecord('users', ({uid: user_uid}));
|
||||||
@@ -392,8 +392,8 @@ exports.userPermissions = async function(user_uid) {
|
|||||||
const role_obj = await db_api.getRecord('roles', {key: role});
|
const role_obj = await db_api.getRecord('roles', {key: role});
|
||||||
const role_permissions = role_obj['permissions'];
|
const role_permissions = role_obj['permissions'];
|
||||||
|
|
||||||
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
for (let i = 0; i < CONSTS.AVAILABLE_PERMISSIONS.length; i++) {
|
||||||
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
let permission = CONSTS.AVAILABLE_PERMISSIONS[i];
|
||||||
|
|
||||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ async function categorize(file_jsons) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < file_jsons.length; i++) {
|
for (const file_json of file_jsons) {
|
||||||
const file_json = file_jsons[i];
|
for (const category of categories) {
|
||||||
for (let j = 0; j < categories.length; j++) {
|
|
||||||
const category = categories[j];
|
|
||||||
const rules = category['rules'];
|
const rules = category['rules'];
|
||||||
|
|
||||||
// if rules for current category apply, then that is the selected category
|
// if rules for current category apply, then that is the selected category
|
||||||
@@ -55,17 +53,18 @@ async function getCategories() {
|
|||||||
return categories ? categories : null;
|
return categories ? categories : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCategoriesAsPlaylists(files = null) {
|
async function getCategoriesAsPlaylists() {
|
||||||
const categories_as_playlists = [];
|
const categories_as_playlists = [];
|
||||||
const available_categories = await getCategories();
|
const available_categories = await getCategories();
|
||||||
if (available_categories && files) {
|
if (available_categories) {
|
||||||
for (let category of available_categories) {
|
for (let category of available_categories) {
|
||||||
const files_that_match = utils.addUIDsToCategory(category, files);
|
const files_that_match = await db_api.getRecords('files', {'category.uid': category['uid']});
|
||||||
if (files_that_match && files_that_match.length > 0) {
|
if (files_that_match && files_that_match.length > 0) {
|
||||||
category['thumbnailURL'] = files_that_match[0].thumbnailURL;
|
category['thumbnailURL'] = files_that_match[0].thumbnailURL;
|
||||||
category['thumbnailPath'] = files_that_match[0].thumbnailPath;
|
category['thumbnailPath'] = files_that_match[0].thumbnailPath;
|
||||||
category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
||||||
category['id'] = category['uid'];
|
category['id'] = category['uid'];
|
||||||
|
category['auto'] = true;
|
||||||
categories_as_playlists.push(category);
|
categories_as_playlists.push(category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { BehaviorSubject } = require('rxjs');
|
||||||
|
|
||||||
|
exports.CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||||
|
exports.descriptors = {}; // to get rid of file locks when needed, TODO: move to youtube-dl.js
|
||||||
|
|
||||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
||||||
|
exports.config_updated = new BehaviorSubject();
|
||||||
|
|
||||||
function initialize() {
|
exports.initialize = () => {
|
||||||
ensureConfigFileExists();
|
ensureConfigFileExists();
|
||||||
ensureConfigItemsExist();
|
ensureConfigItemsExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureConfigItemsExist() {
|
function ensureConfigItemsExist() {
|
||||||
const config_keys = Object.keys(CONFIG_ITEMS);
|
const config_keys = Object.keys(exports.CONFIG_ITEMS);
|
||||||
for (let i = 0; i < config_keys.length; i++) {
|
for (let i = 0; i < config_keys.length; i++) {
|
||||||
const config_key = config_keys[i];
|
const config_key = config_keys[i];
|
||||||
getConfigItem(config_key);
|
exports.getConfigItem(config_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,17 +61,17 @@ function getElementNameInConfig(path) {
|
|||||||
/**
|
/**
|
||||||
* Check if config exists. If not, write default config to config path
|
* Check if config exists. If not, write default config to config path
|
||||||
*/
|
*/
|
||||||
function configExistsCheck() {
|
exports.configExistsCheck = () => {
|
||||||
let exists = fs.existsSync(configPath);
|
let exists = fs.existsSync(configPath);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
setConfigFile(DEFAULT_CONFIG);
|
exports.setConfigFile(DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gets config file and returns as a json
|
* Gets config file and returns as a json
|
||||||
*/
|
*/
|
||||||
function getConfigFile() {
|
exports.getConfigFile = () => {
|
||||||
try {
|
try {
|
||||||
let raw_data = fs.readFileSync(configPath);
|
let raw_data = fs.readFileSync(configPath);
|
||||||
let parsed_data = JSON.parse(raw_data);
|
let parsed_data = JSON.parse(raw_data);
|
||||||
@@ -78,35 +82,40 @@ function getConfigFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigFile(config) {
|
exports.setConfigFile = (config) => {
|
||||||
try {
|
try {
|
||||||
|
const old_config = exports.getConfigFile();
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
const changes = exports.findChangedConfigItems(old_config, config);
|
||||||
|
if (changes.length > 0) {
|
||||||
|
for (const change of changes) exports.config_updated.next(change);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigItem(key) {
|
exports.getConfigItem = (key) => {
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
if (!CONFIG_ITEMS[key]) {
|
if (!exports.CONFIG_ITEMS[key]) {
|
||||||
logger.error(`Config item with key '${key}' is not recognized.`);
|
logger.error(`Config item with key '${key}' is not recognized.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
const val = Object.byString(config_json, path);
|
const val = Object.byString(config_json, path);
|
||||||
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
||||||
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
||||||
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
exports.setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
||||||
return Object.byString(DEFAULT_CONFIG, path);
|
return Object.byString(DEFAULT_CONFIG, path);
|
||||||
}
|
}
|
||||||
return Object.byString(config_json, path);
|
return Object.byString(config_json, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigItem(key, value) {
|
exports.setConfigItem = (key, value) => {
|
||||||
let success = false;
|
let success = false;
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
let element_name = getElementNameInConfig(path);
|
let element_name = getElementNameInConfig(path);
|
||||||
let parent_path = getParentPath(path);
|
let parent_path = getParentPath(path);
|
||||||
let parent_object = Object.byString(config_json, parent_path);
|
let parent_object = Object.byString(config_json, parent_path);
|
||||||
@@ -118,20 +127,18 @@ function setConfigItem(key, value) {
|
|||||||
parent_parent_object[parent_parent_single_key] = {};
|
parent_parent_object[parent_parent_single_key] = {};
|
||||||
parent_object = Object.byString(config_json, parent_path);
|
parent_object = Object.byString(config_json, parent_path);
|
||||||
}
|
}
|
||||||
|
if (value === 'false') value = false;
|
||||||
|
if (value === 'true') value = true;
|
||||||
|
parent_object[element_name] = value;
|
||||||
|
|
||||||
if (value === 'false' || value === 'true') {
|
success = exports.setConfigFile(config_json);
|
||||||
parent_object[element_name] = (value === 'true');
|
|
||||||
} else {
|
|
||||||
parent_object[element_name] = value;
|
|
||||||
}
|
|
||||||
success = setConfigFile(config_json);
|
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
};
|
}
|
||||||
|
|
||||||
function setConfigItems(items) {
|
exports.setConfigItems = (items) => {
|
||||||
let success = false;
|
let success = false;
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
let key = items[i].key;
|
let key = items[i].key;
|
||||||
let value = items[i].value;
|
let value = items[i].value;
|
||||||
@@ -141,7 +148,7 @@ function setConfigItems(items) {
|
|||||||
value = (value === 'true');
|
value = (value === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
let item_path = CONFIG_ITEMS[key]['path'];
|
let item_path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
let item_parent_path = getParentPath(item_path);
|
let item_parent_path = getParentPath(item_path);
|
||||||
let item_element_name = getElementNameInConfig(item_path);
|
let item_element_name = getElementNameInConfig(item_path);
|
||||||
|
|
||||||
@@ -149,28 +156,41 @@ function setConfigItems(items) {
|
|||||||
item_parent_object[item_element_name] = value;
|
item_parent_object[item_element_name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
success = setConfigFile(config_json);
|
success = exports.setConfigFile(config_json);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
function globalArgsRequiresSafeDownload() {
|
exports.globalArgsRequiresSafeDownload = () => {
|
||||||
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
const globalArgs = exports.getConfigItem('ytdl_custom_args').split(',,');
|
||||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
||||||
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||||
return failedArgs && failedArgs.length > 0;
|
return failedArgs && failedArgs.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
exports.findChangedConfigItems = (old_config, new_config, path = '', changedConfigItems = [], depth = 0) => {
|
||||||
getConfigItem: getConfigItem,
|
if (typeof old_config === 'object' && typeof new_config === 'object' && depth < 3) {
|
||||||
setConfigItem: setConfigItem,
|
for (const key in old_config) {
|
||||||
setConfigItems: setConfigItems,
|
if (Object.prototype.hasOwnProperty.call(new_config, key)) {
|
||||||
getConfigFile: getConfigFile,
|
exports.findChangedConfigItems(old_config[key], new_config[key], `${path}${path ? '.' : ''}${key}`, changedConfigItems, depth + 1);
|
||||||
setConfigFile: setConfigFile,
|
}
|
||||||
configExistsCheck: configExistsCheck,
|
}
|
||||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
} else {
|
||||||
initialize: initialize,
|
if (JSON.stringify(old_config) !== JSON.stringify(new_config)) {
|
||||||
descriptors: {},
|
const key = getConfigItemKeyByPath(path);
|
||||||
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
changedConfigItems.push({
|
||||||
|
key: key ? key : path.split('.')[path.split('.').length - 1], // return key in CONFIG_ITEMS or the object key
|
||||||
|
old_value: JSON.parse(JSON.stringify(old_config)),
|
||||||
|
new_value: JSON.parse(JSON.stringify(new_config))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changedConfigItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigItemKeyByPath(path) {
|
||||||
|
const found_item = Object.values(exports.CONFIG_ITEMS).find(item => item.path === path);
|
||||||
|
if (found_item) return found_item['key'];
|
||||||
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
@@ -185,7 +205,6 @@ const DEFAULT_CONFIG = {
|
|||||||
"default_file_output": "",
|
"default_file_output": "",
|
||||||
"use_youtubedl_archive": false,
|
"use_youtubedl_archive": false,
|
||||||
"custom_args": "",
|
"custom_args": "",
|
||||||
"safe_download_override": false,
|
|
||||||
"include_thumbnail": true,
|
"include_thumbnail": true,
|
||||||
"include_metadata": true,
|
"include_metadata": true,
|
||||||
"max_concurrent_downloads": 5,
|
"max_concurrent_downloads": 5,
|
||||||
@@ -196,20 +215,34 @@ const DEFAULT_CONFIG = {
|
|||||||
"file_manager_enabled": true,
|
"file_manager_enabled": true,
|
||||||
"allow_quality_select": true,
|
"allow_quality_select": true,
|
||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"allow_autoplay": true,
|
"force_autoplay": false,
|
||||||
"enable_downloads_manager": true,
|
"enable_downloads_manager": true,
|
||||||
"allow_playlist_categorization": true
|
"allow_playlist_categorization": true,
|
||||||
|
"enable_notifications": true,
|
||||||
|
"enable_all_notifications": true,
|
||||||
|
"allowed_notification_types": [],
|
||||||
|
"enable_rss_feed": false,
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_API_key": false,
|
"use_API_key": false,
|
||||||
"API_key": "",
|
"API_key": "",
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": "",
|
"youtube_API_key": "",
|
||||||
"use_twitch_API": false,
|
|
||||||
"twitch_API_key": "",
|
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false
|
"generate_NFO_files": false,
|
||||||
|
"use_ntfy_API": false,
|
||||||
|
"ntfy_topic_URL": "",
|
||||||
|
"use_gotify_API": false,
|
||||||
|
"gotify_server_URL": "",
|
||||||
|
"gotify_app_token": "",
|
||||||
|
"use_telegram_API": false,
|
||||||
|
"telegram_bot_token": "",
|
||||||
|
"telegram_chat_id": "",
|
||||||
|
"telegram_webhook_proxy": "",
|
||||||
|
"webhook_URL": "",
|
||||||
|
"discord_webhook_URL": "",
|
||||||
|
"slack_webhook_URL": "",
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
@@ -238,7 +271,7 @@ const DEFAULT_CONFIG = {
|
|||||||
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||||
},
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
"default_downloader": "youtube-dl",
|
"default_downloader": "yt-dlp",
|
||||||
"use_default_downloading_agent": true,
|
"use_default_downloading_agent": true,
|
||||||
"custom_downloading_agent": "",
|
"custom_downloading_agent": "",
|
||||||
"multi_user_mode": false,
|
"multi_user_mode": false,
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_custom_args',
|
'key': 'ytdl_custom_args',
|
||||||
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
||||||
},
|
},
|
||||||
'ytdl_safe_download_override': {
|
|
||||||
'key': 'ytdl_safe_download_override',
|
|
||||||
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
|
|
||||||
},
|
|
||||||
'ytdl_include_thumbnail': {
|
'ytdl_include_thumbnail': {
|
||||||
'key': 'ytdl_include_thumbnail',
|
'key': 'ytdl_include_thumbnail',
|
||||||
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
|
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
|
||||||
@@ -68,9 +64,9 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_download_only_mode',
|
'key': 'ytdl_download_only_mode',
|
||||||
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
|
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
|
||||||
},
|
},
|
||||||
'ytdl_allow_autoplay': {
|
'ytdl_force_autoplay': {
|
||||||
'key': 'ytdl_allow_autoplay',
|
'key': 'ytdl_force_autoplay',
|
||||||
'path': 'YoutubeDLMaterial.Extra.allow_autoplay'
|
'path': 'YoutubeDLMaterial.Extra.force_autoplay'
|
||||||
},
|
},
|
||||||
'ytdl_enable_downloads_manager': {
|
'ytdl_enable_downloads_manager': {
|
||||||
'key': 'ytdl_enable_downloads_manager',
|
'key': 'ytdl_enable_downloads_manager',
|
||||||
@@ -80,6 +76,22 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_allow_playlist_categorization',
|
'key': 'ytdl_allow_playlist_categorization',
|
||||||
'path': 'YoutubeDLMaterial.Extra.allow_playlist_categorization'
|
'path': 'YoutubeDLMaterial.Extra.allow_playlist_categorization'
|
||||||
},
|
},
|
||||||
|
'ytdl_enable_notifications': {
|
||||||
|
'key': 'ytdl_enable_notifications',
|
||||||
|
'path': 'YoutubeDLMaterial.Extra.enable_notifications'
|
||||||
|
},
|
||||||
|
'ytdl_enable_all_notifications': {
|
||||||
|
'key': 'ytdl_enable_all_notifications',
|
||||||
|
'path': 'YoutubeDLMaterial.Extra.enable_all_notifications'
|
||||||
|
},
|
||||||
|
'ytdl_allowed_notification_types': {
|
||||||
|
'key': 'ytdl_allowed_notification_types',
|
||||||
|
'path': 'YoutubeDLMaterial.Extra.allowed_notification_types'
|
||||||
|
},
|
||||||
|
'ytdl_enable_rss_feed': {
|
||||||
|
'key': 'ytdl_enable_rss_feed',
|
||||||
|
'path': 'YoutubeDLMaterial.Extra.enable_rss_feed'
|
||||||
|
},
|
||||||
|
|
||||||
// API
|
// API
|
||||||
'ytdl_use_api_key': {
|
'ytdl_use_api_key': {
|
||||||
@@ -98,14 +110,6 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_youtube_api_key',
|
'key': 'ytdl_youtube_api_key',
|
||||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||||
},
|
},
|
||||||
'ytdl_use_twitch_api': {
|
|
||||||
'key': 'ytdl_use_twitch_api',
|
|
||||||
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_api_key': {
|
|
||||||
'key': 'ytdl_twitch_api_key',
|
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_API_key'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_auto_download_chat': {
|
'ytdl_twitch_auto_download_chat': {
|
||||||
'key': 'ytdl_twitch_auto_download_chat',
|
'key': 'ytdl_twitch_auto_download_chat',
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||||
@@ -118,6 +122,54 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_generate_nfo_files',
|
'key': 'ytdl_generate_nfo_files',
|
||||||
'path': 'YoutubeDLMaterial.API.generate_NFO_files'
|
'path': 'YoutubeDLMaterial.API.generate_NFO_files'
|
||||||
},
|
},
|
||||||
|
'ytdl_use_ntfy_API': {
|
||||||
|
'key': 'ytdl_use_ntfy_API',
|
||||||
|
'path': 'YoutubeDLMaterial.API.use_ntfy_API'
|
||||||
|
},
|
||||||
|
'ytdl_ntfy_topic_url': {
|
||||||
|
'key': 'ytdl_ntfy_topic_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.ntfy_topic_URL'
|
||||||
|
},
|
||||||
|
'ytdl_use_gotify_API': {
|
||||||
|
'key': 'ytdl_use_gotify_API',
|
||||||
|
'path': 'YoutubeDLMaterial.API.use_gotify_API'
|
||||||
|
},
|
||||||
|
'ytdl_gotify_server_url': {
|
||||||
|
'key': 'ytdl_gotify_server_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.gotify_server_URL'
|
||||||
|
},
|
||||||
|
'ytdl_gotify_app_token': {
|
||||||
|
'key': 'ytdl_gotify_app_token',
|
||||||
|
'path': 'YoutubeDLMaterial.API.gotify_app_token'
|
||||||
|
},
|
||||||
|
'ytdl_use_telegram_API': {
|
||||||
|
'key': 'ytdl_use_telegram_API',
|
||||||
|
'path': 'YoutubeDLMaterial.API.use_telegram_API'
|
||||||
|
},
|
||||||
|
'ytdl_telegram_bot_token': {
|
||||||
|
'key': 'ytdl_telegram_bot_token',
|
||||||
|
'path': 'YoutubeDLMaterial.API.telegram_bot_token'
|
||||||
|
},
|
||||||
|
'ytdl_telegram_chat_id': {
|
||||||
|
'key': 'ytdl_telegram_chat_id',
|
||||||
|
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
|
||||||
|
},
|
||||||
|
'ytdl_telegram_webhook_proxy': {
|
||||||
|
'key': 'ytdl_telegram_webhook_proxy',
|
||||||
|
'path': 'YoutubeDLMaterial.API.telegram_webhook_proxy'
|
||||||
|
},
|
||||||
|
'ytdl_webhook_url': {
|
||||||
|
'key': 'ytdl_webhook_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
||||||
|
},
|
||||||
|
'ytdl_discord_webhook_url': {
|
||||||
|
'key': 'ytdl_discord_webhook_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
|
||||||
|
},
|
||||||
|
'ytdl_slack_webhook_url': {
|
||||||
|
'key': 'ytdl_slack_webhook_url',
|
||||||
|
'path': 'YoutubeDLMaterial.API.slack_webhook_URL'
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
@@ -217,10 +269,12 @@ exports.AVAILABLE_PERMISSIONS = [
|
|||||||
'subscriptions',
|
'subscriptions',
|
||||||
'sharing',
|
'sharing',
|
||||||
'advanced_download',
|
'advanced_download',
|
||||||
'downloads_manager'
|
'downloads_manager',
|
||||||
|
'tasks_manager'
|
||||||
];
|
];
|
||||||
|
|
||||||
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
|
exports.DETAILS_BIN_PATH = 'appdata/youtube-dl.json'
|
||||||
|
exports.OUTDATED_YOUTUBEDL_VERSION = "2020.00.00";
|
||||||
|
|
||||||
// args that have a value after it (e.g. -o <output> or -f <format>)
|
// args that have a value after it (e.g. -o <output> or -f <format>)
|
||||||
const YTDL_ARGS_WITH_VALUES = [
|
const YTDL_ARGS_WITH_VALUES = [
|
||||||
@@ -298,7 +352,11 @@ const YTDL_ARGS_WITH_VALUES = [
|
|||||||
'--convert-subs'
|
'--convert-subs'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
|
||||||
|
|
||||||
// we're using a Set here for performance
|
// we're using a Set here for performance
|
||||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||||
|
|
||||||
exports.CURRENT_VERSION = 'v4.2';
|
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
||||||
|
|
||||||
|
exports.CURRENT_VERSION = 'v4.3.2';
|
||||||
|
|||||||
475
backend/db.js
475
backend/db.js
@@ -1,18 +1,17 @@
|
|||||||
var fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
const { MongoClient } = require("mongodb");
|
const { MongoClient } = require("mongodb");
|
||||||
const { uuid } = require('uuidv4');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
var utils = require('./utils')
|
const utils = require('./utils')
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
|
||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const FileSync = require('lowdb/adapters/FileSync');
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
const { BehaviorSubject } = require('rxjs');
|
const { BehaviorSubject } = require('rxjs');
|
||||||
const local_adapter = new FileSync('./appdata/local_db.json');
|
|
||||||
const local_db = low(local_adapter);
|
|
||||||
|
|
||||||
|
let local_db = null;
|
||||||
let database = null;
|
let database = null;
|
||||||
exports.database_initialized = false;
|
exports.database_initialized = false;
|
||||||
exports.database_initialized_bs = new BehaviorSubject(false);
|
exports.database_initialized_bs = new BehaviorSubject(false);
|
||||||
@@ -58,6 +57,13 @@ const tables = {
|
|||||||
name: 'tasks',
|
name: 'tasks',
|
||||||
primary_key: 'key'
|
primary_key: 'key'
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
name: 'notifications',
|
||||||
|
primary_key: 'uid'
|
||||||
|
},
|
||||||
|
archives: {
|
||||||
|
name: 'archives'
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
name: 'test'
|
name: 'test'
|
||||||
}
|
}
|
||||||
@@ -65,10 +71,6 @@ const tables = {
|
|||||||
|
|
||||||
const tables_list = Object.keys(tables);
|
const tables_list = Object.keys(tables);
|
||||||
|
|
||||||
const local_db_defaults = {}
|
|
||||||
tables_list.forEach(table => {local_db_defaults[table] = []});
|
|
||||||
local_db.defaults(local_db_defaults).write();
|
|
||||||
|
|
||||||
let using_local_db = null;
|
let using_local_db = null;
|
||||||
|
|
||||||
function setDB(input_db, input_users_db) {
|
function setDB(input_db, input_users_db) {
|
||||||
@@ -77,11 +79,18 @@ function setDB(input_db, input_users_db) {
|
|||||||
exports.users_db = input_users_db
|
exports.users_db = input_users_db
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.initialize = (input_db, input_users_db) => {
|
exports.initialize = (input_db, input_users_db, db_name = 'local_db.json') => {
|
||||||
setDB(input_db, input_users_db);
|
setDB(input_db, input_users_db);
|
||||||
|
|
||||||
// must be done here to prevent getConfigItem from being called before init
|
// must be done here to prevent getConfigItem from being called before init
|
||||||
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
||||||
|
|
||||||
|
const local_adapter = new FileSync(`./appdata/${db_name}`);
|
||||||
|
local_db = low(local_adapter);
|
||||||
|
|
||||||
|
const local_db_defaults = {}
|
||||||
|
tables_list.forEach(table => {local_db_defaults[table] = []});
|
||||||
|
local_db.defaults(local_db_defaults).write();
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
||||||
@@ -148,6 +157,7 @@ exports._connectToDB = async (custom_connection_string = null) => {
|
|||||||
await database.collection(table).createIndex(text_search);
|
await database.collection(table).createIndex(text_search);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
using_local_db = false; // needs to happen for tests (in normal operation using_local_db is guaranteed false)
|
||||||
return true;
|
return true;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@@ -158,82 +168,9 @@ exports._connectToDB = async (custom_connection_string = null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
exports.setVideoProperty = async (file_uid, assignment_obj) => {
|
||||||
if (!file_object) file_object = generateFileObject(file_path, type);
|
// TODO: check if video exists, throw error if not
|
||||||
if (!file_object) {
|
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
|
||||||
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.fixVideoMetadataPerms(file_path, type);
|
|
||||||
|
|
||||||
// add thumbnail path
|
|
||||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
|
|
||||||
|
|
||||||
// if category exists, only include essential info
|
|
||||||
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
|
||||||
|
|
||||||
// modify duration
|
|
||||||
if (cropFileSettings) {
|
|
||||||
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user_uid) file_object['user_uid'] = user_uid;
|
|
||||||
if (sub_id) file_object['sub_id'] = sub_id;
|
|
||||||
|
|
||||||
const file_obj = await registerFileDBManual(file_object);
|
|
||||||
|
|
||||||
// remove metadata JSON if needed
|
|
||||||
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
|
||||||
utils.deleteJSONFile(file_path, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return file_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerFileDBManual(file_object) {
|
|
||||||
// add additional info
|
|
||||||
file_object['uid'] = uuid();
|
|
||||||
file_object['registered'] = Date.now();
|
|
||||||
path_object = path.parse(file_object['path']);
|
|
||||||
file_object['path'] = path.format(path_object);
|
|
||||||
|
|
||||||
exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
|
||||||
|
|
||||||
return file_object;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateFileObject(file_path, type) {
|
|
||||||
var jsonobj = utils.getJSON(file_path, type);
|
|
||||||
if (!jsonobj) {
|
|
||||||
return null;
|
|
||||||
} else if (!jsonobj['_filename']) {
|
|
||||||
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
|
||||||
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
|
|
||||||
// console.
|
|
||||||
var stats = fs.statSync(true_file_path);
|
|
||||||
|
|
||||||
const file_id = utils.removeFileExtension(path.basename(file_path));
|
|
||||||
var title = jsonobj.title;
|
|
||||||
var url = jsonobj.webpage_url;
|
|
||||||
var uploader = jsonobj.uploader;
|
|
||||||
var upload_date = utils.formatDateString(jsonobj.upload_date);
|
|
||||||
|
|
||||||
var size = stats.size;
|
|
||||||
|
|
||||||
var thumbnail = jsonobj.thumbnail;
|
|
||||||
var duration = jsonobj.duration;
|
|
||||||
var isaudio = type === 'audio';
|
|
||||||
var description = jsonobj.description;
|
|
||||||
var file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
|
||||||
return file_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppendedBasePathSub(sub, base_path) {
|
|
||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getFileDirectoriesAndDBs = async () => {
|
exports.getFileDirectoriesAndDBs = async () => {
|
||||||
@@ -252,13 +189,16 @@ exports.getFileDirectoriesAndDBs = async () => {
|
|||||||
dirs_to_check.push({
|
dirs_to_check.push({
|
||||||
basePath: path.join(usersFileFolder, user.uid, 'audio'),
|
basePath: path.join(usersFileFolder, user.uid, 'audio'),
|
||||||
user_uid: user.uid,
|
user_uid: user.uid,
|
||||||
type: 'audio'
|
type: 'audio',
|
||||||
|
archive_path: utils.getArchiveFolder('audio', user.uid)
|
||||||
});
|
});
|
||||||
|
|
||||||
// add user's video dir to check list
|
// add user's video dir to check list
|
||||||
dirs_to_check.push({
|
dirs_to_check.push({
|
||||||
basePath: path.join(usersFileFolder, user.uid, 'video'),
|
basePath: path.join(usersFileFolder, user.uid, 'video'),
|
||||||
type: 'video'
|
user_uid: user.uid,
|
||||||
|
type: 'video',
|
||||||
|
archive_path: utils.getArchiveFolder('video', user.uid)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -268,13 +208,15 @@ exports.getFileDirectoriesAndDBs = async () => {
|
|||||||
// add audio dir to check list
|
// add audio dir to check list
|
||||||
dirs_to_check.push({
|
dirs_to_check.push({
|
||||||
basePath: audioFolderPath,
|
basePath: audioFolderPath,
|
||||||
type: 'audio'
|
type: 'audio',
|
||||||
|
archive_path: utils.getArchiveFolder('audio')
|
||||||
});
|
});
|
||||||
|
|
||||||
// add video dir to check list
|
// add video dir to check list
|
||||||
dirs_to_check.push({
|
dirs_to_check.push({
|
||||||
basePath: videoFolderPath,
|
basePath: videoFolderPath,
|
||||||
type: 'video'
|
type: 'video',
|
||||||
|
archive_path: utils.getArchiveFolder('video')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,262 +237,14 @@ exports.getFileDirectoriesAndDBs = async () => {
|
|||||||
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
|
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
|
||||||
user_uid: subscription_to_check.user_uid,
|
user_uid: subscription_to_check.user_uid,
|
||||||
type: subscription_to_check.type,
|
type: subscription_to_check.type,
|
||||||
sub_id: subscription_to_check['id']
|
sub_id: subscription_to_check['id'],
|
||||||
|
archive_path: utils.getArchiveFolder(subscription_to_check.type, subscription_to_check.user_uid, subscription_to_check)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return dirs_to_check;
|
return dirs_to_check;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.importUnregisteredFiles = async () => {
|
|
||||||
const imported_files = [];
|
|
||||||
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
|
||||||
|
|
||||||
// run through check list and check each file to see if it's missing from the db
|
|
||||||
for (let i = 0; i < dirs_to_check.length; i++) {
|
|
||||||
const dir_to_check = dirs_to_check[i];
|
|
||||||
// recursively get all files in dir's path
|
|
||||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
|
||||||
|
|
||||||
for (let j = 0; j < files.length; j++) {
|
|
||||||
const file = files[j];
|
|
||||||
|
|
||||||
// check if file exists in db, if not add it
|
|
||||||
const files_with_same_url = await exports.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
|
|
||||||
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
|
||||||
if (!file_is_registered) {
|
|
||||||
// add additional info
|
|
||||||
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
|
||||||
if (file_obj) {
|
|
||||||
imported_files.push(file_obj['uid']);
|
|
||||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
|
||||||
} else {
|
|
||||||
logger.error(`Failed to import ${file['path']} automatically.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return imported_files;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.addMetadataPropertyToDB = async (property_key) => {
|
|
||||||
try {
|
|
||||||
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
|
||||||
const update_obj = {};
|
|
||||||
for (let i = 0; i < dirs_to_check.length; i++) {
|
|
||||||
const dir_to_check = dirs_to_check[i];
|
|
||||||
|
|
||||||
// recursively get all files in dir's path
|
|
||||||
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
|
|
||||||
for (let j = 0; j < files.length; j++) {
|
|
||||||
const file = files[j];
|
|
||||||
if (file[property_key]) {
|
|
||||||
update_obj[file.uid] = {[property_key]: file[property_key]};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await exports.bulkUpdateRecords('files', 'uid', update_obj);
|
|
||||||
} catch(err) {
|
|
||||||
logger.error(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
|
|
||||||
const first_video = await exports.getVideo(uids[0]);
|
|
||||||
const thumbnailToUse = first_video['thumbnailURL'];
|
|
||||||
|
|
||||||
let new_playlist = {
|
|
||||||
name: playlist_name,
|
|
||||||
uids: uids,
|
|
||||||
id: uuid(),
|
|
||||||
thumbnailURL: thumbnailToUse,
|
|
||||||
type: type,
|
|
||||||
registered: Date.now(),
|
|
||||||
randomize_order: false
|
|
||||||
};
|
|
||||||
|
|
||||||
new_playlist.user_uid = user_uid ? user_uid : undefined;
|
|
||||||
|
|
||||||
await exports.insertRecordIntoTable('playlists', new_playlist);
|
|
||||||
|
|
||||||
const duration = await exports.calculatePlaylistDuration(new_playlist);
|
|
||||||
await exports.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
|
|
||||||
|
|
||||||
return new_playlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
|
|
||||||
let playlist = await exports.getRecord('playlists', {id: playlist_id});
|
|
||||||
|
|
||||||
if (!playlist) {
|
|
||||||
playlist = await exports.getRecord('categories', {uid: playlist_id});
|
|
||||||
if (playlist) {
|
|
||||||
// category found
|
|
||||||
const files = await exports.getFiles(user_uid);
|
|
||||||
utils.addUIDsToCategory(playlist, files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts playlists to new UID-based schema
|
|
||||||
if (playlist && playlist['fileNames'] && !playlist['uids']) {
|
|
||||||
playlist['uids'] = [];
|
|
||||||
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
|
|
||||||
for (let i = 0; i < playlist['fileNames'].length; i++) {
|
|
||||||
const fileName = playlist['fileNames'][i];
|
|
||||||
const uid = await exports.getVideoUIDByID(fileName, user_uid);
|
|
||||||
if (uid) playlist['uids'].push(uid);
|
|
||||||
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
|
|
||||||
}
|
|
||||||
exports.updatePlaylist(playlist, user_uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
|
||||||
if (require_sharing && !playlist['sharingEnabled']) return null;
|
|
||||||
|
|
||||||
return playlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updatePlaylist = async (playlist) => {
|
|
||||||
let playlistID = playlist.id;
|
|
||||||
|
|
||||||
const duration = await exports.calculatePlaylistDuration(playlist);
|
|
||||||
playlist.duration = duration;
|
|
||||||
|
|
||||||
return await exports.updateRecord('playlists', {id: playlistID}, playlist);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
|
|
||||||
let success = await exports.updateRecord('playlists', {id: playlist_id}, assignment_obj);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
success = await exports.updateRecord('categories', {uid: playlist_id}, assignment_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
|
|
||||||
if (!playlist_file_objs) {
|
|
||||||
playlist_file_objs = [];
|
|
||||||
for (let i = 0; i < playlist['uids'].length; i++) {
|
|
||||||
const uid = playlist['uids'][i];
|
|
||||||
const file_obj = await exports.getVideo(uid);
|
|
||||||
if (file_obj) playlist_file_objs.push(file_obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
|
|
||||||
const file_obj = await exports.getVideo(uid, uuid);
|
|
||||||
const type = file_obj.isAudio ? 'audio' : 'video';
|
|
||||||
const folderPath = path.dirname(file_obj.path);
|
|
||||||
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
|
||||||
const name = file_obj.id;
|
|
||||||
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
|
|
||||||
|
|
||||||
var jsonPath = `${file_obj.path}.info.json`;
|
|
||||||
var altJSONPath = `${filePathNoExtension}.info.json`;
|
|
||||||
var thumbnailPath = `${filePathNoExtension}.webp`;
|
|
||||||
var altThumbnailPath = `${filePathNoExtension}.jpg`;
|
|
||||||
|
|
||||||
jsonPath = path.join(__dirname, jsonPath);
|
|
||||||
altJSONPath = path.join(__dirname, altJSONPath);
|
|
||||||
|
|
||||||
let jsonExists = await fs.pathExists(jsonPath);
|
|
||||||
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
|
||||||
|
|
||||||
if (!jsonExists) {
|
|
||||||
if (await fs.pathExists(altJSONPath)) {
|
|
||||||
jsonExists = true;
|
|
||||||
jsonPath = altJSONPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!thumbnailExists) {
|
|
||||||
if (await fs.pathExists(altThumbnailPath)) {
|
|
||||||
thumbnailExists = true;
|
|
||||||
thumbnailPath = altThumbnailPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileExists = await fs.pathExists(file_obj.path);
|
|
||||||
|
|
||||||
if (config_api.descriptors[uid]) {
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
|
|
||||||
config_api.descriptors[uid][i].destroy();
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
||||||
if (useYoutubeDLArchive) {
|
|
||||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
||||||
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
|
|
||||||
|
|
||||||
// get ID from JSON
|
|
||||||
|
|
||||||
var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
|
|
||||||
let id = null;
|
|
||||||
if (jsonobj) id = jsonobj.id;
|
|
||||||
|
|
||||||
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
|
||||||
if (await fs.pathExists(archive_path)) {
|
|
||||||
const line = id ? await utils.removeIDFromArchive(archive_path, id) : null;
|
|
||||||
if (blacklistMode && line) await writeToBlacklist(type, line);
|
|
||||||
} else {
|
|
||||||
logger.info('Could not find archive file for audio files. Creating...');
|
|
||||||
await fs.close(await fs.open(archive_path, 'w'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonExists) await fs.unlink(jsonPath);
|
|
||||||
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
|
||||||
|
|
||||||
await exports.removeRecord('files', {uid: uid});
|
|
||||||
|
|
||||||
if (fileExists) {
|
|
||||||
await fs.unlink(file_obj.path);
|
|
||||||
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: tell user that the file didn't exist
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
|
|
||||||
exports.getVideoUIDByID = async (file_id, uuid = null) => {
|
|
||||||
const file_obj = await exports.getRecord('files', {id: file_id});
|
|
||||||
return file_obj ? file_obj['uid'] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getVideo = async (file_uid) => {
|
|
||||||
return await exports.getRecord('files', {uid: file_uid});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getFiles = async (uuid = null) => {
|
|
||||||
return await exports.getRecords('files', {user_uid: uuid});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setVideoProperty = async (file_uid, assignment_obj) => {
|
|
||||||
// TODO: check if video exists, throw error if not
|
|
||||||
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic DB functions
|
// Basic DB functions
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
@@ -558,7 +252,7 @@ exports.setVideoProperty = async (file_uid, assignment_obj) => {
|
|||||||
exports.insertRecordIntoTable = async (table, doc, replaceFilter = null) => {
|
exports.insertRecordIntoTable = async (table, doc, replaceFilter = null) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
if (replaceFilter) local_db.get(table).remove(replaceFilter).write();
|
if (replaceFilter) local_db.get(table).remove((doc) => _.isMatch(doc, replaceFilter)).write();
|
||||||
local_db.get(table).push(doc).write();
|
local_db.get(table).push(doc).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -629,7 +323,7 @@ exports.bulkInsertRecordsIntoTable = async (table, docs) => {
|
|||||||
exports.getRecord = async (table, filter_obj) => {
|
exports.getRecord = async (table, filter_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
return applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value();
|
return exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await database.collection(table).findOne(filter_obj);
|
return await database.collection(table).findOne(filter_obj);
|
||||||
@@ -638,7 +332,7 @@ exports.getRecord = async (table, filter_obj) => {
|
|||||||
exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => {
|
exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
let cursor = filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
|
let cursor = filter_obj ? exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
|
||||||
if (sort) {
|
if (sort) {
|
||||||
cursor = cursor.sort((a, b) => (a[sort['by']] > b[sort['by']] ? sort['order'] : sort['order']*-1));
|
cursor = cursor.sort((a, b) => (a[sort['by']] > b[sort['by']] ? sort['order'] : sort['order']*-1));
|
||||||
}
|
}
|
||||||
@@ -661,10 +355,16 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort
|
|||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
|
||||||
exports.updateRecord = async (table, filter_obj, update_obj) => {
|
exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
|
if (nested_mode) {
|
||||||
|
// if object is nested we need to handle it differently
|
||||||
|
update_obj = utils.convertFlatObjectToNestedObject(update_obj);
|
||||||
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').merge(update_obj).write();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +377,14 @@ exports.updateRecord = async (table, filter_obj, update_obj) => {
|
|||||||
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
exports.updateRecords = async (table, filter_obj, update_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').each((record) => {
|
||||||
|
const props_to_update = Object.keys(update_obj);
|
||||||
|
for (let i = 0; i < props_to_update.length; i++) {
|
||||||
|
const prop_to_update = props_to_update[i];
|
||||||
|
const prop_value = update_obj[prop_to_update];
|
||||||
|
record[prop_to_update] = prop_value;
|
||||||
|
}
|
||||||
|
}).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,7 +392,19 @@ exports.updateRecords = async (table, filter_obj, update_obj) => {
|
|||||||
return !!(output['result']['ok']);
|
return !!(output['result']['ok']);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.bulkUpdateRecords = async (table, key_label, update_obj) => {
|
exports.removePropertyFromRecord = async (table, filter_obj, remove_obj) => {
|
||||||
|
// local db override
|
||||||
|
if (using_local_db) {
|
||||||
|
const props_to_remove = Object.keys(remove_obj);
|
||||||
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').unset(props_to_remove).write();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = await database.collection(table).updateOne(filter_obj, {$unset: remove_obj});
|
||||||
|
return !!(output['result']['ok']);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.bulkUpdateRecordsByKey = async (table, key_label, update_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
local_db.get(table).each((record) => {
|
local_db.get(table).each((record) => {
|
||||||
@@ -722,7 +441,7 @@ exports.bulkUpdateRecords = async (table, key_label, update_obj) => {
|
|||||||
exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
|
exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write();
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,7 +452,7 @@ exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
|
|||||||
exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
|
exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write();
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,7 +465,7 @@ exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
|
|||||||
exports.removeRecord = async (table, filter_obj) => {
|
exports.removeRecord = async (table, filter_obj) => {
|
||||||
// local db override
|
// local db override
|
||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,7 +476,7 @@ exports.removeRecord = async (table, filter_obj) => {
|
|||||||
// exports.removeRecordsByUIDBulk = async (table, uids) => {
|
// exports.removeRecordsByUIDBulk = async (table, uids) => {
|
||||||
// // local db override
|
// // local db override
|
||||||
// if (using_local_db) {
|
// if (using_local_db) {
|
||||||
// applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
// exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
||||||
// return true;
|
// return true;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@@ -821,7 +540,7 @@ exports.removeAllRecords = async (table = null, filter_obj = null) => {
|
|||||||
if (using_local_db) {
|
if (using_local_db) {
|
||||||
for (let i = 0; i < tables_to_remove.length; i++) {
|
for (let i = 0; i < tables_to_remove.length; i++) {
|
||||||
const table_to_remove = tables_to_remove[i];
|
const table_to_remove = tables_to_remove[i];
|
||||||
if (filter_obj) applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
if (filter_obj) exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
||||||
else local_db.assign({[table_to_remove]: []}).write();
|
else local_db.assign({[table_to_remove]: []}).write();
|
||||||
logger.debug(`Successfully removed records from ${table_to_remove}`);
|
logger.debug(`Successfully removed records from ${table_to_remove}`);
|
||||||
}
|
}
|
||||||
@@ -933,6 +652,7 @@ exports.importJSONToDB = async (db_json, users_json) => {
|
|||||||
const createFilesRecords = (files, subscriptions) => {
|
const createFilesRecords = (files, subscriptions) => {
|
||||||
for (let i = 0; i < subscriptions.length; i++) {
|
for (let i = 0; i < subscriptions.length; i++) {
|
||||||
const subscription = subscriptions[i];
|
const subscription = subscriptions[i];
|
||||||
|
if (!subscription['videos']) continue;
|
||||||
subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined}));
|
subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined}));
|
||||||
files = files.concat(subscriptions[i]['videos']);
|
files = files.concat(subscriptions[i]['videos']);
|
||||||
}
|
}
|
||||||
@@ -993,7 +713,7 @@ exports.backupDB = async () => {
|
|||||||
const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`;
|
const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`;
|
||||||
const path_to_backups = path.join(backup_dir, backup_file_name);
|
const path_to_backups = path.join(backup_dir, backup_file_name);
|
||||||
|
|
||||||
logger.verbose(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
|
logger.info(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
|
||||||
|
|
||||||
const table_to_records = {};
|
const table_to_records = {};
|
||||||
for (let i = 0; i < tables_list.length; i++) {
|
for (let i = 0; i < tables_list.length; i++) {
|
||||||
@@ -1040,10 +760,11 @@ exports.transferDB = async (local_to_remote) => {
|
|||||||
table_to_records[table] = await exports.getRecords(table);
|
table_to_records[table] = await exports.getRecords(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('Backup up DB...');
|
||||||
|
await exports.backupDB(); // should backup always
|
||||||
|
|
||||||
using_local_db = !local_to_remote;
|
using_local_db = !local_to_remote;
|
||||||
if (local_to_remote) {
|
if (local_to_remote) {
|
||||||
logger.debug('Backup up DB...');
|
|
||||||
await exports.backupDB();
|
|
||||||
const db_connected = await exports.connectToDB(5, true);
|
const db_connected = await exports.connectToDB(5, true);
|
||||||
if (!db_connected) {
|
if (!db_connected) {
|
||||||
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
|
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
|
||||||
@@ -1075,8 +796,13 @@ exports.transferDB = async (local_to_remote) => {
|
|||||||
This function is necessary to emulate mongodb's ability to search for null or missing values.
|
This function is necessary to emulate mongodb's ability to search for null or missing values.
|
||||||
A filter of null or undefined for a property will find docs that have that property missing, or have it
|
A filter of null or undefined for a property will find docs that have that property missing, or have it
|
||||||
null or undefined. We want that same functionality for the local DB as well
|
null or undefined. We want that same functionality for the local DB as well
|
||||||
|
|
||||||
|
error: {$ne: null}
|
||||||
|
^ ^
|
||||||
|
| |
|
||||||
|
filter_prop filter_prop_value
|
||||||
*/
|
*/
|
||||||
const applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
||||||
const filter_props = Object.keys(filter_obj);
|
const filter_props = Object.keys(filter_obj);
|
||||||
const return_val = db_path[operation](record => {
|
const return_val = db_path[operation](record => {
|
||||||
if (!filter_props) return true;
|
if (!filter_props) return true;
|
||||||
@@ -1085,14 +811,28 @@ const applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
|||||||
const filter_prop = filter_props[i];
|
const filter_prop = filter_props[i];
|
||||||
const filter_prop_value = filter_obj[filter_prop];
|
const filter_prop_value = filter_obj[filter_prop];
|
||||||
if (filter_prop_value === undefined || filter_prop_value === null) {
|
if (filter_prop_value === undefined || filter_prop_value === null) {
|
||||||
filtered &= record[filter_prop] === undefined || record[filter_prop] === null
|
filtered &= record[filter_prop] === undefined || record[filter_prop] === null;
|
||||||
} else {
|
} else {
|
||||||
if (typeof filter_prop_value === 'object') {
|
if (typeof filter_prop_value === 'object') {
|
||||||
if (filter_prop_value['$regex']) {
|
if ('$regex' in filter_prop_value) {
|
||||||
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
|
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
|
||||||
|
} else if ('$ne' in filter_prop_value) {
|
||||||
|
filtered &= filter_prop in record && record[filter_prop] !== filter_prop_value['$ne'];
|
||||||
|
} else if ('$lt' in filter_prop_value) {
|
||||||
|
filtered &= filter_prop in record && record[filter_prop] < filter_prop_value['$lt'];
|
||||||
|
} else if ('$gt' in filter_prop_value) {
|
||||||
|
filtered &= filter_prop in record && record[filter_prop] > filter_prop_value['$gt'];
|
||||||
|
} else if ('$lte' in filter_prop_value) {
|
||||||
|
filtered &= filter_prop in record && record[filter_prop] <= filter_prop_value['$lt'];
|
||||||
|
} else if ('$gte' in filter_prop_value) {
|
||||||
|
filtered &= filter_prop in record && record[filter_prop] >= filter_prop_value['$gt'];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filtered &= record[filter_prop] === filter_prop_value;
|
// handle case of nested property check
|
||||||
|
if (filter_prop.includes('.'))
|
||||||
|
filtered &= utils.searchObjectByString(record, filter_prop) === filter_prop_value;
|
||||||
|
else
|
||||||
|
filtered &= record[filter_prop] === filter_prop_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1101,14 +841,7 @@ const applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
|||||||
return return_val;
|
return return_val;
|
||||||
}
|
}
|
||||||
|
|
||||||
// archive helper functions
|
// should only be used for tests
|
||||||
|
exports.setLocalDBMode = (mode) => {
|
||||||
async function writeToBlacklist(type, line) {
|
using_local_db = mode;
|
||||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
}
|
||||||
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
|
|
||||||
// adds newline to the beginning of the line
|
|
||||||
line.replace('\n', '');
|
|
||||||
line.replace('\r', '');
|
|
||||||
line = '\n' + line;
|
|
||||||
await fs.appendFile(blacklistPath, line);
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,54 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const mergeFiles = require('merge-files');
|
|
||||||
const NodeID3 = require('node-id3')
|
const NodeID3 = require('node-id3')
|
||||||
const Mutex = require('async-mutex').Mutex;
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
const { create } = require('xmlbuilder2');
|
const { create } = require('xmlbuilder2');
|
||||||
const categories_api = require('./categories');
|
const categories_api = require('./categories');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
const files_api = require('./files');
|
||||||
|
const notifications_api = require('./notifications');
|
||||||
|
const archive_api = require('./archive');
|
||||||
|
|
||||||
const mutex = new Mutex();
|
const mutex = new Mutex();
|
||||||
let should_check_downloads = true;
|
let should_check_downloads = true;
|
||||||
|
|
||||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
const download_to_child_process = {};
|
||||||
|
|
||||||
if (db_api.database_initialized) {
|
if (db_api.database_initialized) {
|
||||||
setupDownloads();
|
exports.setupDownloads();
|
||||||
} else {
|
} else {
|
||||||
db_api.database_initialized_bs.subscribe(init => {
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
if (init) setupDownloads();
|
if (init) exports.setupDownloads();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
|
/*
|
||||||
|
|
||||||
|
This file handles all the downloading functionality.
|
||||||
|
|
||||||
|
To download a file, we go through 4 steps. Here they are with their respective index & function:
|
||||||
|
|
||||||
|
0: Create the download
|
||||||
|
- createDownload()
|
||||||
|
1: Get info for the download (we need this step for categories and archive functionality)
|
||||||
|
- collectInfo()
|
||||||
|
2: Download the file
|
||||||
|
- downloadQueuedFile()
|
||||||
|
3: Complete
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
We use checkDownloads() to move downloads through the steps and call their respective functions.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null, paused = false) => {
|
||||||
return await mutex.runExclusive(async () => {
|
return await mutex.runExclusive(async () => {
|
||||||
const download = {
|
const download = {
|
||||||
url: url,
|
url: url,
|
||||||
@@ -37,10 +57,11 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
|
|||||||
user_uid: user_uid,
|
user_uid: user_uid,
|
||||||
sub_id: sub_id,
|
sub_id: sub_id,
|
||||||
sub_name: sub_name,
|
sub_name: sub_name,
|
||||||
|
prefetched_info: prefetched_info,
|
||||||
options: options,
|
options: options,
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
step_index: 0,
|
step_index: 0,
|
||||||
paused: false,
|
paused: paused,
|
||||||
running: false,
|
running: false,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -63,8 +84,11 @@ exports.pauseDownload = async (download_uid) => {
|
|||||||
} else if (download['finished']) {
|
} else if (download['finished']) {
|
||||||
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`Pausing download ${download_uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
killActiveDownload(download);
|
||||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,10 +109,10 @@ exports.resumeDownload = async (download_uid) => {
|
|||||||
exports.restartDownload = async (download_uid) => {
|
exports.restartDownload = async (download_uid) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
await exports.clearDownload(download_uid);
|
await exports.clearDownload(download_uid);
|
||||||
const success = !!(await exports.createDownload(download['url'], download['type'], download['options'], download['user_uid']));
|
const new_download = await exports.createDownload(download['url'], download['type'], download['options'], download['user_uid']);
|
||||||
|
|
||||||
should_check_downloads = true;
|
should_check_downloads = true;
|
||||||
return success;
|
return new_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.cancelDownload = async (download_uid) => {
|
exports.cancelDownload = async (download_uid) => {
|
||||||
@@ -99,19 +123,28 @@ exports.cancelDownload = async (download_uid) => {
|
|||||||
} else if (download['finished']) {
|
} else if (download['finished']) {
|
||||||
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`Cancelling download ${download_uid}`);
|
||||||
}
|
}
|
||||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true, running: false});
|
|
||||||
|
killActiveDownload(download);
|
||||||
|
await handleDownloadError(download_uid, 'Cancelled', 'cancelled');
|
||||||
|
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.clearDownload = async (download_uid) => {
|
exports.clearDownload = async (download_uid) => {
|
||||||
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownloadError(download_uid, error_message) {
|
async function handleDownloadError(download_uid, error_message, error_type = null) {
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error_message, finished: true, running: false});
|
if (!download_uid) return;
|
||||||
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (!download || download['error']) return;
|
||||||
|
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDownloads() {
|
exports.setupDownloads = async () => {
|
||||||
await fixDownloadState();
|
await fixDownloadState();
|
||||||
setInterval(checkDownloads, 1000);
|
setInterval(checkDownloads, 1000);
|
||||||
}
|
}
|
||||||
@@ -154,18 +187,33 @@ async function checkDownloads() {
|
|||||||
if (max_concurrent_downloads < 0 || running_downloads_count >= max_concurrent_downloads) break;
|
if (max_concurrent_downloads < 0 || running_downloads_count >= max_concurrent_downloads) break;
|
||||||
|
|
||||||
if (waiting_download['finished_step'] && !waiting_download['finished']) {
|
if (waiting_download['finished_step'] && !waiting_download['finished']) {
|
||||||
|
if (waiting_download['sub_id']) {
|
||||||
|
const sub_missing = !(await db_api.getRecord('subscriptions', {id: waiting_download['sub_id']}));
|
||||||
|
if (sub_missing) {
|
||||||
|
handleDownloadError(waiting_download['uid'], `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
// move to next step
|
// move to next step
|
||||||
running_downloads_count++;
|
running_downloads_count++;
|
||||||
if (waiting_download['step_index'] === 0) {
|
if (waiting_download['step_index'] === 0) {
|
||||||
collectInfo(waiting_download['uid']);
|
exports.collectInfo(waiting_download['uid']);
|
||||||
} else if (waiting_download['step_index'] === 1) {
|
} else if (waiting_download['step_index'] === 1) {
|
||||||
downloadQueuedFile(waiting_download['uid']);
|
exports.downloadQueuedFile(waiting_download['uid']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectInfo(download_uid) {
|
function killActiveDownload(download) {
|
||||||
|
const child_process = download_to_child_process[download['uid']];
|
||||||
|
if (download['step_index'] === 2 && child_process) {
|
||||||
|
youtubedl_api.killYoutubeDLProcess(child_process);
|
||||||
|
delete download_to_child_process[download['uid']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.collectInfo = async (download_uid) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -186,26 +234,43 @@ async function collectInfo(download_uid) {
|
|||||||
let args = await exports.generateArgs(url, type, options, download['user_uid']);
|
let args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||||
|
|
||||||
// get video info prior to download
|
// get video info prior to download
|
||||||
let info = await getVideoInfoByURL(url, args, download_uid);
|
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
|
||||||
|
|
||||||
if (!info) {
|
if (!info || info.length === 0) {
|
||||||
// info failed, error presumably already recorded
|
// info failed, error presumably already recorded
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
|
||||||
|
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
|
if (useYoutubeDLArchive && !options.ignoreArchive && info.length === 1) {
|
||||||
|
const info_obj = info[0];
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info_obj['id'], type, download['user_uid'], download['sub_id']);
|
||||||
|
if (exists_in_archive) {
|
||||||
|
const error = `File '${info_obj['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
|
||||||
|
logger.warn(error);
|
||||||
|
if (download_uid) {
|
||||||
|
await handleDownloadError(download_uid, error, 'exists_in_archive');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let category = null;
|
let category = null;
|
||||||
|
|
||||||
// check if it fits into a category. If so, then get info again using new args
|
// check if it fits into a category. If so, then get info again using new args
|
||||||
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
if (info.length === 1 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
||||||
|
|
||||||
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||||
if (category && category['custom_output']) {
|
if (category && category['custom_output']) {
|
||||||
options.customOutput = category['custom_output'];
|
options.customOutput = category['custom_output'];
|
||||||
options.noRelativePath = true;
|
options.noRelativePath = true;
|
||||||
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
args = await exports.generateArgs(url, type, options, download['user_uid']);
|
||||||
info = await getVideoInfoByURL(url, args, download_uid);
|
info = await exports.getVideoInfoByURL(url, args, download_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stripped_category = category ? {name: category['name'], uid: category['uid']} : null;
|
||||||
|
|
||||||
// setup info required to calculate download progress
|
// setup info required to calculate download progress
|
||||||
|
|
||||||
const expected_file_size = utils.getExpectedFileSize(info);
|
const expected_file_size = utils.getExpectedFileSize(info);
|
||||||
@@ -213,24 +278,22 @@ async function collectInfo(download_uid) {
|
|||||||
const files_to_check_for_progress = [];
|
const files_to_check_for_progress = [];
|
||||||
|
|
||||||
// store info in download for future use
|
// store info in download for future use
|
||||||
if (Array.isArray(info)) {
|
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
||||||
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
|
||||||
} else {
|
|
||||||
files_to_check_for_progress.push(utils.removeFileExtension(info['_filename']));
|
|
||||||
}
|
|
||||||
|
|
||||||
const playlist_title = Array.isArray(info) ? info[0]['playlist_title'] || info[0]['playlist'] : null;
|
const title = info.length > 1 ? info[0]['playlist_title'] || info[0]['playlist'] : info[0]['title'];
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
running: false,
|
running: false,
|
||||||
options: options,
|
options: options,
|
||||||
files_to_check_for_progress: files_to_check_for_progress,
|
files_to_check_for_progress: files_to_check_for_progress,
|
||||||
expected_file_size: expected_file_size,
|
expected_file_size: expected_file_size,
|
||||||
title: playlist_title ? playlist_title : info['title']
|
title: title,
|
||||||
|
category: stripped_category,
|
||||||
|
prefetched_info: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadQueuedFile(download_uid) {
|
exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -239,6 +302,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
|
||||||
|
|
||||||
const url = download['url'];
|
const url = download['url'];
|
||||||
@@ -246,139 +310,143 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
const options = download['options'];
|
const options = download['options'];
|
||||||
const args = download['args'];
|
const args = download['args'];
|
||||||
const category = download['category'];
|
const category = download['category'];
|
||||||
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
||||||
if (options.customFileFolderPath) {
|
if (options.customFileFolderPath) {
|
||||||
fileFolderPath = options.customFileFolderPath;
|
fileFolderPath = options.customFileFolderPath;
|
||||||
|
} else if (download['user_uid']) {
|
||||||
|
fileFolderPath = path.join(usersFolderPath, download['user_uid'], type);
|
||||||
}
|
}
|
||||||
fs.ensureDirSync(fileFolderPath);
|
fs.ensureDirSync(fileFolderPath);
|
||||||
|
|
||||||
const start_time = Date.now();
|
const start_time = Date.now();
|
||||||
|
|
||||||
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
||||||
|
const file_objs = [];
|
||||||
// download file
|
// download file
|
||||||
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
let {child_process, callback} = await youtubedl_api.runYoutubeDL(url, args, customDownloadHandler);
|
||||||
const file_objs = [];
|
if (child_process) download_to_child_process[download['uid']] = child_process;
|
||||||
let end_time = Date.now();
|
const {parsed_output, err} = await callback;
|
||||||
let difference = (end_time - start_time)/1000;
|
clearInterval(download_checker);
|
||||||
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
let end_time = Date.now();
|
||||||
clearInterval(download_checker);
|
let difference = (end_time - start_time)/1000;
|
||||||
if (err) {
|
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
||||||
logger.error(err.stderr);
|
if (!parsed_output) {
|
||||||
await handleDownloadError(download_uid, err.stderr);
|
const errored_download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (errored_download && errored_download['paused']) return;
|
||||||
|
logger.error(err.toString());
|
||||||
|
await handleDownloadError(download_uid, err.toString(), 'unknown_error');
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
} else if (parsed_output) {
|
||||||
|
if (parsed_output.length === 0 || parsed_output[0].length === 0) {
|
||||||
|
// ERROR!
|
||||||
|
const error_message = `No output received for video download, check if it exists in your archive.`;
|
||||||
|
await handleDownloadError(download_uid, error_message, 'no_output');
|
||||||
|
logger.warn(error_message);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (output) {
|
|
||||||
if (output.length === 0 || output[0].length === 0) {
|
|
||||||
// ERROR!
|
|
||||||
const error_message = `No output received for video download, check if it exists in your archive.`;
|
|
||||||
await handleDownloadError(download_uid, error_message);
|
|
||||||
logger.warn(error_message);
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get filepath with no extension
|
|
||||||
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
|
||||||
|
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
|
||||||
var full_file_path = filepath_no_extension + ext;
|
|
||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
|
||||||
|
|
||||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
|
||||||
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
|
||||||
let vodId = url.split('twitch.tv/videos/')[1];
|
|
||||||
vodId = vodId.split('?')[0];
|
|
||||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// renames file if necessary due to bug
|
|
||||||
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
|
||||||
try {
|
|
||||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
|
||||||
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
|
||||||
} catch(e) {
|
|
||||||
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'audio') {
|
|
||||||
let tags = {
|
|
||||||
title: output_json['title'],
|
|
||||||
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
|
||||||
}
|
|
||||||
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
|
||||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
|
||||||
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.cropFileSettings) {
|
|
||||||
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// registers file in DB
|
|
||||||
const file_obj = await db_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
|
||||||
|
|
||||||
file_objs.push(file_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.merged_string !== null && options.merged_string !== undefined) {
|
|
||||||
const archive_folder = getArchiveFolder(fileFolderPath, options, download['user_uid']);
|
|
||||||
const current_merged_archive = fs.readFileSync(path.join(archive_folder, `merged_${type}.txt`), 'utf8');
|
|
||||||
const diff = current_merged_archive.replace(options.merged_string, '');
|
|
||||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
|
||||||
fs.appendFileSync(archive_path, diff);
|
|
||||||
}
|
|
||||||
|
|
||||||
let container = null;
|
|
||||||
|
|
||||||
if (file_objs.length > 1) {
|
|
||||||
// create playlist
|
|
||||||
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
|
||||||
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, download['user_uid']);
|
|
||||||
} else if (file_objs.length === 1) {
|
|
||||||
container = file_objs[0];
|
|
||||||
} else {
|
|
||||||
const error_message = 'Downloaded file failed to result in metadata object.';
|
|
||||||
logger.error(error_message);
|
|
||||||
await handleDownloadError(download_uid, error_message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
for (const output_json of parsed_output) {
|
||||||
|
if (!output_json) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get filepath with no extension
|
||||||
|
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
var full_file_path = filepath_no_extension + ext;
|
||||||
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||||
|
|
||||||
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
|
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
|
vodId = vodId.split('?')[0];
|
||||||
|
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// renames file if necessary due to bug
|
||||||
|
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||||
|
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
||||||
|
} catch(e) {
|
||||||
|
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'audio') {
|
||||||
|
let tags = {
|
||||||
|
title: output_json['title'],
|
||||||
|
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
||||||
|
}
|
||||||
|
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
||||||
|
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
||||||
|
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.cropFileSettings) {
|
||||||
|
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// registers file in DB
|
||||||
|
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
||||||
|
|
||||||
|
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
||||||
|
|
||||||
|
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
||||||
|
|
||||||
|
file_objs.push(file_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = null;
|
||||||
|
|
||||||
|
if (file_objs.length > 1) {
|
||||||
|
// create playlist
|
||||||
|
container = await files_api.createPlaylist(download['title'], file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
||||||
|
} else if (file_objs.length === 1) {
|
||||||
|
container = file_objs[0];
|
||||||
|
} else {
|
||||||
|
const error_message = 'Downloaded file failed to result in metadata object.';
|
||||||
|
logger.error(error_message);
|
||||||
|
await handleDownloadError(download_uid, error_message, 'no_metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
||||||
|
resolve(file_uids);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
|
|
||||||
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
|
||||||
|
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
|
||||||
|
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
|
||||||
|
}
|
||||||
|
|
||||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
|
||||||
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||||
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||||
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
const is_audio = type === 'audio';
|
const is_audio = type === 'audio';
|
||||||
|
|
||||||
let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
|
||||||
|
if (options.customFileFolderPath) {
|
||||||
|
fileFolderPath = options.customFileFolderPath;
|
||||||
|
} else if (user_uid) {
|
||||||
|
fileFolderPath = path.join(usersFolderPath, user_uid, fileFolderPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
||||||
|
|
||||||
@@ -388,6 +456,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
|
|
||||||
// video-specific args
|
// video-specific args
|
||||||
const selectedHeight = options.selectedHeight;
|
const selectedHeight = options.selectedHeight;
|
||||||
|
const maxHeight = options.maxHeight;
|
||||||
|
const heightParam = selectedHeight || maxHeight;
|
||||||
|
|
||||||
// audio-specific args
|
// audio-specific args
|
||||||
const maxBitrate = options.maxBitrate;
|
const maxBitrate = options.maxBitrate;
|
||||||
@@ -401,8 +471,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
if (!is_audio && !is_youtube) {
|
if (!is_audio && !is_youtube) {
|
||||||
// tiktok videos fail when using the default format
|
// tiktok videos fail when using the default format
|
||||||
qualityPath = null;
|
qualityPath = null;
|
||||||
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
|
|
||||||
qualityPath = ['-f', 'bestvideo+bestaudio']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customArgs) {
|
if (customArgs) {
|
||||||
@@ -410,8 +478,9 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
} else {
|
} else {
|
||||||
if (customQualityConfiguration) {
|
if (customQualityConfiguration) {
|
||||||
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
|
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
|
||||||
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
|
} else if (heightParam && heightParam !== '' && !is_audio) {
|
||||||
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
|
const heightFilter = (maxHeight && default_downloader === 'yt-dlp') ? ['-S', `res:${heightParam}`] : ['-f', `best[height${maxHeight ? '<' : ''}=${heightParam}]+bestaudio`]
|
||||||
|
qualityPath = [...heightFilter, '--merge-output-format', 'mp4'];
|
||||||
} else if (is_audio) {
|
} else if (is_audio) {
|
||||||
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
|
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
|
||||||
}
|
}
|
||||||
@@ -448,28 +517,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
|
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
||||||
if (useYoutubeDLArchive) {
|
|
||||||
const archive_folder = getArchiveFolder(fileFolderPath, options, user_uid);
|
|
||||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
|
||||||
|
|
||||||
await fs.ensureDir(archive_folder);
|
|
||||||
await fs.ensureFile(archive_path);
|
|
||||||
|
|
||||||
const blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
|
|
||||||
await fs.ensureFile(blacklist_path);
|
|
||||||
|
|
||||||
const merged_path = path.join(archive_folder, `merged_${type}.txt`);
|
|
||||||
await fs.ensureFile(merged_path);
|
|
||||||
// merges blacklist and regular archive
|
|
||||||
let inputPathList = [archive_path, blacklist_path];
|
|
||||||
await mergeFiles(inputPathList, merged_path);
|
|
||||||
|
|
||||||
options.merged_string = await fs.readFile(merged_path, "utf8");
|
|
||||||
|
|
||||||
downloadConfig.push('--download-archive', merged_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||||
downloadConfig.push('--write-thumbnail');
|
downloadConfig.push('--write-thumbnail');
|
||||||
}
|
}
|
||||||
@@ -493,9 +540,11 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
downloadConfig.push('-r', rate_limit);
|
downloadConfig.push('-r', rate_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
|
||||||
if (default_downloader === 'yt-dlp') {
|
if (default_downloader === 'yt-dlp') {
|
||||||
downloadConfig.push('--no-clean-infojson');
|
downloadConfig = utils.filterArgs(downloadConfig, ['--print-json']);
|
||||||
|
|
||||||
|
// in yt-dlp -j --no-simulate is preferable
|
||||||
|
downloadConfig.push('--no-clean-info-json', '-j', '--no-simulate');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -503,67 +552,41 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
// filter out incompatible args
|
// filter out incompatible args
|
||||||
downloadConfig = filterArgs(downloadConfig, is_audio);
|
downloadConfig = filterArgs(downloadConfig, is_audio);
|
||||||
|
|
||||||
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
if (!simulated) logger.verbose(`${default_downloader} args being used: ${downloadConfig.join(',')}`);
|
||||||
return downloadConfig;
|
return downloadConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoInfoByURL(url, args = [], download_uid = null) {
|
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
||||||
return new Promise(resolve => {
|
// remove bad args
|
||||||
// remove bad args
|
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
||||||
const new_args = [...args];
|
const new_args = [...temp_args];
|
||||||
|
|
||||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||||
if (archiveArgIndex !== -1) {
|
if (archiveArgIndex !== -1) {
|
||||||
new_args.splice(archiveArgIndex, 2);
|
new_args.splice(archiveArgIndex, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_args.push('--dump-json');
|
||||||
|
|
||||||
|
let {callback} = await youtubedl_api.runYoutubeDL(url, new_args);
|
||||||
|
const {parsed_output, err} = await callback;
|
||||||
|
if (!parsed_output || parsed_output.length === 0) {
|
||||||
|
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
||||||
|
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
||||||
|
logger.error(error_message);
|
||||||
|
if (download_uid) {
|
||||||
|
await handleDownloadError(download_uid, error_message, 'info_retrieve_failed');
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
new_args.push('--dump-json');
|
return parsed_output;
|
||||||
|
|
||||||
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
|
||||||
if (output) {
|
|
||||||
let outputs = [];
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs.push(output_json);
|
|
||||||
}
|
|
||||||
resolve(outputs.length === 1 ? outputs[0] : outputs);
|
|
||||||
} catch(e) {
|
|
||||||
const error = `Error while retrieving info on video with URL ${url} with the following message: output JSON could not be parsed. Output JSON: ${output}`;
|
|
||||||
logger.error(error);
|
|
||||||
if (download_uid) {
|
|
||||||
await handleDownloadError(download_uid, error);
|
|
||||||
}
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
|
||||||
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
|
||||||
logger.error(error_message);
|
|
||||||
if (download_uid) {
|
|
||||||
await handleDownloadError(download_uid, error_message);
|
|
||||||
}
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterArgs(args, isAudio) {
|
function filterArgs(args, isAudio) {
|
||||||
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
|
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
|
||||||
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
|
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
|
||||||
const args_to_remove = isAudio ? video_only_args : audio_only_args;
|
return utils.filterArgs(args, isAudio ? video_only_args : audio_only_args);
|
||||||
return args.filter(x => !args_to_remove.includes(x));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkDownloadPercent(download_uid) {
|
async function checkDownloadPercent(download_uid) {
|
||||||
@@ -576,6 +599,7 @@ async function checkDownloadPercent(download_uid) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (!download) return;
|
||||||
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
||||||
const resulting_file_size = download['expected_file_size'];
|
const resulting_file_size = download['expected_file_size'];
|
||||||
|
|
||||||
@@ -618,13 +642,3 @@ exports.generateNFOFile = (info, output_path) => {
|
|||||||
const xml = doc.end({ prettyPrint: true });
|
const xml = doc.end({ prettyPrint: true });
|
||||||
fs.writeFileSync(output_path, xml);
|
fs.writeFileSync(output_path, xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArchiveFolder(fileFolderPath, options, user_uid) {
|
|
||||||
if (options.customArchivePath) {
|
|
||||||
return path.join(options.customArchivePath);
|
|
||||||
} else if (user_uid) {
|
|
||||||
return path.join(fileFolderPath, 'archives');
|
|
||||||
} else {
|
|
||||||
return path.join(archivePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
CMD="pm2-runtime pm2.config.js"
|
|
||||||
|
|
||||||
# if the first arg starts with "-" pass it to program
|
|
||||||
if [ "${1#-}" != "$1" ]; then
|
|
||||||
set -- "$CMD" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# chown current working directory to current user
|
# chown current working directory to current user
|
||||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
echo "[entrypoint] setup permission, this may take a while"
|
||||||
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||||
exec gosu "$UID:$GID" "$0" "$@"
|
exec gosu "$UID:$GID" "$@"
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
|
|||||||
350
backend/files.js
Normal file
350
backend/files.js
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
|
const config_api = require('./config');
|
||||||
|
const db_api = require('./db');
|
||||||
|
const archive_api = require('./archive');
|
||||||
|
const utils = require('./utils')
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
|
||||||
|
if (!file_object) file_object = generateFileObject(file_path, type);
|
||||||
|
if (!file_object) {
|
||||||
|
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.fixVideoMetadataPerms(file_path, type);
|
||||||
|
|
||||||
|
// add thumbnail path
|
||||||
|
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
|
||||||
|
|
||||||
|
// if category exists, only include essential info
|
||||||
|
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
|
||||||
|
|
||||||
|
// modify duration
|
||||||
|
if (cropFileSettings) {
|
||||||
|
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_uid) file_object['user_uid'] = user_uid;
|
||||||
|
if (sub_id) file_object['sub_id'] = sub_id;
|
||||||
|
|
||||||
|
const file_obj = await registerFileDBManual(file_object);
|
||||||
|
|
||||||
|
// remove metadata JSON if needed
|
||||||
|
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
||||||
|
utils.deleteJSONFile(file_path, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerFileDBManual(file_object) {
|
||||||
|
// add additional info
|
||||||
|
file_object['uid'] = uuid();
|
||||||
|
file_object['registered'] = Date.now();
|
||||||
|
const path_object = path.parse(file_object['path']);
|
||||||
|
file_object['path'] = path.format(path_object);
|
||||||
|
|
||||||
|
await db_api.insertRecordIntoTable('files', file_object, {path: file_object['path']})
|
||||||
|
|
||||||
|
return file_object;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFileObject(file_path, type) {
|
||||||
|
const jsonobj = utils.getJSON(file_path, type);
|
||||||
|
if (!jsonobj) {
|
||||||
|
return null;
|
||||||
|
} else if (!jsonobj['_filename']) {
|
||||||
|
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
|
||||||
|
// console.
|
||||||
|
const stats = fs.statSync(true_file_path);
|
||||||
|
|
||||||
|
const file_id = utils.removeFileExtension(path.basename(file_path));
|
||||||
|
const title = jsonobj.title;
|
||||||
|
const url = jsonobj.webpage_url;
|
||||||
|
const uploader = jsonobj.uploader;
|
||||||
|
const upload_date = utils.formatDateString(jsonobj.upload_date);
|
||||||
|
|
||||||
|
const size = stats.size;
|
||||||
|
|
||||||
|
const thumbnail = jsonobj.thumbnail;
|
||||||
|
const duration = jsonobj.duration;
|
||||||
|
const isaudio = type === 'audio';
|
||||||
|
const description = jsonobj.description;
|
||||||
|
const file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||||
|
return file_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importUnregisteredFiles = async () => {
|
||||||
|
const imported_files = [];
|
||||||
|
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||||
|
|
||||||
|
// run through check list and check each file to see if it's missing from the db
|
||||||
|
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||||
|
const dir_to_check = dirs_to_check[i];
|
||||||
|
// recursively get all files in dir's path
|
||||||
|
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
||||||
|
|
||||||
|
for (let j = 0; j < files.length; j++) {
|
||||||
|
const file = files[j];
|
||||||
|
|
||||||
|
// check if file exists in db, if not add it
|
||||||
|
const files_with_same_url = await db_api.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
|
||||||
|
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
||||||
|
if (!file_is_registered) {
|
||||||
|
// add additional info
|
||||||
|
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
||||||
|
if (file_obj) {
|
||||||
|
imported_files.push(file_obj['uid']);
|
||||||
|
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`Failed to import ${file['path']} automatically.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imported_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addMetadataPropertyToDB = async (property_key) => {
|
||||||
|
try {
|
||||||
|
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
|
||||||
|
const update_obj = {};
|
||||||
|
for (let i = 0; i < dirs_to_check.length; i++) {
|
||||||
|
const dir_to_check = dirs_to_check[i];
|
||||||
|
|
||||||
|
// recursively get all files in dir's path
|
||||||
|
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
|
||||||
|
for (let j = 0; j < files.length; j++) {
|
||||||
|
const file = files[j];
|
||||||
|
if (file[property_key]) {
|
||||||
|
update_obj[file.uid] = {[property_key]: file[property_key]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db_api.bulkUpdateRecordsByKey('files', 'uid', update_obj);
|
||||||
|
} catch(err) {
|
||||||
|
logger.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
|
||||||
|
const first_video = await exports.getVideo(uids[0]);
|
||||||
|
const thumbnailToUse = first_video['thumbnailURL'];
|
||||||
|
|
||||||
|
let new_playlist = {
|
||||||
|
name: playlist_name,
|
||||||
|
uids: uids,
|
||||||
|
id: uuid(),
|
||||||
|
thumbnailURL: thumbnailToUse,
|
||||||
|
registered: Date.now(),
|
||||||
|
randomize_order: false
|
||||||
|
};
|
||||||
|
|
||||||
|
new_playlist.user_uid = user_uid ? user_uid : undefined;
|
||||||
|
|
||||||
|
await db_api.insertRecordIntoTable('playlists', new_playlist);
|
||||||
|
|
||||||
|
const duration = await exports.calculatePlaylistDuration(new_playlist);
|
||||||
|
await db_api.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
|
||||||
|
|
||||||
|
return new_playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
|
||||||
|
let playlist = await db_api.getRecord('playlists', {id: playlist_id});
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
playlist = await db_api.getRecord('categories', {uid: playlist_id});
|
||||||
|
if (playlist) {
|
||||||
|
const uids = (await db_api.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
|
||||||
|
playlist['uids'] = uids;
|
||||||
|
playlist['auto'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts playlists to new UID-based schema
|
||||||
|
if (playlist && playlist['fileNames'] && !playlist['uids']) {
|
||||||
|
playlist['uids'] = [];
|
||||||
|
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
|
||||||
|
for (let i = 0; i < playlist['fileNames'].length; i++) {
|
||||||
|
const fileName = playlist['fileNames'][i];
|
||||||
|
const uid = await exports.getVideoUIDByID(fileName, user_uid);
|
||||||
|
if (uid) playlist['uids'].push(uid);
|
||||||
|
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
|
||||||
|
}
|
||||||
|
exports.updatePlaylist(playlist, user_uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent unauthorized users from accessing the file info
|
||||||
|
if (require_sharing && !playlist['sharingEnabled']) return null;
|
||||||
|
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updatePlaylist = async (playlist) => {
|
||||||
|
let playlistID = playlist.id;
|
||||||
|
|
||||||
|
const duration = await exports.calculatePlaylistDuration(playlist);
|
||||||
|
playlist.duration = duration;
|
||||||
|
|
||||||
|
return await db_api.updateRecord('playlists', {id: playlistID}, playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
|
||||||
|
let success = await db_api.updateRecord('playlists', {id: playlist_id}, assignment_obj);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
success = await db_api.updateRecord('categories', {uid: playlist_id}, assignment_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
|
||||||
|
if (!playlist_file_objs) {
|
||||||
|
playlist_file_objs = [];
|
||||||
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
|
const uid = playlist['uids'][i];
|
||||||
|
const file_obj = await exports.getVideo(uid);
|
||||||
|
if (file_obj) playlist_file_objs.push(file_obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.deleteFile = async (uid, blacklistMode = false) => {
|
||||||
|
const file_obj = await exports.getVideo(uid);
|
||||||
|
const type = file_obj.isAudio ? 'audio' : 'video';
|
||||||
|
const folderPath = path.dirname(file_obj.path);
|
||||||
|
const name = file_obj.id;
|
||||||
|
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
|
||||||
|
|
||||||
|
var jsonPath = `${file_obj.path}.info.json`;
|
||||||
|
var altJSONPath = `${filePathNoExtension}.info.json`;
|
||||||
|
var thumbnailPath = `${filePathNoExtension}.webp`;
|
||||||
|
var altThumbnailPath = `${filePathNoExtension}.jpg`;
|
||||||
|
|
||||||
|
jsonPath = path.join(__dirname, jsonPath);
|
||||||
|
altJSONPath = path.join(__dirname, altJSONPath);
|
||||||
|
|
||||||
|
let jsonExists = await fs.pathExists(jsonPath);
|
||||||
|
let thumbnailExists = await fs.pathExists(thumbnailPath);
|
||||||
|
|
||||||
|
if (!jsonExists) {
|
||||||
|
if (await fs.pathExists(altJSONPath)) {
|
||||||
|
jsonExists = true;
|
||||||
|
jsonPath = altJSONPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thumbnailExists) {
|
||||||
|
if (await fs.pathExists(altThumbnailPath)) {
|
||||||
|
thumbnailExists = true;
|
||||||
|
thumbnailPath = altThumbnailPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileExists = await fs.pathExists(file_obj.path);
|
||||||
|
|
||||||
|
if (config_api.descriptors[uid]) {
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
|
||||||
|
config_api.descriptors[uid][i].destroy();
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
|
if (useYoutubeDLArchive || file_obj.sub_id) {
|
||||||
|
// get id/extractor from JSON
|
||||||
|
|
||||||
|
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
|
||||||
|
let retrievedID = null;
|
||||||
|
let retrievedExtractor = null;
|
||||||
|
if (info_json) {
|
||||||
|
retrievedID = info_json['id'];
|
||||||
|
retrievedExtractor = info_json['extractor'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
|
||||||
|
if (!blacklistMode) {
|
||||||
|
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id)
|
||||||
|
} else {
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
|
||||||
|
if (!exists_in_archive) {
|
||||||
|
await archive_api.addToArchive(retrievedExtractor, retrievedID, type, file_obj.title, file_obj.user_uid, file_obj.sub_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonExists) await fs.unlink(jsonPath);
|
||||||
|
if (thumbnailExists) await fs.unlink(thumbnailPath);
|
||||||
|
|
||||||
|
await db_api.removeRecord('files', {uid: uid});
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
await fs.unlink(file_obj.path);
|
||||||
|
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: tell user that the file didn't exist
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
|
||||||
|
exports.getVideoUIDByID = async (file_id, uuid = null) => {
|
||||||
|
const file_obj = await db_api.getRecord('files', {id: file_id});
|
||||||
|
return file_obj ? file_obj['uid'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getVideo = async (file_uid) => {
|
||||||
|
return await db_api.getRecord('files', {uid: file_uid});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
|
||||||
|
const filter_obj = {user_uid: uuid};
|
||||||
|
const regex = true;
|
||||||
|
if (text_search) {
|
||||||
|
if (regex) {
|
||||||
|
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
|
||||||
|
} else {
|
||||||
|
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favorite_filter) {
|
||||||
|
filter_obj['favorite'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub_id) {
|
||||||
|
filter_obj['sub_id'] = sub_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
||||||
|
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
||||||
|
|
||||||
|
const files = JSON.parse(JSON.stringify(await db_api.getRecords('files', filter_obj, false, sort, range, text_search)));
|
||||||
|
const file_count = await db_api.getRecords('files', filter_obj, true);
|
||||||
|
|
||||||
|
return {files, file_count};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
|
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
|
||||||
# Date: 2022-05-03
|
# Date: 2022-05-03
|
||||||
@@ -6,8 +6,7 @@
|
|||||||
# If you want to run this script on a bare-metal installation instead of within Docker
|
# If you want to run this script on a bare-metal installation instead of within Docker
|
||||||
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
|
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
|
||||||
# USAGE: within your container's bash shell:
|
# USAGE: within your container's bash shell:
|
||||||
# chmod -R +x ./fix-scripts/
|
# ./fix-scripts/<name of fix-script>
|
||||||
# ./fix-scripts/001-fix_download_permissions.sh
|
|
||||||
|
|
||||||
# User defines / Docker env defaults
|
# User defines / Docker env defaults
|
||||||
PATH_SUBS=/app/subscriptions
|
PATH_SUBS=/app/subscriptions
|
||||||
|
|||||||
142
backend/fix-scripts/002-fix_dupes_per_archive_file.sh
Normal file
142
backend/fix-scripts/002-fix_dupes_per_archive_file.sh
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M
|
||||||
|
# Date: 2022-05-09
|
||||||
|
|
||||||
|
# If you want to run this script on a bare-metal installation instead of within Docker
|
||||||
|
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
|
||||||
|
# USAGE: within your container's bash shell:
|
||||||
|
# ./fix-scripts/<name of fix-script>
|
||||||
|
|
||||||
|
# User defines (NO TRAILING SLASHES) / Docker env defaults
|
||||||
|
PATH_SUBSARCHIVE=/app/subscriptions/archives
|
||||||
|
PATH_ONEOFFARCHIVE=/app/appdata/archives
|
||||||
|
|
||||||
|
# Backup paths (substitute with your personal preference if you like)
|
||||||
|
PATH_SUBSARCHIVEBKP=$PATH_SUBSARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
|
||||||
|
PATH_ONEOFFARCHIVEBKP=$PATH_ONEOFFARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
|
||||||
|
|
||||||
|
|
||||||
|
# Define Colors for TUI
|
||||||
|
yellow=$(tput setaf 3)
|
||||||
|
normal=$(tput sgr0)
|
||||||
|
|
||||||
|
tput civis # hide the cursor
|
||||||
|
|
||||||
|
clear -x
|
||||||
|
printf "\n"
|
||||||
|
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
|
||||||
|
printf "Welcome to the INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M."
|
||||||
|
printf "\nThis script will cycle through the archive files in the folders mentioned"
|
||||||
|
printf "\nbelow and remove within each archive the dupe entries. (compact them)"
|
||||||
|
printf "\nDuring some older builds of YTDL-M the archives could receive dupe"
|
||||||
|
printf "\nentries and blow up in size, sometimes causing conflicts with download management."
|
||||||
|
printf '\n%*s' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# check whether dirs exist
|
||||||
|
i=0
|
||||||
|
[ -d $PATH_SUBSARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found Subscriptions archive directory at ${PATH_SUBSARCHIVE}"
|
||||||
|
[ -d $PATH_ONEOFFARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found one-off archive directory at ${PATH_ONEOFFARCHIVE}"
|
||||||
|
|
||||||
|
# Ask to proceed or cancel, exit on missing paths
|
||||||
|
case $i in
|
||||||
|
0)
|
||||||
|
printf "\n\n Couldn't find any archive location path! \n\nPlease edit this script to configure!"
|
||||||
|
tput cnorm
|
||||||
|
exit 2;;
|
||||||
|
2)
|
||||||
|
printf "\n\n Found all archive locations. \n\nProceed? (Y/N)";;
|
||||||
|
*)
|
||||||
|
printf "\n\n Only found ${i} out of 2 archive locations! Something about this script's config must be wrong. \n\nProceed anyways? (Y/N)";;
|
||||||
|
esac
|
||||||
|
old_stty_cfg=$(stty -g)
|
||||||
|
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
|
||||||
|
if echo "$answer" | grep -iq "^y" ;then
|
||||||
|
printf "\n\nRunning jobs now... (this may take a while)\n"
|
||||||
|
|
||||||
|
printf "\nBacking up directories...\n"
|
||||||
|
|
||||||
|
chars="⣾⣽⣻⢿⡿⣟⣯⣷"
|
||||||
|
cp -R $PATH_SUBSARCHIVE $PATH_SUBSARCHIVEBKP &
|
||||||
|
PID=$!
|
||||||
|
i=1
|
||||||
|
echo -n ' '
|
||||||
|
while [ -d /proc/$PID ]
|
||||||
|
do
|
||||||
|
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||||
|
sleep 0.15
|
||||||
|
done
|
||||||
|
[ -d $PATH_SUBSARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_SUBSARCHIVE} to ${PATH_SUBSARCHIVEBKP} ($(du -sh $PATH_SUBSARCHIVEBKP | cut -f1))\n"
|
||||||
|
|
||||||
|
cp -R $PATH_ONEOFFARCHIVE $PATH_ONEOFFARCHIVEBKP &
|
||||||
|
PID2=$!
|
||||||
|
i=1
|
||||||
|
echo -n ' '
|
||||||
|
while [ -d /proc/$PID2 ]
|
||||||
|
do
|
||||||
|
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[ -d $PATH_ONEOFFARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_ONEOFFARCHIVE} to ${PATH_ONEOFFARCHIVEBKP} ($(du -sh $PATH_ONEOFFARCHIVEBKP | cut -f1))\n"
|
||||||
|
|
||||||
|
|
||||||
|
printf "\nCompacting files...\n"
|
||||||
|
|
||||||
|
tmpfile=$(mktemp) &&
|
||||||
|
|
||||||
|
[ -d $PATH_SUBSARCHIVE ] &&
|
||||||
|
find $PATH_SUBSARCHIVE -name '*.txt' -print0 | while read -d $'\0' file # Set delimiter to null because we want to catch all possible filenames (WE CANNOT CHANGE IFS HERE) - https://stackoverflow.com/a/15931055
|
||||||
|
do
|
||||||
|
cp "$file" "$tmpfile"
|
||||||
|
{ awk '!x[$0]++' "$tmpfile" > "$file"; } & # https://unix.stackexchange.com/questions/159695/how-does-awk-a0-work
|
||||||
|
PID3=$!
|
||||||
|
i=1
|
||||||
|
echo -n ''
|
||||||
|
while [ -d /proc/$PID3 ]
|
||||||
|
do
|
||||||
|
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
BEFORE=$(wc -l < $tmpfile)
|
||||||
|
AFTER=$(wc -l < $file)
|
||||||
|
if [[ "$AFTER" -ne "$BEFORE" ]]; then
|
||||||
|
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
|
||||||
|
else
|
||||||
|
printf "\bℹ No action needed for file: ${file}\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -d $PATH_ONEOFFARCHIVE ] &&
|
||||||
|
find $PATH_ONEOFFARCHIVE -name '*.txt' -print0 | while read -d $'\0' file
|
||||||
|
do
|
||||||
|
cp "$file" "$tmpfile" &
|
||||||
|
awk '!x[$0]++' "$tmpfile" > "$file" &
|
||||||
|
PID4=$!
|
||||||
|
i=1
|
||||||
|
echo -n ''
|
||||||
|
while [ -d /proc/$PID4 ]
|
||||||
|
do
|
||||||
|
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
BEFORE=$(wc -l < $tmpfile)
|
||||||
|
AFTER=$(wc -l < $file)
|
||||||
|
if [ "$BEFORE" -ne "$AFTER" ]; then
|
||||||
|
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
|
||||||
|
else
|
||||||
|
printf "\bℹ No action ran for file: ${file}\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
tput cnorm # show the cursor
|
||||||
|
rm "$tmpfile"
|
||||||
|
|
||||||
|
printf "\n\n✔ Done."
|
||||||
|
printf "\nℹ Please keep in mind that you may still want to"
|
||||||
|
printf "\n run corruption checks against your archives!\n\n"
|
||||||
|
exit
|
||||||
|
else
|
||||||
|
tput cnorm
|
||||||
|
printf "\nOkay, bye.\n\n"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
293
backend/notifications.js
Normal file
293
backend/notifications.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
const db_api = require('./db');
|
||||||
|
const config_api = require('./config');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const utils = require('./utils');
|
||||||
|
const consts = require('./consts');
|
||||||
|
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const { gotify } = require("gotify");
|
||||||
|
const TelegramBotAPI = require('node-telegram-bot-api');
|
||||||
|
let telegram_bot = null;
|
||||||
|
const REST = require('@discordjs/rest').REST;
|
||||||
|
const API = require('@discordjs/core').API;
|
||||||
|
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
||||||
|
|
||||||
|
const NOTIFICATION_TYPE_TO_TITLE = {
|
||||||
|
task_finished: 'Task finished',
|
||||||
|
download_complete: 'Download complete',
|
||||||
|
download_error: 'Download error'
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOTIFICATION_TYPE_TO_BODY = {
|
||||||
|
task_finished: (notification) => notification['data']['task_title'],
|
||||||
|
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
|
||||||
|
download_error: (notification) => {return `Error: ${notification['data']['download_error_message']}\nError code: ${notification['data']['download_error_type']}\n\nOriginal URL: ${notification['data']['download_url']}`}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOTIFICATION_TYPE_TO_URL = {
|
||||||
|
task_finished: () => {return `${utils.getBaseURL()}/#/tasks`},
|
||||||
|
download_complete: (notification) => {return `${utils.getBaseURL()}/#/player;uid=${notification['data']['file_uid']}`},
|
||||||
|
download_error: () => {return `${utils.getBaseURL()}/#/downloads`},
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOTIFICATION_TYPE_TO_THUMBNAIL = {
|
||||||
|
task_finished: () => null,
|
||||||
|
download_complete: (notification) => notification['data']['file_thumbnail'],
|
||||||
|
download_error: () => null
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendNotification = async (notification) => {
|
||||||
|
// info necessary if we are using 3rd party APIs
|
||||||
|
const type = notification['type'];
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
title: NOTIFICATION_TYPE_TO_TITLE[type],
|
||||||
|
body: NOTIFICATION_TYPE_TO_BODY[type](notification),
|
||||||
|
type: type,
|
||||||
|
url: NOTIFICATION_TYPE_TO_URL[type](notification),
|
||||||
|
thumbnail: NOTIFICATION_TYPE_TO_THUMBNAIL[type](notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config_api.getConfigItem('ytdl_use_ntfy_API') && config_api.getConfigItem('ytdl_ntfy_topic_url')) {
|
||||||
|
sendNtfyNotification(data);
|
||||||
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_use_gotify_API') && config_api.getConfigItem('ytdl_gotify_server_url') && config_api.getConfigItem('ytdl_gotify_app_token')) {
|
||||||
|
sendGotifyNotification(data);
|
||||||
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
|
||||||
|
exports.sendTelegramNotification(data);
|
||||||
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
||||||
|
sendGenericNotification(data);
|
||||||
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
|
||||||
|
sendDiscordNotification(data);
|
||||||
|
}
|
||||||
|
if (config_api.getConfigItem('ytdl_slack_webhook_url')) {
|
||||||
|
sendSlackNotification(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db_api.insertRecordIntoTable('notifications', notification);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendTaskNotification = async (task_obj, confirmed) => {
|
||||||
|
if (!notificationEnabled('task_finished')) return;
|
||||||
|
// workaround for tasks which are user_uid agnostic
|
||||||
|
const user_uid = config_api.getConfigItem('ytdl_multi_user_mode') ? 'admin' : null;
|
||||||
|
await db_api.removeAllRecords('notifications', {"data.task_key": task_obj.key});
|
||||||
|
const data = {task_key: task_obj.key, task_title: task_obj.title, confirmed: confirmed};
|
||||||
|
const notification = exports.createNotification('task_finished', ['view_tasks'], data, user_uid);
|
||||||
|
return await exports.sendNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendDownloadNotification = async (file, user_uid) => {
|
||||||
|
if (!notificationEnabled('download_complete')) return;
|
||||||
|
const data = {file_uid: file.uid, file_title: file.title, file_thumbnail: file.thumbnailURL, original_url: file.url};
|
||||||
|
const notification = exports.createNotification('download_complete', ['play'], data, user_uid);
|
||||||
|
return await exports.sendNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendDownloadErrorNotification = async (download, user_uid, error_message, error_type = null) => {
|
||||||
|
if (!notificationEnabled('download_error')) return;
|
||||||
|
const data = {download_uid: download.uid, download_url: download.url, download_error_message: error_message, download_error_type: error_type};
|
||||||
|
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
|
||||||
|
return await exports.sendNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createNotification = (type, actions, data, user_uid) => {
|
||||||
|
const notification = {
|
||||||
|
type: type,
|
||||||
|
actions: actions,
|
||||||
|
data: data,
|
||||||
|
user_uid: user_uid,
|
||||||
|
uid: uuid(),
|
||||||
|
read: false,
|
||||||
|
timestamp: Date.now()/1000
|
||||||
|
}
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notificationEnabled(type) {
|
||||||
|
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ntfy
|
||||||
|
|
||||||
|
function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
||||||
|
logger.verbose('Sending notification to ntfy');
|
||||||
|
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: body,
|
||||||
|
headers: {
|
||||||
|
'Title': title,
|
||||||
|
'Tags': type,
|
||||||
|
'Click': url,
|
||||||
|
'Attach': thumbnail
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gotify
|
||||||
|
|
||||||
|
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
||||||
|
logger.verbose('Sending notification to gotify');
|
||||||
|
await gotify({
|
||||||
|
server: config_api.getConfigItem('ytdl_gotify_server_url'),
|
||||||
|
app: config_api.getConfigItem('ytdl_gotify_app_token'),
|
||||||
|
title: title,
|
||||||
|
message: body,
|
||||||
|
tag: type,
|
||||||
|
priority: 5, // Keeping default from docs, may want to change this,
|
||||||
|
extras: {
|
||||||
|
"client::notification": {
|
||||||
|
click: { url: url },
|
||||||
|
bigImageUrl: thumbnail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
|
||||||
|
setupTelegramBot();
|
||||||
|
config_api.config_updated.subscribe(change => {
|
||||||
|
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
|
||||||
|
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
||||||
|
if (!use_telegram_api || !bot_token) return;
|
||||||
|
if (!change) return;
|
||||||
|
if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token' || change['key'] === 'ytdl_telegram_webhook_proxy') {
|
||||||
|
logger.debug('Telegram bot setting up');
|
||||||
|
setupTelegramBot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupTelegramBot() {
|
||||||
|
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
|
||||||
|
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
||||||
|
if (!use_telegram_api || !bot_token) return;
|
||||||
|
|
||||||
|
telegram_bot = new TelegramBotAPI(bot_token);
|
||||||
|
const webhook_proxy = config_api.getConfigItem('ytdl_telegram_webhook_proxy');
|
||||||
|
const webhook_url = webhook_proxy ? webhook_proxy : `${utils.getBaseURL()}/api/telegramRequest`;
|
||||||
|
telegram_bot.setWebHook(webhook_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendTelegramNotification = async ({body, title, type, url, thumbnail}) => {
|
||||||
|
if (!telegram_bot){
|
||||||
|
logger.error('Telegram bot not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
|
||||||
|
if (!chat_id){
|
||||||
|
logger.error('Telegram chat ID required!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.verbose('Sending notification to Telegram');
|
||||||
|
if (thumbnail) await telegram_bot.sendPhoto(chat_id, thumbnail);
|
||||||
|
telegram_bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord
|
||||||
|
|
||||||
|
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
||||||
|
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
||||||
|
const url_split = discord_webhook_url.split('webhooks/');
|
||||||
|
const [webhook_id, webhook_token] = url_split[1].split('/');
|
||||||
|
const rest = new REST({ version: '10' });
|
||||||
|
const api = new API(rest);
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(title)
|
||||||
|
.setColor(0x00FFFF)
|
||||||
|
.setURL(url)
|
||||||
|
.setDescription(`ID: ${type}`);
|
||||||
|
if (thumbnail) embed.setThumbnail(thumbnail);
|
||||||
|
if (type === 'download_error') embed.setColor(0xFC2003);
|
||||||
|
|
||||||
|
const result = await api.webhooks.execute(webhook_id, webhook_token, {
|
||||||
|
content: body,
|
||||||
|
username: 'YoutubeDL-Material',
|
||||||
|
avatar_url: consts.ICON_URL,
|
||||||
|
embeds: [embed],
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slack
|
||||||
|
|
||||||
|
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
||||||
|
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
||||||
|
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
||||||
|
const data = {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `*${title}*`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// add thumbnail if exists
|
||||||
|
if (thumbnail) {
|
||||||
|
data['blocks'].push({
|
||||||
|
type: "image",
|
||||||
|
image_url: thumbnail,
|
||||||
|
alt_text: "notification_thumbnail"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
data['blocks'].push(
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `<${url}|${url}>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "context",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `*ID:* ${type}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fetch(slack_webhook_url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic
|
||||||
|
|
||||||
|
function sendGenericNotification(data) {
|
||||||
|
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
||||||
|
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
||||||
|
fetch(webhook_url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
5771
backend/package-lock.json
generated
5771
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,21 +4,10 @@
|
|||||||
"description": "backend for YoutubeDL-Material",
|
"description": "backend for YoutubeDL-Material",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "mocha test --exit -s 1000",
|
||||||
"start": "nodemon app.js",
|
"start": "pm2-runtime --raw pm2.config.js",
|
||||||
"debug": "set YTDL_MODE=debug && node app.js"
|
"debug": "set YTDL_MODE=debug && node app.js"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
|
||||||
"ignore": [
|
|
||||||
"*.js",
|
|
||||||
"appdata/*",
|
|
||||||
"public/*"
|
|
||||||
],
|
|
||||||
"watch": [
|
|
||||||
"restart_update.json",
|
|
||||||
"restart_general.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": ""
|
"url": ""
|
||||||
@@ -28,33 +17,44 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": ""
|
"url": ""
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16",
|
||||||
|
"npm": "6.14.4"
|
||||||
|
},
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.6.1",
|
||||||
|
"@discordjs/core": "^0.5.2",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"async-mutex": "^0.3.1",
|
"async-mutex": "^0.4.0",
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"bcryptjs": "^2.4.0",
|
"bcryptjs": "^2.4.0",
|
||||||
|
"command-exists": "^1.2.9",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
"express": "^4.17.3",
|
"execa": "^5.1.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"feed": "^4.2.2",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"gotify": "^1.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"merge-files": "^0.1.2",
|
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.4",
|
||||||
"mongodb": "^3.6.9",
|
"mongodb": "^3.6.9",
|
||||||
"multer": "^1.4.2",
|
"multer": "1.4.5-lts.1",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"node-id3": "^0.1.14",
|
"node-id3": "^0.2.6",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemon": "^2.0.7",
|
"node-telegram-bot-api": "^0.61.0",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.6.0",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
"read-last-lines": "^1.7.2",
|
"read-last-lines": "^1.7.2",
|
||||||
"rxjs": "^7.3.0",
|
"rxjs": "^7.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.0.6",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.7.2",
|
"winston": "^3.7.2",
|
||||||
"xmlbuilder2": "^3.0.2",
|
"xmlbuilder2": "^3.0.2"
|
||||||
"youtube-dl": "^3.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ module.exports = {
|
|||||||
out_file: "/dev/null",
|
out_file: "/dev/null",
|
||||||
error_file: "/dev/null"
|
error_file: "/dev/null"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
|
const archive_api = require('./archive');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
const downloader_api = require('./downloader');
|
const downloader_api = require('./downloader');
|
||||||
|
|
||||||
async function subscribe(sub, user_uid = null) {
|
exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
||||||
const result_obj = {
|
const result_obj = {
|
||||||
success: false,
|
success: false,
|
||||||
error: ''
|
error: ''
|
||||||
};
|
};
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
// sub should just have url and name. here we will get isPlaylist and path
|
// sub should just have url and name. here we will get isPlaylist and path
|
||||||
sub.isPlaylist = sub.url.includes('playlist');
|
sub.isPlaylist = sub.isPlaylist || sub.url.includes('playlist');
|
||||||
sub.videos = [];
|
sub.videos = [];
|
||||||
|
|
||||||
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
||||||
@@ -31,12 +33,13 @@ async function subscribe(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub['user_uid'] = user_uid ? user_uid : undefined;
|
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||||
await db_api.insertRecordIntoTable('subscriptions', sub);
|
await db_api.insertRecordIntoTable('subscriptions', JSON.parse(JSON.stringify(sub)));
|
||||||
|
|
||||||
let success = await getSubscriptionInfo(sub);
|
let success = skip_get_info ? true : await getSubscriptionInfo(sub);
|
||||||
|
exports.writeSubscriptionMetadata(sub);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
getVideosForSub(sub, user_uid);
|
if (!sub.paused) exports.getVideosForSub(sub.id);
|
||||||
} else {
|
} else {
|
||||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||||
}
|
}
|
||||||
@@ -60,52 +63,41 @@ async function getSubscriptionInfo(sub) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
const {parsed_output, err} = await callback;
|
||||||
if (debugMode) {
|
if (err) {
|
||||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
logger.error(err.stderr);
|
||||||
}
|
return false;
|
||||||
if (err) {
|
}
|
||||||
logger.error(err.stderr);
|
logger.verbose('Subscribe: got info for subscription ' + sub.id);
|
||||||
resolve(false);
|
for (const output_json of parsed_output) {
|
||||||
} else if (output) {
|
if (!output_json) {
|
||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
continue;
|
||||||
logger.verbose('Could not get info for ' + sub.id);
|
}
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!sub.name) {
|
|
||||||
if (sub.isPlaylist) {
|
|
||||||
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
|
||||||
} else {
|
|
||||||
sub.name = output_json.uploader;
|
|
||||||
}
|
|
||||||
// if it's now valid, update
|
|
||||||
if (sub.name) {
|
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub.name});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: get even more info
|
if (!sub.name) {
|
||||||
|
if (sub.isPlaylist) {
|
||||||
resolve(true);
|
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
||||||
}
|
} else {
|
||||||
resolve(false);
|
sub.name = output_json.uploader;
|
||||||
}
|
}
|
||||||
});
|
// if it's now valid, update
|
||||||
});
|
if (sub.name) {
|
||||||
|
let sub_name = sub.name;
|
||||||
|
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
|
||||||
|
if (sub_name_exists) sub_name += ` - ${sub.id}`;
|
||||||
|
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
exports.unsubscribe = async (sub_id, deleteMode, user_uid = null) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -128,6 +120,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await killSubDownloads(sub_id, true);
|
||||||
await db_api.removeRecord('subscriptions', {id: id});
|
await db_api.removeRecord('subscriptions', {id: id});
|
||||||
await db_api.removeAllRecords('files', {sub_id: id});
|
await db_api.removeAllRecords('files', {sub_id: id});
|
||||||
|
|
||||||
@@ -138,28 +131,25 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
|
|
||||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
if (deleteMode && (await fs.pathExists(appendedBasePath))) {
|
if (deleteMode && (await fs.pathExists(appendedBasePath))) {
|
||||||
if (sub.archive && (await fs.pathExists(sub.archive))) {
|
|
||||||
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
|
||||||
// deletes archive if it exists
|
|
||||||
// TODO: Keep entries in blacklist_video.txt by moving them to a global blacklist
|
|
||||||
if (await fs.pathExists(archive_file_path)) {
|
|
||||||
await fs.unlink(archive_file_path);
|
|
||||||
}
|
|
||||||
await fs.rmdir(sub.archive);
|
|
||||||
}
|
|
||||||
await fs.remove(appendedBasePath);
|
await fs.remove(appendedBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
|
||||||
|
if (typeof sub === 'string') {
|
||||||
|
// TODO: fix bad workaround where sub is a sub_id
|
||||||
|
sub = await db_api.getRecord('subscriptions', {sub_id: sub});
|
||||||
|
}
|
||||||
// TODO: combine this with deletefile
|
// TODO: combine this with deletefile
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
basePath = user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions')
|
basePath = user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions')
|
||||||
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
||||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
const name = file;
|
const name = file;
|
||||||
let retrievedID = null;
|
let retrievedID = null;
|
||||||
|
let retrievedExtractor = null;
|
||||||
|
|
||||||
await db_api.removeRecord('files', {uid: file_uid});
|
await db_api.removeRecord('files', {uid: file_uid});
|
||||||
|
|
||||||
@@ -178,7 +168,9 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (jsonExists) {
|
if (jsonExists) {
|
||||||
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
|
const info_json = fs.readJSONSync(jsonPath);
|
||||||
|
retrievedID = info_json['id'];
|
||||||
|
retrievedExtractor = info_json['extractor'];
|
||||||
await fs.unlink(jsonPath);
|
await fs.unlink(jsonPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,12 +188,14 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||||
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
if (deleteForever) {
|
||||||
const archive_path = path.join(sub.archive, 'archive.txt')
|
// ensure video is in the archives
|
||||||
// if archive exists, remove line with video ID
|
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
|
||||||
if (await fs.pathExists(archive_path)) {
|
if (!exists_in_archive) {
|
||||||
utils.removeIDFromArchive(archive_path, retrievedID);
|
await archive_api.addToArchive(retrievedExtractor, retrievedID, sub.type, file.title, user_uid, sub.id);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -211,12 +205,76 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideosForSub(sub, user_uid = null) {
|
let current_sub_index = 0; // To keep track of the current subscription
|
||||||
const latest_sub_obj = await getSubscription(sub.id);
|
exports.watchSubscriptionsInterval = async () => {
|
||||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
const subscriptions_check_interval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
let parent_interval = setInterval(() => watchSubscriptions(), subscriptions_check_interval*1000);
|
||||||
|
watchSubscriptions();
|
||||||
|
config_api.config_updated.subscribe(change => {
|
||||||
|
if (!change) return;
|
||||||
|
if (change['key'] === 'ytdl_subscriptions_check_interval' || change['key'] === 'ytdl_multi_user_mode') {
|
||||||
|
current_sub_index = 0; // TODO: start after the last sub check
|
||||||
|
logger.verbose('Resetting sub check schedule due to config change');
|
||||||
|
clearInterval(parent_interval);
|
||||||
|
const new_interval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
parent_interval = setInterval(() => watchSubscriptions(), new_interval*1000);
|
||||||
|
watchSubscriptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchSubscriptions() {
|
||||||
|
const subscription_ids = await getValidSubscriptionsToCheck();
|
||||||
|
if (subscription_ids.length === 0) {
|
||||||
|
logger.info('Skipping subscription check as no valid subscriptions exist.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkSubscription(subscription_ids[current_sub_index]);
|
||||||
|
current_sub_index = (current_sub_index + 1) % subscription_ids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSubscription(sub_id) {
|
||||||
|
let sub = await exports.getSubscription(sub_id);
|
||||||
|
|
||||||
|
// don't check the sub if the last check for the same subscription has not completed
|
||||||
|
if (sub.downloading) {
|
||||||
|
logger.verbose(`Subscription: skipped checking ${sub.name} as it's downloading videos.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sub.name) {
|
||||||
|
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exports.getVideosForSub(sub.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getValidSubscriptionsToCheck() {
|
||||||
|
const subscriptions = await exports.getAllSubscriptions();
|
||||||
|
|
||||||
|
if (!subscriptions) return;
|
||||||
|
|
||||||
|
// auto pause deprecated streamingOnly mode
|
||||||
|
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
|
||||||
|
exports.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
|
||||||
|
|
||||||
|
const valid_subscription_ids = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly).map(sub => sub.id);
|
||||||
|
return valid_subscription_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getVideosForSub = async (sub_id) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
|
if (!sub || sub['downloading']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getVideosForSub(sub);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getVideosForSub(sub) {
|
||||||
|
const user_uid = sub['user_uid'];
|
||||||
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
||||||
|
|
||||||
// get basePath
|
// get basePath
|
||||||
@@ -232,84 +290,54 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
||||||
|
|
||||||
// get videos
|
// get videos
|
||||||
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
|
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
let {child_process, callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
updateSubscriptionProperty(sub, {child_process: child_process}, user_uid);
|
||||||
// cleanup
|
const {parsed_output, err} = await callback;
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: false, child_process: null}, user_uid);
|
||||||
|
if (!parsed_output) {
|
||||||
|
logger.error('Subscription check failed!');
|
||||||
|
if (err) logger.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
// remove temporary archive file if it exists
|
||||||
if (err && !output) {
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
logger.error(err.stderr ? err.stderr : err.message);
|
const archive_exists = await fs.pathExists(archive_path);
|
||||||
if (err.stderr.includes('This video is unavailable')) {
|
if (archive_exists) {
|
||||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
await fs.unlink(archive_path);
|
||||||
try {
|
}
|
||||||
// TODO: reimplement
|
|
||||||
|
|
||||||
// const outputs = err.stdout.split(/\r\n|\r|\n/);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
// for (let i = 0; i < outputs.length; i++) {
|
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
|
||||||
// const output = JSON.parse(outputs[i]);
|
return files_to_download;
|
||||||
// await handleOutputJSON(sub, output, i === 0, multiUserMode)
|
|
||||||
// if (err.stderr.includes(output['id']) && archive_path) {
|
|
||||||
// // we found a video that errored! add it to the archive to prevent future errors
|
|
||||||
// if (sub.archive) {
|
|
||||||
// archive_dir = sub.archive;
|
|
||||||
// archive_path = path.join(archive_dir, 'archive.txt')
|
|
||||||
// fs.appendFileSync(archive_path, output['id']);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
} catch(e) {
|
|
||||||
logger.error('Backup method failed. See error below:');
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(false);
|
|
||||||
} else if (output) {
|
|
||||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
|
||||||
await setFreshUploads(sub, user_uid);
|
|
||||||
checkVideosForFreshUploads(sub, user_uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
|
||||||
logger.verbose('No additional videos to download for ' + sub.name);
|
|
||||||
resolve(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const output_jsons = [];
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
output_jsons.push(output_json);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
|
||||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
|
||||||
|
|
||||||
for (let j = 0; j < files_to_download.length; j++) {
|
|
||||||
const file_to_download = files_to_download[j];
|
|
||||||
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(files_to_download);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
logger.error(err);
|
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
async function handleOutputJSON(output_jsons, sub, user_uid) {
|
||||||
|
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||||
|
await setFreshUploads(sub, user_uid);
|
||||||
|
checkVideosForFreshUploads(sub, user_uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output_jsons.length === 0 || (output_jsons.length === 1 && output_jsons[0] === '')) {
|
||||||
|
logger.verbose('No additional videos to download for ' + sub.name);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||||
|
const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||||
|
|
||||||
|
for (let j = 0; j < files_to_download.length; j++) {
|
||||||
|
const file_to_download = files_to_download[j];
|
||||||
|
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
|
||||||
|
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, [file_to_download]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files_to_download;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -319,10 +347,10 @@ function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
|||||||
let default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
let default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||||
|
|
||||||
const base_download_options = {
|
const base_download_options = {
|
||||||
selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
|
maxHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
|
||||||
customFileFolderPath: getAppendedBasePath(sub, basePath),
|
customFileFolderPath: getAppendedBasePath(sub, basePath),
|
||||||
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
|
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
|
||||||
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
|
customArchivePath: path.join(basePath, 'archives', sub.name),
|
||||||
additionalArgs: sub.custom_args
|
additionalArgs: sub.custom_args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,8 +365,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
else
|
else
|
||||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
||||||
|
|
||||||
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
|
||||||
const file_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
const file_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||||
@@ -364,6 +390,16 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
downloadConfig.push(...qualityPath)
|
downloadConfig.push(...qualityPath)
|
||||||
|
|
||||||
|
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||||
|
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||||
|
const archive_count = archive_text.split('\n').length - 1;
|
||||||
|
if (archive_count > 0) {
|
||||||
|
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_count} entries.`)
|
||||||
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
|
await fs.writeFile(archive_path, archive_text);
|
||||||
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
|
}
|
||||||
|
|
||||||
if (sub.custom_args) {
|
if (sub.custom_args) {
|
||||||
const customArgsArray = sub.custom_args.split(',,');
|
const customArgsArray = sub.custom_args.split(',,');
|
||||||
if (customArgsArray.indexOf('-f') !== -1) {
|
if (customArgsArray.indexOf('-f') !== -1) {
|
||||||
@@ -374,26 +410,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
downloadConfig.push(...customArgsArray);
|
downloadConfig.push(...customArgsArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
let archive_dir = null;
|
|
||||||
let archive_path = null;
|
|
||||||
|
|
||||||
if (useArchive && !redownload) {
|
|
||||||
if (sub.archive) {
|
|
||||||
archive_dir = sub.archive;
|
|
||||||
if (sub.type && sub.type === 'audio') {
|
|
||||||
archive_path = path.join(archive_dir, 'merged_audio.txt');
|
|
||||||
} else {
|
|
||||||
archive_path = path.join(archive_dir, 'merged_video.txt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
downloadConfig.push('--download-archive', archive_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if streaming only mode, just get the list of videos
|
|
||||||
if (sub.streamingOnly) {
|
|
||||||
downloadConfig = ['-f', 'best', '--dump-json'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub.timerange && !redownload) {
|
if (sub.timerange && !redownload) {
|
||||||
downloadConfig.push('--dateafter', sub.timerange);
|
downloadConfig.push('--dateafter', sub.timerange);
|
||||||
}
|
}
|
||||||
@@ -416,11 +432,13 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
downloadConfig.push('-r', rate_limit);
|
downloadConfig.push('-r', rate_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
if (default_downloader === 'yt-dlp') {
|
if (default_downloader === 'yt-dlp') {
|
||||||
downloadConfig.push('--no-clean-infojson');
|
downloadConfig.push('--no-clean-info-json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadConfig = utils.filterArgs(downloadConfig, ['--write-comments']);
|
||||||
|
|
||||||
return downloadConfig;
|
return downloadConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,40 +452,79 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
if (file_with_path_exists) {
|
if (file_with_path_exists) {
|
||||||
// or maybe just overwrite???
|
// or maybe just overwrite???
|
||||||
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
|
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
|
||||||
|
if (exists_in_archive) continue;
|
||||||
|
|
||||||
files_to_download.push(output_json);
|
files_to_download.push(output_json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files_to_download;
|
return files_to_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.cancelCheckSubscription = async (sub_id) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
|
if (!sub['downloading'] && !sub['child_process']) {
|
||||||
|
logger.error('Failed to cancel subscription check, verify that it is still running!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function getSubscriptions(user_uid = null) {
|
// if check is ongoing
|
||||||
|
if (sub['child_process']) {
|
||||||
|
const child_process = sub['child_process'];
|
||||||
|
youtubedl_api.killYoutubeDLProcess(child_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel activate video downloads
|
||||||
|
await killSubDownloads(sub_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killSubDownloads(sub_id, remove_downloads = false) {
|
||||||
|
const sub_downloads = await db_api.getRecords('download_queue', {sub_id: sub_id});
|
||||||
|
for (const sub_download of sub_downloads) {
|
||||||
|
if (sub_download['running'])
|
||||||
|
await downloader_api.cancelDownload(sub_download['uid']);
|
||||||
|
if (remove_downloads)
|
||||||
|
await db_api.removeRecord('download_queue', {uid: sub_download['uid']});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getSubscriptions = async (user_uid = null) => {
|
||||||
|
// TODO: fix issue where the downloading property may not match getSubscription()
|
||||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllSubscriptions() {
|
exports.getAllSubscriptions = async () => {
|
||||||
const all_subs = await db_api.getRecords('subscriptions');
|
const all_subs = await db_api.getRecords('subscriptions');
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscription(subID) {
|
exports.getSubscription = async (subID) => {
|
||||||
return await db_api.getRecord('subscriptions', {id: subID});
|
// stringify and parse because we may override the 'downloading' property
|
||||||
|
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
|
||||||
|
// now with the download_queue, we may need to override 'downloading'
|
||||||
|
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: subID}, true);
|
||||||
|
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
||||||
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscriptionByName(subName, user_uid = null) {
|
exports.getSubscriptionByName = async (subName, user_uid = null) => {
|
||||||
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSubscription(sub) {
|
exports.updateSubscription = async (sub) => {
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
||||||
|
exports.writeSubscriptionMetadata(sub);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
|
||||||
subs.forEach(async sub => {
|
subs.forEach(async sub => {
|
||||||
await updateSubscriptionProperty(sub, assignment_obj, sub.user_uid);
|
await updateSubscriptionProperty(sub, assignment_obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,8 +534,19 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.writeSubscriptionMetadata = (sub) => {
|
||||||
|
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
|
||||||
|
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||||
|
|
||||||
|
fs.ensureDirSync(appendedBasePath);
|
||||||
|
fs.writeJSONSync(metadata_path, sub);
|
||||||
|
}
|
||||||
|
|
||||||
async function setFreshUploads(sub) {
|
async function setFreshUploads(sub) {
|
||||||
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
|
if (!sub_files) return;
|
||||||
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||||
sub_files.forEach(async file => {
|
sub_files.forEach(async file => {
|
||||||
if (current_date === file['upload_date'].replace(/-/g, '')) {
|
if (current_date === file['upload_date'].replace(/-/g, '')) {
|
||||||
@@ -504,24 +572,22 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
||||||
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
|
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
|
||||||
// simulate a download to verify that a better version exists
|
// simulate a download to verify that a better version exists
|
||||||
youtubedl.getInfo(file_obj['url'], downloadConfig, async (err, output) => {
|
|
||||||
if (err) {
|
const info = await downloader_api.getVideoInfoByURL(file_obj['url'], downloadConfig);
|
||||||
// video is not available anymore for whatever reason
|
if (info && info.length === 1) {
|
||||||
} else if (output) {
|
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
||||||
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
if (info[metric_to_compare] > file_obj[metric_to_compare]) {
|
||||||
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
|
// download new video as the simulated one is better
|
||||||
// download new video as the simulated one is better
|
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(file_obj['url'], downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
const {parsed_output, err} = await callback;
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
||||||
} else if (output) {
|
} else if (parsed_output) {
|
||||||
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
|
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${info[metric_to_compare]}`);
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]});
|
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: info[metric_to_compare]});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,17 +596,3 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
function getAppendedBasePath(sub, base_path) {
|
function getAppendedBasePath(sub, base_path) {
|
||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getSubscription : getSubscription,
|
|
||||||
getSubscriptionByName : getSubscriptionByName,
|
|
||||||
getSubscriptions : getSubscriptions,
|
|
||||||
getAllSubscriptions : getAllSubscriptions,
|
|
||||||
updateSubscription : updateSubscription,
|
|
||||||
subscribe : subscribe,
|
|
||||||
unsubscribe : unsubscribe,
|
|
||||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
|
||||||
getVideosForSub : getVideosForSub,
|
|
||||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
|
||||||
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
|
||||||
}
|
|
||||||
|
|||||||
160
backend/tasks.js
160
backend/tasks.js
@@ -1,8 +1,17 @@
|
|||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
const notifications_api = require('./notifications');
|
||||||
const youtubedl_api = require('./youtube-dl');
|
const youtubedl_api = require('./youtube-dl');
|
||||||
|
const archive_api = require('./archive');
|
||||||
|
const files_api = require('./files');
|
||||||
|
const subscriptions_api = require('./subscriptions');
|
||||||
|
const config_api = require('./config');
|
||||||
|
const auth_api = require('./authentication/auth');
|
||||||
|
const utils = require('./utils');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const logger = require('./logger');
|
const path = require('path');
|
||||||
const scheduler = require('node-schedule');
|
const scheduler = require('node-schedule');
|
||||||
|
|
||||||
const TASKS = {
|
const TASKS = {
|
||||||
@@ -18,7 +27,7 @@ const TASKS = {
|
|||||||
job: null
|
job: null
|
||||||
},
|
},
|
||||||
missing_db_records: {
|
missing_db_records: {
|
||||||
run: db_api.importUnregisteredFiles,
|
run: files_api.importUnregisteredFiles,
|
||||||
title: 'Import missing DB records',
|
title: 'Import missing DB records',
|
||||||
job: null
|
job: null
|
||||||
},
|
},
|
||||||
@@ -33,6 +42,33 @@ const TASKS = {
|
|||||||
confirm: youtubedl_api.updateYoutubeDL,
|
confirm: youtubedl_api.updateYoutubeDL,
|
||||||
title: 'Update youtube-dl',
|
title: 'Update youtube-dl',
|
||||||
job: null
|
job: null
|
||||||
|
},
|
||||||
|
delete_old_files: {
|
||||||
|
run: checkForAutoDeleteFiles,
|
||||||
|
confirm: autoDeleteFiles,
|
||||||
|
title: 'Delete old files',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
import_legacy_archives: {
|
||||||
|
run: archive_api.importArchives,
|
||||||
|
title: 'Import legacy archives',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
rebuild_database: {
|
||||||
|
run: rebuildDB,
|
||||||
|
title: 'Rebuild database',
|
||||||
|
job: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
all: {
|
||||||
|
auto_confirm: false
|
||||||
|
},
|
||||||
|
delete_old_files: {
|
||||||
|
blacklist_files: false,
|
||||||
|
blacklist_subscription_files: false,
|
||||||
|
threshold_days: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +81,7 @@ function scheduleJob(task_key, schedule) {
|
|||||||
const dayOfWeek = schedule['data']['dayOfWeek'] != null ? schedule['data']['dayOfWeek'] : null;
|
const dayOfWeek = schedule['data']['dayOfWeek'] != null ? schedule['data']['dayOfWeek'] : null;
|
||||||
const hour = schedule['data']['hour'] != null ? schedule['data']['hour'] : null;
|
const hour = schedule['data']['hour'] != null ? schedule['data']['hour'] : null;
|
||||||
const minute = schedule['data']['minute'] != null ? schedule['data']['minute'] : null;
|
const minute = schedule['data']['minute'] != null ? schedule['data']['minute'] : null;
|
||||||
converted_schedule = new scheduler.RecurrenceRule(null, null, null, dayOfWeek, hour, minute);
|
converted_schedule = new scheduler.RecurrenceRule(null, null, null, dayOfWeek, hour, minute, undefined, schedule['data']['tz'] ? schedule['data']['tz'] : undefined);
|
||||||
} else {
|
} else {
|
||||||
logger.error(`Failed to schedule job '${task_key}' as the type '${schedule['type']}' is invalid.`)
|
logger.error(`Failed to schedule job '${task_key}' as the type '${schedule['type']}' is invalid.`)
|
||||||
return null;
|
return null;
|
||||||
@@ -57,7 +93,7 @@ function scheduleJob(task_key, schedule) {
|
|||||||
logger.verbose(`Skipping running task ${task_state['key']} as it is already in progress.`);
|
logger.verbose(`Skipping running task ${task_state['key']} as it is already in progress.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove schedule if it's a one-time task
|
// remove schedule if it's a one-time task
|
||||||
if (task_state['schedule']['type'] !== 'recurring') await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
|
if (task_state['schedule']['type'] !== 'recurring') await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
|
||||||
// we're just "running" the task, any confirmation should be user-initiated
|
// we're just "running" the task, any confirmation should be user-initiated
|
||||||
@@ -77,9 +113,10 @@ exports.setupTasks = async () => {
|
|||||||
const tasks_keys = Object.keys(TASKS);
|
const tasks_keys = Object.keys(TASKS);
|
||||||
for (let i = 0; i < tasks_keys.length; i++) {
|
for (let i = 0; i < tasks_keys.length; i++) {
|
||||||
const task_key = tasks_keys[i];
|
const task_key = tasks_keys[i];
|
||||||
|
const mergedDefaultOptions = Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {});
|
||||||
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
|
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
|
||||||
if (!task_in_db) {
|
if (!task_in_db) {
|
||||||
// insert task metadata into table if missing
|
// insert task metadata into table if missing, eventually move title to UI
|
||||||
await db_api.insertRecordIntoTable('tasks', {
|
await db_api.insertRecordIntoTable('tasks', {
|
||||||
key: task_key,
|
key: task_key,
|
||||||
title: TASKS[task_key]['title'],
|
title: TASKS[task_key]['title'],
|
||||||
@@ -90,9 +127,19 @@ exports.setupTasks = async () => {
|
|||||||
data: null,
|
data: null,
|
||||||
error: null,
|
error: null,
|
||||||
schedule: null,
|
schedule: null,
|
||||||
options: {}
|
options: Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// verify all options exist in task
|
||||||
|
for (const key of Object.keys(mergedDefaultOptions)) {
|
||||||
|
const option_key = `options.${key}`;
|
||||||
|
// Remove any potential mangled option keys (#861)
|
||||||
|
await db_api.removePropertyFromRecord('tasks', {key: task_key}, {[option_key]: true});
|
||||||
|
if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) {
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reset task if necessary
|
// reset task if necessary
|
||||||
await db_api.updateRecord('tasks', {key: task_key}, {running: false, confirming: false});
|
await db_api.updateRecord('tasks', {key: task_key}, {running: false, confirming: false});
|
||||||
|
|
||||||
@@ -123,15 +170,23 @@ exports.executeTask = async (task_key) => {
|
|||||||
|
|
||||||
exports.executeRun = async (task_key) => {
|
exports.executeRun = async (task_key) => {
|
||||||
logger.verbose(`Running task ${task_key}`);
|
logger.verbose(`Running task ${task_key}`);
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {error: null})
|
||||||
// don't set running to true when backup up DB as it will be stick "running" if restored
|
// don't set running to true when backup up DB as it will be stick "running" if restored
|
||||||
if (task_key !== 'backup_local_db') await db_api.updateRecord('tasks', {key: task_key}, {running: true});
|
if (task_key !== 'backup_local_db') await db_api.updateRecord('tasks', {key: task_key}, {running: true});
|
||||||
const data = await TASKS[task_key].run();
|
const data = await TASKS[task_key].run();
|
||||||
await db_api.updateRecord('tasks', {key: task_key}, {data: TASKS[task_key]['confirm'] ? data : null, last_ran: Date.now()/1000, running: false});
|
await db_api.updateRecord('tasks', {key: task_key}, {data: TASKS[task_key]['confirm'] ? data : null, last_ran: Date.now()/1000, running: false});
|
||||||
logger.verbose(`Finished running task ${task_key}`);
|
logger.verbose(`Finished running task ${task_key}`);
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
await notifications_api.sendTaskNotification(task_obj, false);
|
||||||
|
|
||||||
|
if (task_obj['options'] && task_obj['options']['auto_confirm']) {
|
||||||
|
exports.executeConfirm(task_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.executeConfirm = async (task_key) => {
|
exports.executeConfirm = async (task_key) => {
|
||||||
logger.verbose(`Confirming task ${task_key}`);
|
logger.verbose(`Confirming task ${task_key}`);
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {error: null})
|
||||||
if (!TASKS[task_key]['confirm']) {
|
if (!TASKS[task_key]['confirm']) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -141,6 +196,7 @@ exports.executeConfirm = async (task_key) => {
|
|||||||
await TASKS[task_key].confirm(data);
|
await TASKS[task_key].confirm(data);
|
||||||
await db_api.updateRecord('tasks', {key: task_key}, {confirming: false, last_confirmed: Date.now()/1000, data: null});
|
await db_api.updateRecord('tasks', {key: task_key}, {confirming: false, last_confirmed: Date.now()/1000, data: null});
|
||||||
logger.verbose(`Finished confirming task ${task_key}`);
|
logger.verbose(`Finished confirming task ${task_key}`);
|
||||||
|
await notifications_api.sendTaskNotification(task_obj, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateTaskSchedule = async (task_key, schedule) => {
|
exports.updateTaskSchedule = async (task_key, schedule) => {
|
||||||
@@ -148,6 +204,7 @@ exports.updateTaskSchedule = async (task_key, schedule) => {
|
|||||||
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
|
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
|
||||||
if (TASKS[task_key]['job']) {
|
if (TASKS[task_key]['job']) {
|
||||||
TASKS[task_key]['job'].cancel();
|
TASKS[task_key]['job'].cancel();
|
||||||
|
TASKS[task_key]['job'] = null;
|
||||||
}
|
}
|
||||||
if (schedule) {
|
if (schedule) {
|
||||||
TASKS[task_key]['job'] = scheduleJob(task_key, schedule);
|
TASKS[task_key]['job'] = scheduleJob(task_key, schedule);
|
||||||
@@ -192,4 +249,95 @@ async function removeDuplicates(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auto delete files
|
||||||
|
|
||||||
|
async function checkForAutoDeleteFiles() {
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: 'delete_old_files'});
|
||||||
|
if (!task_obj['options'] || !task_obj['options']['threshold_days']) {
|
||||||
|
const error_message = 'Failed to do delete check because no limit was set!';
|
||||||
|
logger.error(error_message);
|
||||||
|
await db_api.updateRecord('tasks', {key: 'delete_old_files'}, {error: error_message})
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const delete_older_than_timestamp = Date.now() - task_obj['options']['threshold_days']*86400*1000;
|
||||||
|
const files = (await db_api.getRecords('files', {registered: {$lt: delete_older_than_timestamp}}))
|
||||||
|
const files_to_remove = files.map(file => {return {uid: file.uid, sub_id: file.sub_id}});
|
||||||
|
return {files_to_remove: files_to_remove};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoDeleteFiles(data) {
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: 'delete_old_files'});
|
||||||
|
if (data['files_to_remove']) {
|
||||||
|
logger.info(`Removing ${data['files_to_remove'].length} old files!`);
|
||||||
|
for (let i = 0; i < data['files_to_remove'].length; i++) {
|
||||||
|
const file_to_remove = data['files_to_remove'][i];
|
||||||
|
await files_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebuildDB() {
|
||||||
|
await db_api.backupDB();
|
||||||
|
let subs_to_add = await guessSubscriptions(false);
|
||||||
|
subs_to_add = subs_to_add.concat(await guessSubscriptions(true));
|
||||||
|
const users_to_add = await guessUsers();
|
||||||
|
for (const user_to_add of users_to_add) {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
|
||||||
|
const user_exists = await db_api.getRecord('users', {uid: user_to_add});
|
||||||
|
if (!user_exists) {
|
||||||
|
await auth_api.registerUser(user_to_add, user_to_add, 'password');
|
||||||
|
logger.info(`Regenerated user ${user_to_add}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user_channel_subs = await guessSubscriptions(false, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||||
|
const user_playlist_subs = await guessSubscriptions(true, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||||
|
subs_to_add = subs_to_add.concat(user_channel_subs, user_playlist_subs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sub_to_add of subs_to_add) {
|
||||||
|
const sub_exists = !!(await subscriptions_api.getSubscriptionByName(sub_to_add['name'], sub_to_add['user_uid']));
|
||||||
|
// TODO: we shouldn't be creating this here
|
||||||
|
const new_sub = Object.assign({}, sub_to_add, {paused: true});
|
||||||
|
if (!sub_exists) {
|
||||||
|
await subscriptions_api.subscribe(new_sub, sub_to_add['user_uid'], true);
|
||||||
|
logger.info(`Regenerated subscription ${sub_to_add['name']}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Importing unregistered files`);
|
||||||
|
await files_api.importUnregisteredFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessUsers = async () => {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
const userPaths = await utils.getDirectoriesInDirectory(usersFileFolder);
|
||||||
|
return userPaths.map(userPath => path.basename(userPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessSubscriptions = async (isPlaylist, basePath = null) => {
|
||||||
|
const guessed_subs = [];
|
||||||
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
const subsSubPath = basePath ? path.join(basePath, 'subscriptions') : subscriptionsFileFolder;
|
||||||
|
const subsPath = path.join(subsSubPath, isPlaylist ? 'playlists' : 'channels');
|
||||||
|
|
||||||
|
const subs = await utils.getDirectoriesInDirectory(subsPath);
|
||||||
|
for (const subPath of subs) {
|
||||||
|
const sub_backup_path = path.join(subPath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||||
|
if (!fs.existsSync(sub_backup_path)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sub_backup = fs.readJSONSync(sub_backup_path)
|
||||||
|
delete sub_backup['_id'];
|
||||||
|
guessed_subs.push(sub_backup);
|
||||||
|
} catch(err) {
|
||||||
|
logger.warn(`Failed to reimport subscription in path ${subPath}`)
|
||||||
|
logger.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return guessed_subs;
|
||||||
|
}
|
||||||
|
|
||||||
exports.TASKS = TASKS;
|
exports.TASKS = TASKS;
|
||||||
File diff suppressed because one or more lines are too long
1
backend/test/sample_mp3.info.json
Normal file
1
backend/test/sample_mp3.info.json
Normal file
File diff suppressed because one or more lines are too long
1
backend/test/sample_mp4.info.json
Normal file
1
backend/test/sample_mp4.info.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,90 +1,74 @@
|
|||||||
var moment = require('moment');
|
|
||||||
var Axios = require('axios');
|
|
||||||
var fs = require('fs-extra')
|
|
||||||
var path = require('path');
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
async function getCommentsForVOD(clientID, vodId) {
|
const moment = require('moment');
|
||||||
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
|
const fs = require('fs-extra')
|
||||||
batch,
|
const path = require('path');
|
||||||
cursor;
|
const { promisify } = require('util');
|
||||||
|
const child_process = require('child_process');
|
||||||
|
const commandExistsSync = require('command-exists').sync;
|
||||||
|
|
||||||
let comments = null;
|
async function getCommentsForVOD(vodId) {
|
||||||
|
const exec = promisify(child_process.exec);
|
||||||
try {
|
|
||||||
do {
|
// Reject invalid params to prevent command injection attack
|
||||||
batch = (await Axios.get(url, {
|
if (!vodId.match(/^[0-9a-z]+$/)) {
|
||||||
headers: {
|
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
|
||||||
'Client-ID': clientID,
|
return null;
|
||||||
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
|
|
||||||
'Content-Type': 'application/json; charset=UTF-8',
|
|
||||||
}
|
|
||||||
})).data;
|
|
||||||
|
|
||||||
const str = batch.comments.map(c => {
|
|
||||||
let {
|
|
||||||
created_at: msgCreated,
|
|
||||||
content_offset_seconds: timestamp,
|
|
||||||
commenter: {
|
|
||||||
name,
|
|
||||||
_id,
|
|
||||||
created_at: acctCreated
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
body: msg,
|
|
||||||
user_color: user_color
|
|
||||||
}
|
|
||||||
} = c;
|
|
||||||
|
|
||||||
const timestamp_str = moment.duration(timestamp, 'seconds')
|
|
||||||
.toISOString()
|
|
||||||
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
|
||||||
(_, ...ms) => {
|
|
||||||
const seg = v => v ? v.padStart(2, '0') : '00';
|
|
||||||
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
acctCreated = moment(acctCreated).utc();
|
|
||||||
msgCreated = moment(msgCreated).utc();
|
|
||||||
|
|
||||||
if (!comments) comments = [];
|
|
||||||
|
|
||||||
comments.push({
|
|
||||||
timestamp: timestamp,
|
|
||||||
timestamp_str: timestamp_str,
|
|
||||||
name: name,
|
|
||||||
message: msg,
|
|
||||||
user_color: user_color
|
|
||||||
});
|
|
||||||
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
|
|
||||||
// return line;
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
cursor = batch._next;
|
|
||||||
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
|
|
||||||
await new Promise(res => setTimeout(res, 300));
|
|
||||||
} while (cursor);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return comments;
|
const is_windows = process.platform === 'win32';
|
||||||
|
const cliExt = is_windows ? '.exe' : ''
|
||||||
|
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
||||||
|
|
||||||
|
if (!commandExistsSync(cliPath)) {
|
||||||
|
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await exec(`${cliPath} chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
|
||||||
|
|
||||||
|
if (result['stderr']) {
|
||||||
|
logger.error(`Failed to download twitch comments for ${vodId}`);
|
||||||
|
logger.error(result['stderr']);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const temp_chat_path = path.join('appdata', `${vodId}.json`);
|
||||||
|
|
||||||
|
const raw_json = fs.readJSONSync(temp_chat_path);
|
||||||
|
const new_json = raw_json.comments.map(comment_obj => {
|
||||||
|
return {
|
||||||
|
timestamp: comment_obj.content_offset_seconds,
|
||||||
|
timestamp_str: convertTimestamp(comment_obj.content_offset_seconds),
|
||||||
|
name: comment_obj.commenter.name,
|
||||||
|
message: comment_obj.message.body,
|
||||||
|
user_color: comment_obj.message.user_color
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.unlinkSync(temp_chat_path);
|
||||||
|
|
||||||
|
return new_json;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
let file_path = null;
|
let file_path = null;
|
||||||
|
|
||||||
if (user_uid) {
|
if (user_uid) {
|
||||||
if (sub) {
|
if (sub) {
|
||||||
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||||
} else {
|
} else {
|
||||||
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sub) {
|
if (sub) {
|
||||||
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||||
} else {
|
} else {
|
||||||
file_path = path.join(type, id + '.twitch_chat.json');
|
const typeFolder = config_api.getConfigItem(`ytdl_${type}_folder_path`);
|
||||||
|
file_path = path.join(typeFolder, `${id}.twitch_chat.json`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,23 +80,26 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
|||||||
return chat_file;
|
return chat_file;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
|
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
||||||
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
const chat = await getCommentsForVOD(twitch_api_key, vodId);
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const chat = await getCommentsForVOD(vodId);
|
||||||
|
|
||||||
// save file if needed params are included
|
// save file if needed params are included
|
||||||
let file_path = null;
|
let file_path = null;
|
||||||
if (user_uid) {
|
if (customFileFolderPath) {
|
||||||
|
file_path = path.join(customFileFolderPath, `${id}.twitch_chat.json`)
|
||||||
|
} else if (user_uid) {
|
||||||
if (sub) {
|
if (sub) {
|
||||||
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||||
} else {
|
} else {
|
||||||
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sub) {
|
if (sub) {
|
||||||
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
|
||||||
} else {
|
} else {
|
||||||
file_path = path.join(type, id + '.twitch_chat.json');
|
file_path = path.join(type, `${id}.twitch_chat.json`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +108,14 @@ async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
|
|||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
|
||||||
|
.toISOString()
|
||||||
|
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
||||||
|
(_, ...ms) => {
|
||||||
|
const seg = v => v ? v.padStart(2, '0') : '00';
|
||||||
|
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getCommentsForVOD: getCommentsForVOD,
|
getCommentsForVOD: getCommentsForVOD,
|
||||||
getTwitchChatByFileID: getTwitchChatByFileID,
|
getTwitchChatByFileID: getTwitchChatByFileID,
|
||||||
|
|||||||
307
backend/utils.js
307
backend/utils.js
@@ -4,6 +4,7 @@ const ffmpeg = require('fluent-ffmpeg');
|
|||||||
const archiver = require('archiver');
|
const archiver = require('archiver');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const ProgressBar = require('progress');
|
const ProgressBar = require('progress');
|
||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
@@ -12,7 +13,7 @@ const CONSTS = require('./consts');
|
|||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
// replaces .webm with appropriate extension
|
// replaces .webm with appropriate extension
|
||||||
function getTrueFileName(unfixed_path, type) {
|
exports.getTrueFileName = (unfixed_path, type, force_ext = null) => {
|
||||||
let fixed_path = unfixed_path;
|
let fixed_path = unfixed_path;
|
||||||
|
|
||||||
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||||
@@ -21,19 +22,19 @@ function getTrueFileName(unfixed_path, type) {
|
|||||||
|
|
||||||
|
|
||||||
if (old_ext !== new_ext) {
|
if (old_ext !== new_ext) {
|
||||||
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
unfixed_parts[unfixed_parts.length-1] = force_ext || new_ext;
|
||||||
fixed_path = unfixed_parts.join('.');
|
fixed_path = unfixed_parts.join('.');
|
||||||
}
|
}
|
||||||
return fixed_path;
|
return fixed_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
exports.getDownloadedFilesByType = async (basePath, type, full_metadata = false) => {
|
||||||
// return empty array if the path doesn't exist
|
// return empty array if the path doesn't exist
|
||||||
if (!(await fs.pathExists(basePath))) return [];
|
if (!(await fs.pathExists(basePath))) return [];
|
||||||
|
|
||||||
let files = [];
|
let files = [];
|
||||||
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
||||||
var located_files = await recFindByExt(basePath, ext);
|
var located_files = await exports.recFindByExt(basePath, ext);
|
||||||
for (let i = 0; i < located_files.length; i++) {
|
for (let i = 0; i < located_files.length; i++) {
|
||||||
let file = located_files[i];
|
let file = located_files[i];
|
||||||
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
|
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
|
||||||
@@ -41,33 +42,33 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
|||||||
var stats = await fs.stat(file);
|
var stats = await fs.stat(file);
|
||||||
|
|
||||||
var id = file_path.substring(0, file_path.length-4);
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
var jsonobj = await getJSONByType(type, id, basePath);
|
var jsonobj = await exports.getJSONByType(type, id, basePath);
|
||||||
if (!jsonobj) continue;
|
if (!jsonobj) continue;
|
||||||
if (full_metadata) {
|
if (full_metadata) {
|
||||||
jsonobj['id'] = id;
|
jsonobj['id'] = id;
|
||||||
files.push(jsonobj);
|
files.push(jsonobj);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var upload_date = formatDateString(jsonobj.upload_date);
|
var upload_date = exports.formatDateString(jsonobj.upload_date);
|
||||||
|
|
||||||
var isaudio = type === 'audio';
|
var isaudio = type === 'audio';
|
||||||
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
var file_obj = new exports.File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
||||||
stats.size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
stats.size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||||
files.push(file_obj);
|
files.push(file_obj);
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createContainerZipFile(file_name, container_file_objs) {
|
exports.createContainerZipFile = async (file_name, container_file_objs) => {
|
||||||
const container_files_to_download = [];
|
const container_files_to_download = [];
|
||||||
for (let i = 0; i < container_file_objs.length; i++) {
|
for (let i = 0; i < container_file_objs.length; i++) {
|
||||||
const container_file_obj = container_file_objs[i];
|
const container_file_obj = container_file_objs[i];
|
||||||
container_files_to_download.push(container_file_obj.path);
|
container_files_to_download.push(container_file_obj.path);
|
||||||
}
|
}
|
||||||
return await createZipFile(path.join('appdata', file_name + '.zip'), container_files_to_download);
|
return await exports.createZipFile(path.join('appdata', file_name + '.zip'), container_files_to_download);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createZipFile(zip_file_path, file_paths) {
|
exports.createZipFile = async (zip_file_path, file_paths) => {
|
||||||
let output = fs.createWriteStream(zip_file_path);
|
let output = fs.createWriteStream(zip_file_path);
|
||||||
|
|
||||||
var archive = archiver('zip', {
|
var archive = archiver('zip', {
|
||||||
@@ -91,11 +92,11 @@ async function createZipFile(zip_file_path, file_paths) {
|
|||||||
await archive.finalize();
|
await archive.finalize();
|
||||||
|
|
||||||
// wait a tiny bit for the zip to reload in fs
|
// wait a tiny bit for the zip to reload in fs
|
||||||
await wait(100);
|
await exports.wait(100);
|
||||||
return zip_file_path;
|
return zip_file_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJSONMp4(name, customPath, openReadPerms = false) {
|
exports.getJSONMp4 = (name, customPath, openReadPerms = false) => {
|
||||||
var obj = null; // output
|
var obj = null; // output
|
||||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
|
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
var jsonPath = path.join(customPath, name + ".info.json");
|
var jsonPath = path.join(customPath, name + ".info.json");
|
||||||
@@ -110,7 +111,7 @@ function getJSONMp4(name, customPath, openReadPerms = false) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJSONMp3(name, customPath, openReadPerms = false) {
|
exports.getJSONMp3 = (name, customPath, openReadPerms = false) => {
|
||||||
var obj = null;
|
var obj = null;
|
||||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
var jsonPath = path.join(customPath, name + ".info.json");
|
var jsonPath = path.join(customPath, name + ".info.json");
|
||||||
@@ -127,11 +128,11 @@ function getJSONMp3(name, customPath, openReadPerms = false) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJSON(file_path, type) {
|
exports.getJSON = (file_path, type) => {
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
let obj = null;
|
let obj = null;
|
||||||
var jsonPath = removeFileExtension(file_path) + '.info.json';
|
var jsonPath = exports.removeFileExtension(file_path) + '.info.json';
|
||||||
var alternateJsonPath = removeFileExtension(file_path) + `${ext}.info.json`;
|
var alternateJsonPath = exports.removeFileExtension(file_path) + `${ext}.info.json`;
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
{
|
{
|
||||||
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
@@ -142,12 +143,12 @@ function getJSON(file_path, type) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJSONByType(type, name, customPath, openReadPerms = false) {
|
exports.getJSONByType = (type, name, customPath, openReadPerms = false) => {
|
||||||
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
|
return type === 'audio' ? exports.getJSONMp3(name, customPath, openReadPerms) : exports.getJSONMp4(name, customPath, openReadPerms)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDownloadedThumbnail(file_path) {
|
exports.getDownloadedThumbnail = (file_path) => {
|
||||||
const file_path_no_extension = removeFileExtension(file_path);
|
const file_path_no_extension = exports.removeFileExtension(file_path);
|
||||||
|
|
||||||
let jpgPath = file_path_no_extension + '.jpg';
|
let jpgPath = file_path_no_extension + '.jpg';
|
||||||
let webpPath = file_path_no_extension + '.webp';
|
let webpPath = file_path_no_extension + '.webp';
|
||||||
@@ -163,7 +164,7 @@ function getDownloadedThumbnail(file_path) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpectedFileSize(input_info_jsons) {
|
exports.getExpectedFileSize = (input_info_jsons) => {
|
||||||
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
|
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
|
||||||
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
|
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
|
||||||
|
|
||||||
@@ -172,11 +173,13 @@ function getExpectedFileSize(input_info_jsons) {
|
|||||||
const formats = info_json['format_id'].split('+');
|
const formats = info_json['format_id'].split('+');
|
||||||
let individual_expected_filesize = 0;
|
let individual_expected_filesize = 0;
|
||||||
formats.forEach(format_id => {
|
formats.forEach(format_id => {
|
||||||
info_json.formats.forEach(available_format => {
|
if (info_json.formats !== undefined) {
|
||||||
if (available_format.format_id === format_id && available_format.filesize) {
|
info_json.formats.forEach(available_format => {
|
||||||
individual_expected_filesize += available_format.filesize;
|
if (available_format.format_id === format_id && (available_format.filesize || available_format.filesize_approx)) {
|
||||||
}
|
individual_expected_filesize += (available_format.filesize ? available_format.filesize : available_format.filesize_approx);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expected_filesize += individual_expected_filesize;
|
expected_filesize += individual_expected_filesize;
|
||||||
});
|
});
|
||||||
@@ -184,12 +187,12 @@ function getExpectedFileSize(input_info_jsons) {
|
|||||||
return expected_filesize;
|
return expected_filesize;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixVideoMetadataPerms(file_path, type) {
|
exports.fixVideoMetadataPerms = (file_path, type) => {
|
||||||
if (is_windows) return;
|
if (is_windows) return;
|
||||||
|
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
|
||||||
const file_path_no_extension = removeFileExtension(file_path);
|
const file_path_no_extension = exports.removeFileExtension(file_path);
|
||||||
|
|
||||||
const files_to_fix = [
|
const files_to_fix = [
|
||||||
// JSONs
|
// JSONs
|
||||||
@@ -206,11 +209,11 @@ function fixVideoMetadataPerms(file_path, type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteJSONFile(file_path, type) {
|
exports.deleteJSONFile = (file_path, type) => {
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
|
||||||
const file_path_no_extension = removeFileExtension(file_path);
|
const file_path_no_extension = exports.removeFileExtension(file_path);
|
||||||
|
|
||||||
let json_path = file_path_no_extension + '.info.json';
|
let json_path = file_path_no_extension + '.info.json';
|
||||||
let alternate_json_path = file_path_no_extension + ext + '.info.json';
|
let alternate_json_path = file_path_no_extension + ext + '.info.json';
|
||||||
|
|
||||||
@@ -218,33 +221,7 @@ function deleteJSONFile(file_path, type) {
|
|||||||
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeIDFromArchive(archive_path, id) {
|
exports.durationStringToNumber = (dur_str) => {
|
||||||
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
|
|
||||||
if (!data) {
|
|
||||||
logger.error('Archive could not be found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataArray = data.split('\n'); // convert file data in an array
|
|
||||||
const searchKeyword = id; // we are looking for a line, contains, key word id in the file
|
|
||||||
let lastIndex = -1; // let say, we have not found the keyword
|
|
||||||
|
|
||||||
for (let index=0; index<dataArray.length; index++) {
|
|
||||||
if (dataArray[index].includes(searchKeyword)) { // check if a line contains the id keyword
|
|
||||||
lastIndex = index; // found a line includes a id keyword
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
|
|
||||||
|
|
||||||
// UPDATE FILE WITH NEW DATA
|
|
||||||
const updatedData = dataArray.join('\n');
|
|
||||||
await fs.writeFile(archive_path, updatedData);
|
|
||||||
if (line) return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
function durationStringToNumber(dur_str) {
|
|
||||||
if (typeof dur_str === 'number') return dur_str;
|
if (typeof dur_str === 'number') return dur_str;
|
||||||
let num_sum = 0;
|
let num_sum = 0;
|
||||||
const dur_str_parts = dur_str.split(':');
|
const dur_str_parts = dur_str.split(':');
|
||||||
@@ -254,23 +231,17 @@ function durationStringToNumber(dur_str) {
|
|||||||
return num_sum;
|
return num_sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMatchingCategoryFiles(category, files) {
|
exports.getMatchingCategoryFiles = (category, files) => {
|
||||||
return files && files.filter(file => file.category && file.category.uid === category.uid);
|
return files && files.filter(file => file.category && file.category.uid === category.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUIDsToCategory(category, files) {
|
exports.addUIDsToCategory = (category, files) => {
|
||||||
const files_that_match = getMatchingCategoryFiles(category, files);
|
const files_that_match = exports.getMatchingCategoryFiles(category, files);
|
||||||
category['uids'] = files_that_match.map(file => file.uid);
|
category['uids'] = files_that_match.map(file => file.uid);
|
||||||
return files_that_match;
|
return files_that_match;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentDownloader() {
|
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
return details_json['downloader'];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function recFindByExt(base, ext, files, result, recursive = true)
|
|
||||||
{
|
|
||||||
files = files || (await fs.readdir(base))
|
files = files || (await fs.readdir(base))
|
||||||
result = result || []
|
result = result || []
|
||||||
|
|
||||||
@@ -279,7 +250,7 @@ async function recFindByExt(base, ext, files, result, recursive = true)
|
|||||||
if ( (await fs.stat(newbase)).isDirectory() )
|
if ( (await fs.stat(newbase)).isDirectory() )
|
||||||
{
|
{
|
||||||
if (!recursive) continue;
|
if (!recursive) continue;
|
||||||
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
|
result = await exports.recFindByExt(newbase,ext,await fs.readdir(newbase),result)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -292,23 +263,23 @@ async function recFindByExt(base, ext, files, result, recursive = true)
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFileExtension(filename) {
|
exports.removeFileExtension = (filename) => {
|
||||||
const filename_parts = filename.split('.');
|
const filename_parts = filename.split('.');
|
||||||
filename_parts.splice(filename_parts.length - 1);
|
filename_parts.splice(filename_parts.length - 1);
|
||||||
return filename_parts.join('.');
|
return filename_parts.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateString(date_string) {
|
exports.formatDateString = (date_string) => {
|
||||||
return date_string ? `${date_string.substring(0, 4)}-${date_string.substring(4, 6)}-${date_string.substring(6, 8)}` : 'N/A';
|
return date_string ? `${date_string.substring(0, 4)}-${date_string.substring(4, 6)}-${date_string.substring(6, 8)}` : 'N/A';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEdgeNGrams(str) {
|
exports.createEdgeNGrams = (str) => {
|
||||||
if (str && str.length > 3) {
|
if (str && str.length > 3) {
|
||||||
const minGram = 3
|
const minGram = 3
|
||||||
const maxGram = str.length
|
const maxGram = str.length
|
||||||
|
|
||||||
return str.split(" ").reduce((ngrams, token) => {
|
return str.split(" ").reduce((ngrams, token) => {
|
||||||
if (token.length > minGram) {
|
if (token.length > minGram) {
|
||||||
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
|
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
|
||||||
ngrams = [...ngrams, token.substr(0, i)]
|
ngrams = [...ngrams, token.substr(0, i)]
|
||||||
}
|
}
|
||||||
@@ -318,13 +289,13 @@ function createEdgeNGrams(str) {
|
|||||||
return ngrams
|
return ngrams
|
||||||
}, []).join(" ")
|
}, []).join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
// ffmpeg helper functions
|
// ffmpeg helper functions
|
||||||
|
|
||||||
async function cropFile(file_path, start, end, ext) {
|
exports.cropFile = async (file_path, start, end, ext) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const temp_file_path = `${file_path}.cropped${ext}`;
|
const temp_file_path = `${file_path}.cropped${ext}`;
|
||||||
let base_ffmpeg_call = ffmpeg(file_path);
|
let base_ffmpeg_call = ffmpeg(file_path);
|
||||||
@@ -353,13 +324,13 @@ async function cropFile(file_path, start, end, ext) {
|
|||||||
* setTimeout, but its a promise.
|
* setTimeout, but its a promise.
|
||||||
* @param {number} ms
|
* @param {number} ms
|
||||||
*/
|
*/
|
||||||
async function wait(ms) {
|
exports.wait = async (ms) => {
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkExistsWithTimeout(filePath, timeout) {
|
exports.checkExistsWithTimeout = async (filePath, timeout) => {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
||||||
var timer = setTimeout(function () {
|
var timer = setTimeout(function () {
|
||||||
@@ -371,7 +342,7 @@ async function checkExistsWithTimeout(filePath, timeout) {
|
|||||||
if (!err) {
|
if (!err) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (watcher) watcher.close();
|
if (watcher) watcher.close();
|
||||||
resolve();
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,14 +352,14 @@ async function checkExistsWithTimeout(filePath, timeout) {
|
|||||||
if (eventType === 'rename' && filename === basename) {
|
if (eventType === 'rename' && filename === basename) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (watcher) watcher.close();
|
if (watcher) watcher.close();
|
||||||
resolve();
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to download file using fetch
|
// helper function to download file using fetch
|
||||||
async function fetchFile(url, path, file_label) {
|
exports.fetchFile = async (url, path, file_label) => {
|
||||||
var len = null;
|
var len = null;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
@@ -415,10 +386,10 @@ async function fetchFile(url, path, file_label) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restartServer(is_update = false) {
|
exports.restartServer = async (is_update = false) => {
|
||||||
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
|
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
|
||||||
|
|
||||||
// the following line restarts the server through nodemon
|
// the following line restarts the server through pm2
|
||||||
fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only');
|
fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -428,20 +399,21 @@ async function restartServer(is_update = false) {
|
|||||||
// - if already exists and doesn't have value, ignore
|
// - if already exists and doesn't have value, ignore
|
||||||
// - if it doesn't exist and has value, add both arg and value
|
// - if it doesn't exist and has value, add both arg and value
|
||||||
// - if it doesn't exist and doesn't have value, add arg
|
// - if it doesn't exist and doesn't have value, add arg
|
||||||
function injectArgs(original_args, new_args) {
|
exports.injectArgs = (original_args, new_args) => {
|
||||||
const updated_args = original_args.slice();
|
const updated_args = original_args.slice();
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < new_args.length; i++) {
|
for (let i = 0; i < new_args.length; i++) {
|
||||||
const new_arg = new_args[i];
|
const new_arg = new_args[i];
|
||||||
if (!new_arg.startsWith('-') && !new_arg.startsWith('--') && i > 0 && original_args.includes(new_args[i - 1])) continue;
|
if (!new_arg.startsWith('-') && !new_arg.startsWith('--') && i > 0 && original_args.includes(new_args[i - 1])) continue;
|
||||||
|
|
||||||
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
|
||||||
if (original_args.includes(new_arg)) {
|
if (original_args.includes(new_arg)) {
|
||||||
const original_index = original_args.indexOf(new_arg);
|
const original_index = original_args.indexOf(new_arg);
|
||||||
original_args.splice(original_index, 2);
|
updated_args.splice(original_index, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
updated_args.push(new_arg, new_args[i + 1]);
|
updated_args.push(new_arg, new_args[i + 1]);
|
||||||
|
i++; // we need to skip the arg value on the next loop
|
||||||
} else {
|
} else {
|
||||||
if (!original_args.includes(new_arg)) {
|
if (!original_args.includes(new_arg)) {
|
||||||
updated_args.push(new_arg);
|
updated_args.push(new_arg);
|
||||||
@@ -456,6 +428,137 @@ function injectArgs(original_args, new_args) {
|
|||||||
return updated_args;
|
return updated_args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.filterArgs = (args, args_to_remove) => {
|
||||||
|
return args.filter(x => !args_to_remove.includes(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.searchObjectByString = (o, s) => {
|
||||||
|
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||||
|
s = s.replace(/^\./, ''); // strip a leading dot
|
||||||
|
var a = s.split('.');
|
||||||
|
for (var i = 0, n = a.length; i < n; ++i) {
|
||||||
|
var k = a[i];
|
||||||
|
if (k in o) {
|
||||||
|
o = o[k];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.stripPropertiesFromObject = (obj, properties, whitelist = false) => {
|
||||||
|
if (!whitelist) {
|
||||||
|
const new_obj = JSON.parse(JSON.stringify(obj));
|
||||||
|
for (let field of properties) {
|
||||||
|
delete new_obj[field];
|
||||||
|
}
|
||||||
|
return new_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_obj = {};
|
||||||
|
for (let field of properties) {
|
||||||
|
new_obj[field] = obj[field];
|
||||||
|
}
|
||||||
|
return new_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getArchiveFolder = (type, user_uid = null, sub = null) => {
|
||||||
|
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
const subsFolderPath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
if (user_uid) {
|
||||||
|
if (sub) {
|
||||||
|
return path.join(usersFolderPath, user_uid, 'subscriptions', 'archives', sub.name);
|
||||||
|
} else {
|
||||||
|
return path.join(usersFolderPath, user_uid, type, 'archives');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sub) {
|
||||||
|
return path.join(subsFolderPath, 'archives', sub.name);
|
||||||
|
} else {
|
||||||
|
return path.join('appdata', 'archives');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getBaseURL = () => {
|
||||||
|
return `${config_api.getConfigItem('ytdl_url')}:${config_api.getConfigItem('ytdl_port')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateLoggerLevel = (new_logger_level) => {
|
||||||
|
const possible_levels = ['error', 'warn', 'info', 'verbose', 'debug'];
|
||||||
|
if (!possible_levels.includes(new_logger_level)) {
|
||||||
|
logger.error(`${new_logger_level} is not a valid logger level! Choose one of the following: ${possible_levels.join(', ')}.`)
|
||||||
|
new_logger_level = 'info';
|
||||||
|
}
|
||||||
|
logger.level = new_logger_level;
|
||||||
|
winston.loggers.get('console').level = new_logger_level;
|
||||||
|
logger.transports[2].level = new_logger_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.convertFlatObjectToNestedObject = (obj) => {
|
||||||
|
const result = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
const nestedKeys = key.split('.');
|
||||||
|
let currentObj = result;
|
||||||
|
for (let i = 0; i < nestedKeys.length; i++) {
|
||||||
|
if (i === nestedKeys.length - 1) {
|
||||||
|
currentObj[nestedKeys[i]] = obj[key];
|
||||||
|
} else {
|
||||||
|
currentObj[nestedKeys[i]] = currentObj[nestedKeys[i]] || {};
|
||||||
|
currentObj = currentObj[nestedKeys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getDirectoriesInDirectory = async (basePath) => {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(basePath, { withFileTypes: true });
|
||||||
|
return files
|
||||||
|
.filter((file) => file.isDirectory())
|
||||||
|
.map((file) => path.join(basePath, file.name));
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.parseOutputJSON = (output, err) => {
|
||||||
|
let split_output = [];
|
||||||
|
// const output_jsons = [];
|
||||||
|
if (err && !output) {
|
||||||
|
if (!err.stderr.includes('This video is unavailable') && !err.stderr.includes('Private video')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||||
|
try {
|
||||||
|
split_output = err.stdout.split(/\r\n|\r|\n/);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Backup method failed. See error below:');
|
||||||
|
logger.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (output.length === 0 || (output.length === 1 && output[0].length === 0)) {
|
||||||
|
// output is '' or ['']
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
for (const output_item of output) {
|
||||||
|
// we have to do this because sometimes there will be leading characters before the actual json
|
||||||
|
const start_idx = output_item.indexOf('{"');
|
||||||
|
const clean_output = output_item.slice(start_idx, output_item.length);
|
||||||
|
split_output.push(clean_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return split_output.map(split_output_str => JSON.parse(split_output_str));
|
||||||
|
} catch(e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// objects
|
// objects
|
||||||
|
|
||||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||||
@@ -473,33 +576,7 @@ function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, p
|
|||||||
this.view_count = view_count;
|
this.view_count = view_count;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.abr = abr;
|
this.abr = abr;
|
||||||
}
|
this.favorite = false;
|
||||||
|
}
|
||||||
|
exports.File = File;
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getJSONMp3: getJSONMp3,
|
|
||||||
getJSONMp4: getJSONMp4,
|
|
||||||
getJSON: getJSON,
|
|
||||||
getTrueFileName: getTrueFileName,
|
|
||||||
getDownloadedThumbnail: getDownloadedThumbnail,
|
|
||||||
getExpectedFileSize: getExpectedFileSize,
|
|
||||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
|
||||||
deleteJSONFile: deleteJSONFile,
|
|
||||||
removeIDFromArchive: removeIDFromArchive,
|
|
||||||
getDownloadedFilesByType: getDownloadedFilesByType,
|
|
||||||
createContainerZipFile: createContainerZipFile,
|
|
||||||
durationStringToNumber: durationStringToNumber,
|
|
||||||
getMatchingCategoryFiles: getMatchingCategoryFiles,
|
|
||||||
addUIDsToCategory: addUIDsToCategory,
|
|
||||||
getCurrentDownloader: getCurrentDownloader,
|
|
||||||
recFindByExt: recFindByExt,
|
|
||||||
removeFileExtension: removeFileExtension,
|
|
||||||
formatDateString: formatDateString,
|
|
||||||
cropFile: cropFile,
|
|
||||||
createEdgeNGrams: createEdgeNGrams,
|
|
||||||
wait: wait,
|
|
||||||
checkExistsWithTimeout: checkExistsWithTimeout,
|
|
||||||
fetchFile: fetchFile,
|
|
||||||
restartServer: restartServer,
|
|
||||||
injectArgs: injectArgs,
|
|
||||||
File: File
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,141 +1,165 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const path = require('path');
|
||||||
|
const execa = require('execa');
|
||||||
|
const kill = require('tree-kill');
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const CONSTS = require('./consts');
|
const CONSTS = require('./consts');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
|
|
||||||
const OUTDATED_VERSION = "2020.00.00";
|
|
||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
const download_sources = {
|
exports.youtubedl_forks = {
|
||||||
'youtube-dl': {
|
'youtube-dl': {
|
||||||
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
|
'download_url': 'https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl',
|
||||||
'func': downloadLatestYoutubeDLBinary
|
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
|
||||||
},
|
},
|
||||||
'youtube-dlc': {
|
'youtube-dlc': {
|
||||||
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
|
'download_url': 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc',
|
||||||
'func': downloadLatestYoutubeDLCBinary
|
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
|
||||||
},
|
},
|
||||||
'yt-dlp': {
|
'yt-dlp': {
|
||||||
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
|
'download_url': 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp',
|
||||||
'func': downloadLatestYoutubeDLPBinary
|
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.checkForYoutubeDLUpdate = async () => {
|
exports.runYoutubeDL = async (url, args, customDownloadHandler = null) => {
|
||||||
return new Promise(async resolve => {
|
const output_file_path = getYoutubeDLPath();
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
if (!fs.existsSync(output_file_path)) await exports.checkForYoutubeDLUpdate();
|
||||||
const tags_url = download_sources[default_downloader]['tags_url'];
|
let callback = null;
|
||||||
// get current version
|
let child_process = null;
|
||||||
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
if (customDownloadHandler) {
|
||||||
if (!current_app_details_exists) {
|
callback = runYoutubeDLCustom(url, args, customDownloadHandler);
|
||||||
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
} else {
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version": OUTDATED_VERSION, "downloader": default_downloader});
|
({callback, child_process} = await runYoutubeDLProcess(url, args));
|
||||||
}
|
}
|
||||||
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
|
||||||
let current_version = current_app_details['version'];
|
|
||||||
let current_downloader = current_app_details['downloader'];
|
|
||||||
let stored_binary_path = current_app_details['path'];
|
|
||||||
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
|
|
||||||
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${CONSTS.DETAILS_BIN_PATH}, attempting to guess actual path...`);
|
|
||||||
const guessed_base_path = 'node_modules/youtube-dl/bin/';
|
|
||||||
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
|
|
||||||
if (fs.existsSync(guessed_file_path)) {
|
|
||||||
stored_binary_path = guessed_file_path;
|
|
||||||
// logger.info('INFO: Guess successful! Update process continuing...')
|
|
||||||
} else {
|
|
||||||
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// got version, now let's check the latest version from the youtube-dl API
|
return {child_process, callback};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run youtube-dl directly (not cancellable)
|
||||||
fetch(tags_url, {method: 'Get'})
|
const runYoutubeDLCustom = async (url, args, customDownloadHandler) => {
|
||||||
.then(async res => res.json())
|
const downloadHandler = customDownloadHandler;
|
||||||
.then(async (json) => {
|
return new Promise(resolve => {
|
||||||
// check if the versions are different
|
downloadHandler(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
if (!json || !json[0]) {
|
const parsed_output = utils.parseOutputJSON(output, err);
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
resolve({parsed_output, err});
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const latest_update_version = json[0]['name'];
|
|
||||||
if (current_version !== latest_update_version || default_downloader !== current_downloader) {
|
|
||||||
// versions different or different downloader is being used, download new update
|
|
||||||
resolve(latest_update_version);
|
|
||||||
} else {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
|
||||||
logger.error(err);
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateYoutubeDL = async (latest_update_version) => {
|
// Run youtube-dl in a subprocess (cancellable)
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
const runYoutubeDLProcess = async (url, args, youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) => {
|
||||||
await download_sources[default_downloader]['func'](latest_update_version);
|
const youtubedl_path = getYoutubeDLPath(youtubedl_fork);
|
||||||
|
const binary_exists = fs.existsSync(youtubedl_path);
|
||||||
|
if (!binary_exists) {
|
||||||
|
const err = `Could not find path for ${youtubedl_fork} at ${youtubedl_path}`;
|
||||||
|
logger.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const child_process = execa(getYoutubeDLPath(youtubedl_fork), [url, ...args], {maxBuffer: Infinity});
|
||||||
|
const callback = new Promise(async resolve => {
|
||||||
|
try {
|
||||||
|
const {stdout, stderr} = await child_process;
|
||||||
|
const parsed_output = utils.parseOutputJSON(stdout.trim().split(/\r?\n/), stderr);
|
||||||
|
resolve({parsed_output, err: stderr});
|
||||||
|
} catch (e) {
|
||||||
|
resolve({parsed_output: null, err: e})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {child_process, callback}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.verifyBinaryExistsLinux = () => {
|
function getYoutubeDLPath(youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) {
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
const binary_file_name = youtubedl_fork + (is_windows ? '.exe' : '');
|
||||||
if (!is_windows && details_json && details_json['path'].includes('.exe')) {
|
const binary_path = path.join('appdata', 'bin', binary_file_name);
|
||||||
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
|
return binary_path;
|
||||||
details_json['exec'] = 'youtube-dl';
|
}
|
||||||
details_json['version'] = OUTDATED_VERSION;
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
|
||||||
|
|
||||||
utils.restartServer();
|
exports.killYoutubeDLProcess = async (child_process) => {
|
||||||
|
kill(child_process.pid, 'SIGKILL');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.checkForYoutubeDLUpdate = async () => {
|
||||||
|
const selected_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const output_file_path = getYoutubeDLPath();
|
||||||
|
// get current version
|
||||||
|
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
if (!current_app_details_exists[selected_fork]) {
|
||||||
|
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
||||||
|
updateDetailsJSON(CONSTS.OUTDATED_YOUTUBEDL_VERSION, selected_fork, output_file_path);
|
||||||
|
}
|
||||||
|
const current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
||||||
|
const current_version = current_app_details[selected_fork]['version'];
|
||||||
|
const current_fork = current_app_details[selected_fork]['downloader'];
|
||||||
|
|
||||||
|
const latest_version = await exports.getLatestUpdateVersion(selected_fork);
|
||||||
|
// if the binary does not exist, or default_downloader doesn't match existing fork, or if the fork has been updated, redownload
|
||||||
|
// TODO: don't redownload if fork already exists
|
||||||
|
if (!fs.existsSync(output_file_path) || current_fork !== selected_fork || !current_version || current_version !== latest_version) {
|
||||||
|
logger.warn(`Updating ${selected_fork} binary to '${output_file_path}', downloading...`);
|
||||||
|
await exports.updateYoutubeDL(latest_version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLBinary(new_version) {
|
exports.updateYoutubeDL = async (latest_update_version, custom_output_path = null) => {
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
await fs.ensureDir(path.join('appdata', 'bin'));
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
|
await downloadLatestYoutubeDLBinaryGeneric(default_downloader, latest_update_version, custom_output_path);
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await utils.fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dl');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLCBinary(new_version) {
|
async function downloadLatestYoutubeDLBinaryGeneric(youtubedl_fork, new_version, custom_output_path = null) {
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
// build the URL
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
const download_url = `${exports.youtubedl_forks[youtubedl_fork]['download_url']}${file_ext}`;
|
||||||
|
const output_path = custom_output_path || getYoutubeDLPath(youtubedl_fork);
|
||||||
|
|
||||||
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
try {
|
||||||
|
await utils.fetchFile(download_url, output_path, `${youtubedl_fork} ${new_version}`);
|
||||||
|
fs.chmod(output_path, 0o777);
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dlc');
|
updateDetailsJSON(new_version, youtubedl_fork, output_path);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to download new ${youtubedl_fork} version: ${new_version}`);
|
||||||
|
logger.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getLatestUpdateVersion = async (youtubedl_fork) => {
|
||||||
|
const tags_url = exports.youtubedl_forks[youtubedl_fork]['tags_url'];
|
||||||
|
return new Promise(resolve => {
|
||||||
|
fetch(tags_url, {method: 'Get'})
|
||||||
|
.then(async res => res.json())
|
||||||
|
.then(async (json) => {
|
||||||
|
if (!json || !json[0]) {
|
||||||
|
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latest_update_version = json[0]['name'];
|
||||||
|
resolve(latest_update_version);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
|
||||||
|
logger.error(err);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLPBinary(new_version) {
|
function updateDetailsJSON(new_version, fork, output_path) {
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
const details_json = fs.existsSync(CONSTS.DETAILS_BIN_PATH) ? fs.readJSONSync(CONSTS.DETAILS_BIN_PATH) : {};
|
||||||
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
|
if (!details_json[fork]) details_json[fork] = {};
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
const fork_json = details_json[fork];
|
||||||
|
fork_json['version'] = new_version;
|
||||||
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
|
fork_json['downloader'] = fork;
|
||||||
|
fork_json['path'] = output_path; // unused
|
||||||
updateDetailsJSON(new_version, 'yt-dlp');
|
fork_json['exec'] = fork + file_ext; // unused
|
||||||
}
|
|
||||||
|
|
||||||
function updateDetailsJSON(new_version, downloader) {
|
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
if (new_version) details_json['version'] = new_version;
|
|
||||||
details_json['downloader'] = downloader;
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for https://github.com/Tzahi12345/YoutubeDL-Material
|
||||||
|
|
||||||
# A chart can be either an 'application' or a 'library' chart.
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
#
|
#
|
||||||
@@ -15,10 +15,10 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "4.2"
|
appVersion: "4.3.2"
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
{{- if .Values.ingress.enabled -}}
|
{{- if .Values.ingress.enabled -}}
|
||||||
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
||||||
{{- $svcPort := .Values.service.port -}}
|
{{- $svcPort := .Values.service.port -}}
|
||||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
|
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
|
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
apiVersion: networking.k8s.io/v1beta1
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: extensions/v1beta1
|
||||||
@@ -16,6 +23,9 @@ metadata:
|
|||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
|
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
{{- if .Values.ingress.tls }}
|
{{- if .Values.ingress.tls }}
|
||||||
tls:
|
tls:
|
||||||
{{- range .Values.ingress.tls }}
|
{{- range .Values.ingress.tls }}
|
||||||
@@ -33,9 +43,19 @@ spec:
|
|||||||
paths:
|
paths:
|
||||||
{{- range .paths }}
|
{{- range .paths }}
|
||||||
- path: {{ .path }}
|
- path: {{ .path }}
|
||||||
|
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
{{- end }}
|
||||||
backend:
|
backend:
|
||||||
|
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
{{- else }}
|
||||||
serviceName: {{ $fullName }}
|
serviceName: {{ $fullName }}
|
||||||
servicePort: {{ $svcPort }}
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
3
chrome-extension/css/bootstrap.min.css
vendored
3
chrome-extension/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@ version: "2"
|
|||||||
services:
|
services:
|
||||||
ytdl_material:
|
ytdl_material:
|
||||||
environment:
|
environment:
|
||||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
|
||||||
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
|
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
|
||||||
ytdl_use_local_db: 'false'
|
ytdl_use_local_db: 'false'
|
||||||
write_ytdl_config: 'true'
|
write_ytdl_config: 'true'
|
||||||
@@ -17,14 +16,13 @@ services:
|
|||||||
- ./users:/app/users
|
- ./users:/app/users
|
||||||
ports:
|
ports:
|
||||||
- "8998:17442"
|
- "8998:17442"
|
||||||
image: tzahi12345/youtubedl-material:nightly
|
image: tzahi12345/youtubedl-material:latest
|
||||||
ytdl-mongo-db:
|
ytdl-mongo-db:
|
||||||
image: mongo
|
# If you are using a Raspberry Pi, use mongo:4.4.18
|
||||||
ports:
|
image: mongo:4
|
||||||
- "27017:27017"
|
|
||||||
logging:
|
logging:
|
||||||
driver: "none"
|
driver: "none"
|
||||||
container_name: mongo-db
|
container_name: mongo-db
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./db/:/data/db
|
- ./db/:/data/db
|
||||||
|
|||||||
69
docker-utils/GetTwitchDownloader.py
Normal file
69
docker-utils/GetTwitchDownloader.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import platform
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from github import Github
|
||||||
|
|
||||||
|
machine = platform.machine()
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
|
||||||
|
MACHINES_TO_ZIP = OrderedDict([
|
||||||
|
("x86_64", "Linux-x64"),
|
||||||
|
("aarch64", "LinuxArm64"),
|
||||||
|
("armv8", "LinuxArm64"),
|
||||||
|
("arm", "LinuxArm"),
|
||||||
|
("AMD64", "Windows-x64")
|
||||||
|
])
|
||||||
|
|
||||||
|
def getZipName():
|
||||||
|
for possibleMachine, possibleZipName in MACHINES_TO_ZIP.items():
|
||||||
|
if possibleMachine in machine:
|
||||||
|
return possibleZipName
|
||||||
|
|
||||||
|
def getLatestFileInRepo(repo, search_string):
|
||||||
|
# Create an unauthenticated instance of the Github object
|
||||||
|
g = Github(os.environ.get('GH_TOKEN'))
|
||||||
|
|
||||||
|
# Replace with the repository owner and name
|
||||||
|
repo = g.get_repo(repo)
|
||||||
|
|
||||||
|
# Get all releases of the repository
|
||||||
|
releases = repo.get_releases()
|
||||||
|
|
||||||
|
# Loop through the releases in reverse order (from latest to oldest)
|
||||||
|
for release in list(releases):
|
||||||
|
# Get the release assets (files attached to the release)
|
||||||
|
assets = release.get_assets()
|
||||||
|
|
||||||
|
# Loop through the assets
|
||||||
|
for asset in assets:
|
||||||
|
if re.search(search_string, asset.name):
|
||||||
|
print(f'Downloading: {asset.name}')
|
||||||
|
response = requests.get(asset.browser_download_url)
|
||||||
|
with open(asset.name, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print(f'Download complete: {asset.name}. Unzipping...')
|
||||||
|
shutil.unpack_archive(asset.name, './')
|
||||||
|
print(f'Unzipping complete!')
|
||||||
|
os.remove(asset.name)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# If no matching release is found, print a message
|
||||||
|
print(f'No release found with {search_string}')
|
||||||
|
|
||||||
|
def getLatestCLIRelease():
|
||||||
|
zipName = getZipName()
|
||||||
|
if not zipName:
|
||||||
|
print(f"GetTwitchDownloader.py could not get valid path for '{machine}'. Exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
searchString = r'.*CLI.*' + zipName
|
||||||
|
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
|
||||||
|
|
||||||
|
getLatestCLIRelease()
|
||||||
39
docker-utils/fetch-twitchdownloader.sh
Normal file
39
docker-utils/fetch-twitchdownloader.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# THANK YOU TALULAH (https://github.com/nottalulah) for your help in figuring this out
|
||||||
|
# and also optimizing some code with this commit.
|
||||||
|
# xoxo :D
|
||||||
|
|
||||||
|
case $(uname -m) in
|
||||||
|
x86_64)
|
||||||
|
ARCH=Linux-x64;;
|
||||||
|
aarch64)
|
||||||
|
ARCH=LinuxArm64;;
|
||||||
|
armhf)
|
||||||
|
ARCH=LinuxArm;;
|
||||||
|
armv7)
|
||||||
|
ARCH=LinuxArm;;
|
||||||
|
armv7l)
|
||||||
|
ARCH=LinuxArm;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $(uname -m)"
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "(INFO) Architecture detected: $ARCH"
|
||||||
|
echo "(1/5) READY - Install unzip"
|
||||||
|
apt-get update && apt-get -y install unzip curl jq libicu70
|
||||||
|
VERSION=$(curl --silent "https://api.github.com/repos/lay295/TwitchDownloader/releases" | jq -r --arg arch "$ARCH" '[.[] | select(.assets | length > 0) | select(.assets[].name | contains("CLI") and contains($arch))] | max_by(.published_at) | .tag_name')
|
||||||
|
echo "(2/5) DOWNLOAD - Acquire twitchdownloader"
|
||||||
|
curl -o twitchdownloader.zip \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 120 \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 0 \
|
||||||
|
--retry-max-time 40 \
|
||||||
|
-L "https://github.com/lay295/TwitchDownloader/releases/download/$VERSION/TwitchDownloaderCLI-$VERSION-$ARCH.zip"
|
||||||
|
unzip twitchdownloader.zip
|
||||||
|
chmod +x TwitchDownloaderCLI
|
||||||
|
echo "(3/5) Smoke test"
|
||||||
|
./TwitchDownloaderCLI --help
|
||||||
|
cp ./TwitchDownloaderCLI /usr/local/bin/TwitchDownloaderCLI
|
||||||
@@ -26,11 +26,11 @@ apt-get update && apt-get -y install curl xz-utils
|
|||||||
echo "(2/5) DOWNLOAD - Acquire latest ffmpeg and ffprobe from John van Sickle's master-sourced builds in ffmpeg obtain layer"
|
echo "(2/5) DOWNLOAD - Acquire latest ffmpeg and ffprobe from John van Sickle's master-sourced builds in ffmpeg obtain layer"
|
||||||
curl -o ffmpeg.txz \
|
curl -o ffmpeg.txz \
|
||||||
--connect-timeout 5 \
|
--connect-timeout 5 \
|
||||||
--max-time 10 \
|
--max-time 120 \
|
||||||
--retry 5 \
|
--retry 5 \
|
||||||
--retry-delay 0 \
|
--retry-delay 0 \
|
||||||
--retry-max-time 40 \
|
--retry-max-time 40 \
|
||||||
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz"
|
"https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-${ARCH}-static.tar.xz"
|
||||||
mkdir /tmp/ffmpeg
|
mkdir /tmp/ffmpeg
|
||||||
tar xf ffmpeg.txz -C /tmp/ffmpeg
|
tar xf ffmpeg.txz -C /tmp/ffmpeg
|
||||||
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
|
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
|
||||||
@@ -40,4 +40,4 @@ echo "(4/5) PROVISION - Provide ffmpeg and ffprobe from ffmpeg obtain layer"
|
|||||||
cp /tmp/ffmpeg/*/ffmpeg /usr/local/bin/ffmpeg
|
cp /tmp/ffmpeg/*/ffmpeg /usr/local/bin/ffmpeg
|
||||||
cp /tmp/ffmpeg/*/ffprobe /usr/local/bin/ffprobe
|
cp /tmp/ffmpeg/*/ffprobe /usr/local/bin/ffprobe
|
||||||
echo "(5/5) CLEANUP - Remove temporary downloads from ffmpeg obtain layer"
|
echo "(5/5) CLEANUP - Remove temporary downloads from ffmpeg obtain layer"
|
||||||
rm -rf /tmp/ffmpeg ffmpeg.txz
|
rm -rf /tmp/ffmpeg ffmpeg.txz
|
||||||
20545
package-lock.json
generated
20545
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-dl-material",
|
"name": "youtube-dl-material",
|
||||||
"version": "4.2.0",
|
"version": "4.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
"codespaces": "ng serve --configuration=codespaces",
|
||||||
"build": "ng build --configuration production",
|
"build": "ng build --configuration production",
|
||||||
"prebuild": "node src/postbuild.mjs",
|
"prebuild": "node src/postbuild.mjs",
|
||||||
"heroku-postbuild": "npm install --prefix backend",
|
"heroku-postbuild": "npm install --prefix backend",
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"electron": "ng build --base-href ./ && electron .",
|
"electron": "ng build --base-href ./ && electron .",
|
||||||
"generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true",
|
"generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true",
|
||||||
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n"
|
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.3.1",
|
"node": "12.3.1",
|
||||||
@@ -21,64 +22,68 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^13.3.3",
|
"@angular-devkit/core": "^15.0.1",
|
||||||
"@angular/animations": "^13.3.4",
|
"@angular/animations": "^15.0.1",
|
||||||
"@angular/cdk": "^13.3.4",
|
"@angular/cdk": "^15.0.0",
|
||||||
"@angular/common": "^13.3.4",
|
"@angular/common": "^15.0.1",
|
||||||
"@angular/compiler": "^13.3.4",
|
"@angular/compiler": "^15.0.1",
|
||||||
"@angular/core": "^13.3.4",
|
"@angular/core": "^15.0.1",
|
||||||
"@angular/forms": "^13.3.4",
|
"@angular/forms": "^15.0.1",
|
||||||
"@angular/localize": "^13.3.4",
|
"@angular/localize": "^15.0.1",
|
||||||
"@angular/material": "^13.3.4",
|
"@angular/material": "^15.0.0",
|
||||||
"@angular/platform-browser": "^13.3.4",
|
"@angular/platform-browser": "^15.0.1",
|
||||||
"@angular/platform-browser-dynamic": "^13.3.4",
|
"@angular/platform-browser-dynamic": "^15.0.1",
|
||||||
"@angular/router": "^13.3.4",
|
"@angular/router": "^15.0.1",
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@ngneat/content-loader": "^5.0.0",
|
"@ngneat/content-loader": "^7.0.0",
|
||||||
"@videogular/ngx-videogular": "^5.0.1",
|
"@videogular/ngx-videogular": "^6.0.0",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^10.0.7",
|
||||||
"fingerprintjs2": "^2.1.0",
|
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"material-icons": "^1.10.8",
|
"material-icons": "^1.10.8",
|
||||||
"nan": "^2.14.1",
|
"nan": "^2.14.1",
|
||||||
"ng-lazyload-image": "^7.0.1",
|
"ngx-avatars": "^1.4.1",
|
||||||
"ngx-avatars": "^1.3.1",
|
"ngx-file-drop": "^15.0.0",
|
||||||
"ngx-file-drop": "^13.0.0",
|
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.0.0-rc.0",
|
"rxjs-compat": "^6.6.7",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~4.6.3",
|
"typescript": "~4.8.4",
|
||||||
"xliff-to-json": "^1.0.4",
|
"xliff-to-json": "^1.0.4",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^13.3.3",
|
"@angular-devkit/build-angular": "^15.0.1",
|
||||||
"@angular/cli": "^13.3.3",
|
"@angular/cli": "^15.0.1",
|
||||||
"@angular/compiler-cli": "^13.3.4",
|
"@angular/compiler-cli": "^15.0.1",
|
||||||
"@angular/language-service": "^13.3.4",
|
"@angular/language-service": "^15.0.1",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "^4.3.1",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||||
"@typescript-eslint/parser": "^4.29.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
|
"ajv": "^7.2.4",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"electron": "^13.6.6",
|
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~6.3.16",
|
"karma": "~6.4.2",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-cli": "~1.0.1",
|
"karma-cli": "~1.0.1",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"openapi-typescript-codegen": "^0.21.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
"ts-node": "~3.0.4",
|
"ts-node": "~3.0.4",
|
||||||
"tslint": "~6.1.0"
|
"tslint": "~6.1.0"
|
||||||
}
|
},
|
||||||
|
"overrides": {
|
||||||
|
"ngx-avatars": {
|
||||||
|
"@angular/common": "15.0.1",
|
||||||
|
"@angular/core": "15.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -3,7 +3,9 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest';
|
export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest';
|
||||||
|
export type { Archive } from './models/Archive';
|
||||||
export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest';
|
export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest';
|
||||||
|
export type { binary } from './models/binary';
|
||||||
export type { body_19 } from './models/body_19';
|
export type { body_19 } from './models/body_19';
|
||||||
export type { body_20 } from './models/body_20';
|
export type { body_20 } from './models/body_20';
|
||||||
export type { Category } from './models/Category';
|
export type { Category } from './models/Category';
|
||||||
@@ -12,6 +14,8 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission
|
|||||||
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
||||||
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
||||||
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
||||||
|
export type { CheckSubscriptionRequest } from './models/CheckSubscriptionRequest';
|
||||||
|
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
|
||||||
export type { ConcurrentStream } from './models/ConcurrentStream';
|
export type { ConcurrentStream } from './models/ConcurrentStream';
|
||||||
export type { Config } from './models/Config';
|
export type { Config } from './models/Config';
|
||||||
export type { ConfigResponse } from './models/ConfigResponse';
|
export type { ConfigResponse } from './models/ConfigResponse';
|
||||||
@@ -23,8 +27,11 @@ export type { CropFileSettings } from './models/CropFileSettings';
|
|||||||
export type { DatabaseFile } from './models/DatabaseFile';
|
export type { DatabaseFile } from './models/DatabaseFile';
|
||||||
export { DBBackup } from './models/DBBackup';
|
export { DBBackup } from './models/DBBackup';
|
||||||
export type { DBInfoResponse } from './models/DBInfoResponse';
|
export type { DBInfoResponse } from './models/DBInfoResponse';
|
||||||
|
export type { DeleteAllFilesResponse } from './models/DeleteAllFilesResponse';
|
||||||
|
export type { DeleteArchiveItemsRequest } from './models/DeleteArchiveItemsRequest';
|
||||||
export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest';
|
export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest';
|
||||||
export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request';
|
export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request';
|
||||||
|
export type { DeleteNotificationRequest } from './models/DeleteNotificationRequest';
|
||||||
export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest';
|
export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest';
|
||||||
export type { DeleteSubscriptionFileRequest } from './models/DeleteSubscriptionFileRequest';
|
export type { DeleteSubscriptionFileRequest } from './models/DeleteSubscriptionFileRequest';
|
||||||
export type { DeleteUserRequest } from './models/DeleteUserRequest';
|
export type { DeleteUserRequest } from './models/DeleteUserRequest';
|
||||||
@@ -36,16 +43,19 @@ export type { DownloadResponse } from './models/DownloadResponse';
|
|||||||
export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchChatByVODIDRequest';
|
export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchChatByVODIDRequest';
|
||||||
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
|
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
|
||||||
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
|
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
|
||||||
export type { File } from './models/File';
|
|
||||||
export { FileType } from './models/FileType';
|
export { FileType } from './models/FileType';
|
||||||
|
export { FileTypeFilter } from './models/FileTypeFilter';
|
||||||
export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
|
export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
|
||||||
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
|
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
|
||||||
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
|
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
|
||||||
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
|
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
|
||||||
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
|
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
|
||||||
|
export type { GetAllFilesRequest } from './models/GetAllFilesRequest';
|
||||||
export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
|
export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
|
||||||
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
|
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
|
||||||
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
|
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
|
||||||
|
export type { GetArchivesRequest } from './models/GetArchivesRequest';
|
||||||
|
export type { GetArchivesResponse } from './models/GetArchivesResponse';
|
||||||
export type { GetDBBackupsResponse } from './models/GetDBBackupsResponse';
|
export type { GetDBBackupsResponse } from './models/GetDBBackupsResponse';
|
||||||
export type { GetDownloadRequest } from './models/GetDownloadRequest';
|
export type { GetDownloadRequest } from './models/GetDownloadRequest';
|
||||||
export type { GetDownloadResponse } from './models/GetDownloadResponse';
|
export type { GetDownloadResponse } from './models/GetDownloadResponse';
|
||||||
@@ -59,6 +69,7 @@ export type { GetLogsRequest } from './models/GetLogsRequest';
|
|||||||
export type { GetLogsResponse } from './models/GetLogsResponse';
|
export type { GetLogsResponse } from './models/GetLogsResponse';
|
||||||
export type { GetMp3sResponse } from './models/GetMp3sResponse';
|
export type { GetMp3sResponse } from './models/GetMp3sResponse';
|
||||||
export type { GetMp4sResponse } from './models/GetMp4sResponse';
|
export type { GetMp4sResponse } from './models/GetMp4sResponse';
|
||||||
|
export type { GetNotificationsResponse } from './models/GetNotificationsResponse';
|
||||||
export type { GetPlaylistRequest } from './models/GetPlaylistRequest';
|
export type { GetPlaylistRequest } from './models/GetPlaylistRequest';
|
||||||
export type { GetPlaylistResponse } from './models/GetPlaylistResponse';
|
export type { GetPlaylistResponse } from './models/GetPlaylistResponse';
|
||||||
export type { GetPlaylistsRequest } from './models/GetPlaylistsRequest';
|
export type { GetPlaylistsRequest } from './models/GetPlaylistsRequest';
|
||||||
@@ -69,17 +80,24 @@ export type { GetSubscriptionResponse } from './models/GetSubscriptionResponse';
|
|||||||
export type { GetTaskRequest } from './models/GetTaskRequest';
|
export type { GetTaskRequest } from './models/GetTaskRequest';
|
||||||
export type { GetTaskResponse } from './models/GetTaskResponse';
|
export type { GetTaskResponse } from './models/GetTaskResponse';
|
||||||
export type { GetUsersResponse } from './models/GetUsersResponse';
|
export type { GetUsersResponse } from './models/GetUsersResponse';
|
||||||
|
export type { ImportArchiveRequest } from './models/ImportArchiveRequest';
|
||||||
export type { IncrementViewCountRequest } from './models/IncrementViewCountRequest';
|
export type { IncrementViewCountRequest } from './models/IncrementViewCountRequest';
|
||||||
export type { inline_response_200_15 } from './models/inline_response_200_15';
|
export type { inline_response_200_15 } from './models/inline_response_200_15';
|
||||||
export type { LoginRequest } from './models/LoginRequest';
|
export type { LoginRequest } from './models/LoginRequest';
|
||||||
export type { LoginResponse } from './models/LoginResponse';
|
export type { LoginResponse } from './models/LoginResponse';
|
||||||
|
export type { Notification } from './models/Notification';
|
||||||
|
export { NotificationAction } from './models/NotificationAction';
|
||||||
|
export { NotificationType } from './models/NotificationType';
|
||||||
export type { Playlist } from './models/Playlist';
|
export type { Playlist } from './models/Playlist';
|
||||||
export type { RegisterRequest } from './models/RegisterRequest';
|
export type { RegisterRequest } from './models/RegisterRequest';
|
||||||
export type { RegisterResponse } from './models/RegisterResponse';
|
export type { RegisterResponse } from './models/RegisterResponse';
|
||||||
|
export type { RestartDownloadResponse } from './models/RestartDownloadResponse';
|
||||||
export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
|
export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
|
||||||
export { Schedule } from './models/Schedule';
|
export { Schedule } from './models/Schedule';
|
||||||
export type { SetConfigRequest } from './models/SetConfigRequest';
|
export type { SetConfigRequest } from './models/SetConfigRequest';
|
||||||
|
export type { SetNotificationsToReadRequest } from './models/SetNotificationsToReadRequest';
|
||||||
export type { SharingToggle } from './models/SharingToggle';
|
export type { SharingToggle } from './models/SharingToggle';
|
||||||
|
export type { Sort } from './models/Sort';
|
||||||
export type { SubscribeRequest } from './models/SubscribeRequest';
|
export type { SubscribeRequest } from './models/SubscribeRequest';
|
||||||
export type { SubscribeResponse } from './models/SubscribeResponse';
|
export type { SubscribeResponse } from './models/SubscribeResponse';
|
||||||
export type { Subscription } from './models/Subscription';
|
export type { Subscription } from './models/Subscription';
|
||||||
@@ -87,6 +105,7 @@ export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
|
|||||||
export type { SuccessObject } from './models/SuccessObject';
|
export type { SuccessObject } from './models/SuccessObject';
|
||||||
export type { TableInfo } from './models/TableInfo';
|
export type { TableInfo } from './models/TableInfo';
|
||||||
export type { Task } from './models/Task';
|
export type { Task } from './models/Task';
|
||||||
|
export { TaskType } from './models/TaskType';
|
||||||
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
||||||
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
||||||
export type { TransferDBRequest } from './models/TransferDBRequest';
|
export type { TransferDBRequest } from './models/TransferDBRequest';
|
||||||
@@ -98,12 +117,15 @@ export type { UpdateCategoriesRequest } from './models/UpdateCategoriesRequest';
|
|||||||
export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest';
|
export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest';
|
||||||
export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest';
|
export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest';
|
||||||
export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse';
|
export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse';
|
||||||
|
export type { UpdateFileRequest } from './models/UpdateFileRequest';
|
||||||
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
|
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
|
||||||
export type { UpdaterStatus } from './models/UpdaterStatus';
|
export type { UpdaterStatus } from './models/UpdaterStatus';
|
||||||
export type { UpdateServerRequest } from './models/UpdateServerRequest';
|
export type { UpdateServerRequest } from './models/UpdateServerRequest';
|
||||||
export type { UpdateTaskDataRequest } from './models/UpdateTaskDataRequest';
|
export type { UpdateTaskDataRequest } from './models/UpdateTaskDataRequest';
|
||||||
|
export type { UpdateTaskOptionsRequest } from './models/UpdateTaskOptionsRequest';
|
||||||
export type { UpdateTaskScheduleRequest } from './models/UpdateTaskScheduleRequest';
|
export type { UpdateTaskScheduleRequest } from './models/UpdateTaskScheduleRequest';
|
||||||
export type { UpdateUserRequest } from './models/UpdateUserRequest';
|
export type { UpdateUserRequest } from './models/UpdateUserRequest';
|
||||||
|
export type { UploadCookiesRequest } from './models/UploadCookiesRequest';
|
||||||
export type { User } from './models/User';
|
export type { User } from './models/User';
|
||||||
export { UserPermission } from './models/UserPermission';
|
export { UserPermission } from './models/UserPermission';
|
||||||
export type { Version } from './models/Version';
|
export type { Version } from './models/Version';
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type AddFileToPlaylistRequest = {
|
||||||
export interface AddFileToPlaylistRequest {
|
|
||||||
file_uid: string;
|
file_uid: string;
|
||||||
playlist_id: string;
|
playlist_id: string;
|
||||||
}
|
};
|
||||||
|
|||||||
16
src/api-types/models/Archive.ts
Normal file
16
src/api-types/models/Archive.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { FileType } from './FileType';
|
||||||
|
|
||||||
|
export type Archive = {
|
||||||
|
extractor: string;
|
||||||
|
id: string;
|
||||||
|
type: FileType;
|
||||||
|
title: string;
|
||||||
|
user_uid?: string;
|
||||||
|
sub_id?: string;
|
||||||
|
timestamp: number;
|
||||||
|
uid: string;
|
||||||
|
};
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { UserPermission } from './UserPermission';
|
import type { UserPermission } from './UserPermission';
|
||||||
import { YesNo } from './YesNo';
|
import type { YesNo } from './YesNo';
|
||||||
|
|
||||||
export interface BaseChangePermissionsRequest {
|
export type BaseChangePermissionsRequest = {
|
||||||
permission: UserPermission;
|
permission: UserPermission;
|
||||||
new_value: YesNo;
|
new_value: YesNo;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { CategoryRule } from './CategoryRule';
|
import type { CategoryRule } from './CategoryRule';
|
||||||
|
|
||||||
export interface Category {
|
export type Category = {
|
||||||
name?: string;
|
name?: string;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
rules?: Array<CategoryRule>;
|
rules?: Array<CategoryRule>;
|
||||||
@@ -12,4 +12,4 @@ export interface Category {
|
|||||||
* Overrides file output for downloaded files in category
|
* Overrides file output for downloaded files in category
|
||||||
*/
|
*/
|
||||||
custom_output?: string;
|
custom_output?: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CategoryRule = {
|
||||||
export interface CategoryRule {
|
|
||||||
preceding_operator?: CategoryRule.preceding_operator;
|
preceding_operator?: CategoryRule.preceding_operator;
|
||||||
comparator?: CategoryRule.comparator;
|
comparator?: CategoryRule.comparator;
|
||||||
}
|
};
|
||||||
|
|
||||||
export namespace CategoryRule {
|
export namespace CategoryRule {
|
||||||
|
|
||||||
@@ -23,4 +22,4 @@ export namespace CategoryRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
|
import type { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
|
||||||
|
|
||||||
export interface ChangeRolePermissionsRequest extends BaseChangePermissionsRequest {
|
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
|
||||||
role: string;
|
role: string;
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
|
import type { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
|
||||||
|
|
||||||
export interface ChangeUserPermissionsRequest extends BaseChangePermissionsRequest {
|
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
|
||||||
user_uid: string;
|
user_uid: string;
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CheckConcurrentStreamRequest = {
|
||||||
export interface CheckConcurrentStreamRequest {
|
|
||||||
/**
|
/**
|
||||||
* UID of the concurrent stream
|
* UID of the concurrent stream
|
||||||
*/
|
*/
|
||||||
uid: string;
|
uid: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { ConcurrentStream } from './ConcurrentStream';
|
import type { ConcurrentStream } from './ConcurrentStream';
|
||||||
|
|
||||||
export interface CheckConcurrentStreamResponse {
|
export type CheckConcurrentStreamResponse = {
|
||||||
stream: ConcurrentStream;
|
stream: ConcurrentStream;
|
||||||
}
|
};
|
||||||
|
|||||||
7
src/api-types/models/CheckSubscriptionRequest.ts
Normal file
7
src/api-types/models/CheckSubscriptionRequest.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CheckSubscriptionRequest = {
|
||||||
|
sub_id: string;
|
||||||
|
};
|
||||||
9
src/api-types/models/ClearDownloadsRequest.ts
Normal file
9
src/api-types/models/ClearDownloadsRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type ClearDownloadsRequest = {
|
||||||
|
clear_finished?: boolean;
|
||||||
|
clear_paused?: boolean;
|
||||||
|
clear_errors?: boolean;
|
||||||
|
};
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type ConcurrentStream = {
|
||||||
export interface ConcurrentStream {
|
|
||||||
playback_timestamp?: number;
|
playback_timestamp?: number;
|
||||||
unix_timestamp?: number;
|
unix_timestamp?: number;
|
||||||
playing?: boolean;
|
playing?: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
export interface Config {
|
|
||||||
YoutubeDLMaterial: any;
|
YoutubeDLMaterial: any;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { Config } from './Config';
|
import type { Config } from './Config';
|
||||||
|
|
||||||
export interface ConfigResponse {
|
export type ConfigResponse = {
|
||||||
config_file: Config;
|
config_file: Config;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CreateCategoryRequest = {
|
||||||
export interface CreateCategoryRequest {
|
|
||||||
name: string;
|
name: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { Category } from './Category';
|
import type { Category } from './Category';
|
||||||
|
|
||||||
export interface CreateCategoryResponse {
|
export type CreateCategoryResponse = {
|
||||||
new_category?: Category;
|
new_category?: Category;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { FileType } from './FileType';
|
export type CreatePlaylistRequest = {
|
||||||
|
|
||||||
export interface CreatePlaylistRequest {
|
|
||||||
playlistName: string;
|
playlistName: string;
|
||||||
uids: Array<string>;
|
uids: Array<string>;
|
||||||
type: FileType;
|
|
||||||
thumbnailURL: string;
|
thumbnailURL: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { Playlist } from './Playlist';
|
import type { Playlist } from './Playlist';
|
||||||
|
|
||||||
export interface CreatePlaylistResponse {
|
export type CreatePlaylistResponse = {
|
||||||
new_playlist: Playlist;
|
new_playlist: Playlist;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CropFileSettings = {
|
||||||
export interface CropFileSettings {
|
|
||||||
cropFileStart: number;
|
cropFileStart: number;
|
||||||
cropFileEnd: number;
|
cropFileEnd: number;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DBBackup = {
|
||||||
export interface DBBackup {
|
|
||||||
name: string;
|
name: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
size: number;
|
size: number;
|
||||||
source: DBBackup.source;
|
source: DBBackup.source;
|
||||||
}
|
};
|
||||||
|
|
||||||
export namespace DBBackup {
|
export namespace DBBackup {
|
||||||
|
|
||||||
@@ -18,4 +17,4 @@ export namespace DBBackup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { TableInfo } from './TableInfo';
|
import type { TableInfo } from './TableInfo';
|
||||||
|
|
||||||
export interface DBInfoResponse {
|
export type DBInfoResponse = {
|
||||||
using_local_db?: boolean;
|
using_local_db?: boolean;
|
||||||
stats_by_table?: {
|
stats_by_table?: {
|
||||||
files?: TableInfo,
|
files?: TableInfo;
|
||||||
playlists?: TableInfo,
|
playlists?: TableInfo;
|
||||||
categories?: TableInfo,
|
categories?: TableInfo;
|
||||||
subscriptions?: TableInfo,
|
subscriptions?: TableInfo;
|
||||||
users?: TableInfo,
|
users?: TableInfo;
|
||||||
roles?: TableInfo,
|
roles?: TableInfo;
|
||||||
download_queue?: TableInfo,
|
download_queue?: TableInfo;
|
||||||
|
archives?: TableInfo;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
@@ -2,11 +2,16 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { Category } from './Category';
|
||||||
|
|
||||||
export interface DatabaseFile {
|
export type DatabaseFile = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* Backup if thumbnailPath is not defined
|
||||||
|
*/
|
||||||
thumbnailURL: string;
|
thumbnailURL: string;
|
||||||
|
thumbnailPath?: string;
|
||||||
isAudio: boolean;
|
isAudio: boolean;
|
||||||
/**
|
/**
|
||||||
* In seconds
|
* In seconds
|
||||||
@@ -14,9 +19,27 @@ export interface DatabaseFile {
|
|||||||
duration: number;
|
duration: number;
|
||||||
url: string;
|
url: string;
|
||||||
uploader: string;
|
uploader: string;
|
||||||
|
/**
|
||||||
|
* In bytes
|
||||||
|
*/
|
||||||
size: number;
|
size: number;
|
||||||
path: string;
|
path: string;
|
||||||
upload_date: string;
|
upload_date: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
|
user_uid?: string;
|
||||||
sharingEnabled?: boolean;
|
sharingEnabled?: boolean;
|
||||||
}
|
category?: Category;
|
||||||
|
view_count?: number;
|
||||||
|
local_view_count?: number;
|
||||||
|
sub_id?: string;
|
||||||
|
registered?: number;
|
||||||
|
/**
|
||||||
|
* In pixels, only for videos
|
||||||
|
*/
|
||||||
|
height?: number;
|
||||||
|
/**
|
||||||
|
* In Kbps
|
||||||
|
*/
|
||||||
|
abr?: number;
|
||||||
|
favorite: boolean;
|
||||||
|
};
|
||||||
|
|||||||
14
src/api-types/models/DeleteAllFilesResponse.ts
Normal file
14
src/api-types/models/DeleteAllFilesResponse.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DeleteAllFilesResponse = {
|
||||||
|
/**
|
||||||
|
* Number of files found matching search parameters
|
||||||
|
*/
|
||||||
|
file_count?: number;
|
||||||
|
/**
|
||||||
|
* Number of files removed
|
||||||
|
*/
|
||||||
|
delete_count?: number;
|
||||||
|
};
|
||||||
9
src/api-types/models/DeleteArchiveItemsRequest.ts
Normal file
9
src/api-types/models/DeleteArchiveItemsRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { Archive } from './Archive';
|
||||||
|
|
||||||
|
export type DeleteArchiveItemsRequest = {
|
||||||
|
archives: Array<Archive>;
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DeleteCategoryRequest = {
|
||||||
export interface DeleteCategoryRequest {
|
|
||||||
category_uid: string;
|
category_uid: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DeleteMp3Mp4Request = {
|
||||||
export interface DeleteMp3Mp4Request {
|
|
||||||
uid: string;
|
uid: string;
|
||||||
blacklistMode?: boolean;
|
blacklistMode?: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
7
src/api-types/models/DeleteNotificationRequest.ts
Normal file
7
src/api-types/models/DeleteNotificationRequest.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DeleteNotificationRequest = {
|
||||||
|
uid: string;
|
||||||
|
};
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { FileType } from './FileType';
|
export type DeletePlaylistRequest = {
|
||||||
|
|
||||||
export interface DeletePlaylistRequest {
|
|
||||||
playlist_id: string;
|
playlist_id: string;
|
||||||
type: FileType;
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,14 +2,10 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { SubscriptionRequestData } from './SubscriptionRequestData';
|
export type DeleteSubscriptionFileRequest = {
|
||||||
|
file_uid: string;
|
||||||
export interface DeleteSubscriptionFileRequest {
|
|
||||||
file: string;
|
|
||||||
file_uid?: string;
|
|
||||||
sub: SubscriptionRequestData;
|
|
||||||
/**
|
/**
|
||||||
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
||||||
*/
|
*/
|
||||||
deleteForever?: boolean;
|
deleteForever?: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DeleteUserRequest = {
|
||||||
export interface DeleteUserRequest {
|
|
||||||
uid: string;
|
uid: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type Download = {
|
||||||
export interface Download {
|
|
||||||
uid: string;
|
uid: string;
|
||||||
ui_uid?: string;
|
ui_uid?: string;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
finished: boolean;
|
finished: boolean;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
cancelled?: boolean;
|
||||||
finished_step: boolean;
|
finished_step: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -20,7 +20,12 @@ export interface Download {
|
|||||||
* Error text, set if download fails.
|
* Error text, set if download fails.
|
||||||
*/
|
*/
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
|
/**
|
||||||
|
* Error type, may or may not be set in case of an error
|
||||||
|
*/
|
||||||
|
error_type?: string | null;
|
||||||
user_uid?: string;
|
user_uid?: string;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
sub_name?: string;
|
sub_name?: string;
|
||||||
}
|
prefetched_info?: any;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { FileType } from './FileType';
|
||||||
|
|
||||||
export interface DownloadArchiveRequest {
|
export type DownloadArchiveRequest = {
|
||||||
sub: {
|
type?: FileType;
|
||||||
archive_dir: string,
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { FileType } from './FileType';
|
import type { FileType } from './FileType';
|
||||||
|
|
||||||
export interface DownloadFileRequest {
|
export type DownloadFileRequest = {
|
||||||
uid?: string;
|
uid?: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
playlist_id?: string;
|
playlist_id?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { CropFileSettings } from './CropFileSettings';
|
import type { CropFileSettings } from './CropFileSettings';
|
||||||
import { FileType } from './FileType';
|
import type { FileType } from './FileType';
|
||||||
|
|
||||||
export interface DownloadRequest {
|
export type DownloadRequest = {
|
||||||
url: string;
|
url: string;
|
||||||
/**
|
/**
|
||||||
* Video format code. Overrides other quality options.
|
* Video format code. Overrides other quality options.
|
||||||
@@ -35,10 +35,18 @@ export interface DownloadRequest {
|
|||||||
* Height of the video, if known
|
* Height of the video, if known
|
||||||
*/
|
*/
|
||||||
selectedHeight?: string;
|
selectedHeight?: string;
|
||||||
|
/**
|
||||||
|
* Max height that should be used, useful for playlists. selectedHeight will override this.
|
||||||
|
*/
|
||||||
|
maxHeight?: string;
|
||||||
/**
|
/**
|
||||||
* Specify ffmpeg/avconv audio quality
|
* Specify ffmpeg/avconv audio quality
|
||||||
*/
|
*/
|
||||||
maxBitrate?: string;
|
maxBitrate?: string;
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
cropFileSettings?: CropFileSettings;
|
cropFileSettings?: CropFileSettings;
|
||||||
}
|
/**
|
||||||
|
* If using youtube-dl archive, download will ignore it
|
||||||
|
*/
|
||||||
|
ignoreArchive?: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { Download } from './Download';
|
import type { Download } from './Download';
|
||||||
|
|
||||||
export interface DownloadResponse {
|
export type DownloadResponse = {
|
||||||
download?: Download;
|
download?: Download;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { FileType } from './FileType';
|
import type { FileType } from './FileType';
|
||||||
import { Subscription } from './Subscription';
|
import type { Subscription } from './Subscription';
|
||||||
|
|
||||||
export interface DownloadTwitchChatByVODIDRequest {
|
export type DownloadTwitchChatByVODIDRequest = {
|
||||||
/**
|
/**
|
||||||
* File ID
|
* File ID
|
||||||
*/
|
*/
|
||||||
@@ -20,4 +20,4 @@ export interface DownloadTwitchChatByVODIDRequest {
|
|||||||
*/
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
sub?: Subscription;
|
sub?: Subscription;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { TwitchChatMessage } from './TwitchChatMessage';
|
import type { TwitchChatMessage } from './TwitchChatMessage';
|
||||||
|
|
||||||
export interface DownloadTwitchChatByVODIDResponse {
|
export type DownloadTwitchChatByVODIDResponse = {
|
||||||
chat: Array<TwitchChatMessage>;
|
chat: Array<TwitchChatMessage>;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type DownloadVideosForSubscriptionRequest = {
|
||||||
export interface DownloadVideosForSubscriptionRequest {
|
|
||||||
subID: string;
|
subID: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
export enum FileType {
|
export enum FileType {
|
||||||
AUDIO = 'audio',
|
AUDIO = 'audio',
|
||||||
VIDEO = 'video',
|
VIDEO = 'video',
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/api-types/models/FileTypeFilter.ts
Normal file
9
src/api-types/models/FileTypeFilter.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export enum FileTypeFilter {
|
||||||
|
AUDIO_ONLY = 'audio_only',
|
||||||
|
VIDEO_ONLY = 'video_only',
|
||||||
|
BOTH = 'both',
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type GenerateArgsResponse = {
|
||||||
export interface GenerateArgsResponse {
|
|
||||||
args?: Array<string>;
|
args?: Array<string>;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type GenerateNewApiKeyResponse = {
|
||||||
export interface GenerateNewApiKeyResponse {
|
|
||||||
new_api_key: string;
|
new_api_key: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { Category } from './Category';
|
import type { Category } from './Category';
|
||||||
|
|
||||||
export interface GetAllCategoriesResponse {
|
export type GetAllCategoriesResponse = {
|
||||||
categories: Array<Category>;
|
categories: Array<Category>;
|
||||||
}
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user