mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 12:30:03 +03:00
Compare commits
399 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9f197e46d | ||
|
|
3732d13562 | ||
|
|
cf14880d21 | ||
|
|
e81d0cab42 | ||
|
|
7e24180f03 | ||
|
|
053c8db9dd | ||
|
|
5537852134 | ||
|
|
efdc471ccf | ||
|
|
6f1b37d5eb | ||
|
|
06557673a2 | ||
|
|
c20d09e902 | ||
|
|
a68ecfa730 | ||
|
|
86c609c1b2 | ||
|
|
d100e80ccf | ||
|
|
5511c94071 | ||
|
|
b21886d8f8 | ||
|
|
e535603103 | ||
|
|
9415901f17 | ||
|
|
92e5716f93 | ||
|
|
5b5c93f783 | ||
|
|
4db6a49df5 | ||
|
|
3abb29eee4 | ||
|
|
e5461de2f7 | ||
|
|
4a69a0d362 | ||
|
|
97f7f0b462 | ||
|
|
d3cbfa265e | ||
|
|
42bd219ed6 | ||
|
|
f8123cf03b | ||
|
|
94df98e5d0 | ||
|
|
2998562655 | ||
|
|
09d8ce04d7 | ||
|
|
504c818c2f | ||
|
|
ca0e6b993d | ||
|
|
0346833c3b | ||
|
|
32da9dd9dd | ||
|
|
20f162d794 | ||
|
|
319bb0160b | ||
|
|
5983a8bd52 | ||
|
|
49b8cd416e | ||
|
|
58f71469b5 | ||
|
|
db81120645 | ||
|
|
163a88bcfd | ||
|
|
2441270d88 | ||
|
|
a518ac680f | ||
|
|
78d3145e0b | ||
|
|
b8a4e0773f | ||
|
|
f04139634a | ||
|
|
a074166903 | ||
|
|
6893dbd506 | ||
|
|
e8ee4ffb64 | ||
|
|
378025bd9d | ||
|
|
d8e85df6d6 | ||
|
|
0c864c3d8d | ||
|
|
dd8ab9be29 | ||
|
|
bab354ce81 | ||
|
|
d3d0f92ea5 | ||
|
|
b37d912e04 | ||
|
|
1c93a4f9f2 | ||
|
|
abfe0dad03 | ||
|
|
5bfecfcefe | ||
|
|
ffe3133635 | ||
|
|
68c67ca7d5 | ||
|
|
c4d50c9018 | ||
|
|
42b749a101 | ||
|
|
9c729abfaa | ||
|
|
dcc7fbd81c | ||
|
|
b3c8f9e57a | ||
|
|
80f214fdde | ||
|
|
57a9434b3c | ||
|
|
cec0ed78ec | ||
|
|
c8a8046056 | ||
|
|
73cd142b77 | ||
|
|
b071fb9e2e | ||
|
|
f485da06b5 | ||
|
|
59098d4693 | ||
|
|
4cf92b8f3d | ||
|
|
e07acfd4b3 | ||
|
|
05d962328b | ||
|
|
0816cb7046 | ||
|
|
c6553d99c6 | ||
|
|
8bf3680b6f | ||
|
|
9e5ad66a9d | ||
|
|
3487813cb5 | ||
|
|
ecc2737a05 | ||
|
|
39e737024f | ||
|
|
550013a2e7 | ||
|
|
b6f8551cfa | ||
|
|
98e94d3c38 | ||
|
|
8c94255f61 | ||
|
|
409fd0fe20 | ||
|
|
d4ad1f9fce | ||
|
|
cc47823b0c | ||
|
|
747735dffe | ||
|
|
76b63329ca | ||
|
|
f094d18e03 | ||
|
|
c1b0b583e4 | ||
|
|
a1c9c97616 | ||
|
|
0504167734 | ||
|
|
49081db8cb | ||
|
|
98e33ac399 | ||
|
|
a3424f973e | ||
|
|
8e5db3e9d1 | ||
|
|
1861011fb0 | ||
|
|
74e47b7d04 | ||
|
|
68f791eea3 | ||
|
|
f73ec2dd94 | ||
|
|
26ad195597 | ||
|
|
fb23d7c41e | ||
|
|
4e6d68d9e6 | ||
|
|
8bc99fb557 | ||
|
|
8277c95c4e | ||
|
|
e5db376914 | ||
|
|
661b96cfe5 | ||
|
|
2eef1b062c | ||
|
|
d376ee4409 | ||
|
|
7661b1e79e | ||
|
|
0401769968 | ||
|
|
c901efebc4 | ||
|
|
4e550da4f6 | ||
|
|
ae76e9db8d | ||
|
|
d763f88ceb | ||
|
|
a8b188cd22 | ||
|
|
1034aa1980 | ||
|
|
da26d88ba9 | ||
|
|
93e117a7aa | ||
|
|
ae9c52a14d | ||
|
|
b685b955df | ||
|
|
e7b841c056 | ||
|
|
e5f9694da0 | ||
|
|
81b0ef4a72 | ||
|
|
31f581c642 | ||
|
|
d2af233a1f | ||
|
|
0fb00bac12 | ||
|
|
6980828853 | ||
|
|
a48e122763 | ||
|
|
2d66d653f6 | ||
|
|
03ea04f8d8 | ||
|
|
8fbb1c9bbd | ||
|
|
c67d6ea89a | ||
|
|
a701d0fe83 | ||
|
|
ff51a49d1b | ||
|
|
46b595db45 | ||
|
|
4b2b278439 | ||
|
|
1ac6683f33 | ||
|
|
e790c9fadf | ||
|
|
c18bf568c7 | ||
|
|
fa1b291f97 | ||
|
|
cb6451ef96 | ||
|
|
912a419bd4 | ||
|
|
a7c810136b | ||
|
|
e6ea2238f8 | ||
|
|
98f1d003c3 | ||
|
|
c3cc28540f | ||
|
|
68ed66072e | ||
|
|
eca06a7fb1 | ||
|
|
b583305940 | ||
|
|
f361b8a974 | ||
|
|
1565c328d5 | ||
|
|
a6534f66a6 | ||
|
|
a78ccefc83 | ||
|
|
6fe7d20498 | ||
|
|
d887380fd1 | ||
|
|
1f3572a630 | ||
|
|
da8571fb1a | ||
|
|
4617362270 | ||
|
|
bdb5072014 | ||
|
|
e5baf094c9 | ||
|
|
264b3606d6 | ||
|
|
2408184cc7 | ||
|
|
e4851253dd | ||
|
|
87696f71f8 | ||
|
|
d6fe2a5720 | ||
|
|
90c2d3f70b | ||
|
|
0d54cb9872 | ||
|
|
a8d6298cfd | ||
|
|
65b31633d9 | ||
|
|
3db3077dec | ||
|
|
61633e817b | ||
|
|
6cc93ba4f9 | ||
|
|
9b8b92b8df | ||
|
|
d9f6b8c64c | ||
|
|
10b59191f6 | ||
|
|
a89787698b | ||
|
|
3d3ab5180f | ||
|
|
eddc25566d | ||
|
|
b5a82b9385 | ||
|
|
2082a78846 | ||
|
|
fe170a4de8 | ||
|
|
18dab72b51 | ||
|
|
6849bd00d5 | ||
|
|
1e96e31053 | ||
|
|
02441ac846 | ||
|
|
4666aa15b3 | ||
|
|
f6f54c0e53 | ||
|
|
e15141c5e0 | ||
|
|
a61606b69f | ||
|
|
d96fab49f5 | ||
|
|
346d41d3e1 | ||
|
|
597e1f5b60 | ||
|
|
dd0f58d421 | ||
|
|
2ca6aa7bd7 | ||
|
|
ba2b837cc5 | ||
|
|
22f0ee834b | ||
|
|
cb02227302 | ||
|
|
1b4f2830f5 | ||
|
|
720fceefb6 | ||
|
|
fa488015c3 | ||
|
|
88f1b3daff | ||
|
|
0ea8a11c85 | ||
|
|
14bf2248cf | ||
|
|
822aec4de8 | ||
|
|
2a1aa4036c | ||
|
|
2414e16021 | ||
|
|
69cd22d992 | ||
|
|
1905129201 | ||
|
|
7ef6c78612 | ||
|
|
1d9595d056 | ||
|
|
d258bc2218 | ||
|
|
4d3a687d34 | ||
|
|
2b91293abd | ||
|
|
3990e25c18 | ||
|
|
2f0bbca15c | ||
|
|
717f6deb11 | ||
|
|
c36867d368 | ||
|
|
458e4b45f8 | ||
|
|
c40513ba4a | ||
|
|
6fa52cecbc | ||
|
|
a5474141bb | ||
|
|
89ececdbeb | ||
|
|
58718b6e3b | ||
|
|
a5224f80a8 | ||
|
|
c2ee6b6230 | ||
|
|
37614a1611 | ||
|
|
b71bdfcec2 | ||
|
|
1b09bf4881 | ||
|
|
82df232f03 | ||
|
|
af4de44016 | ||
|
|
61f27d6fe9 | ||
|
|
b3dbdd1790 | ||
|
|
785306c59a | ||
|
|
38774d8593 | ||
|
|
df11aca1e0 | ||
|
|
bcff631936 | ||
|
|
347df89aa7 | ||
|
|
eb084d03b2 | ||
|
|
8c942b0343 | ||
|
|
baad97182a | ||
|
|
26ca5d51a5 | ||
|
|
0c5cd291fe | ||
|
|
57234b4690 | ||
|
|
b993c8e1d6 | ||
|
|
66cb0af762 | ||
|
|
8331c319ce | ||
|
|
da9dcc0249 | ||
|
|
4b9dc4a950 | ||
|
|
5af0d742ef | ||
|
|
ca3a42c075 | ||
|
|
4906e52c57 | ||
|
|
d33346b11d | ||
|
|
b07f16bdd0 | ||
|
|
a5c47737c7 | ||
|
|
603c13eb4c | ||
|
|
a35d85d7df | ||
|
|
25b5b28df8 | ||
|
|
0c60c12124 | ||
|
|
72b42dea5a | ||
|
|
d0221f2233 | ||
|
|
43ea401d53 | ||
|
|
1ed415d733 | ||
|
|
1808281b50 | ||
|
|
b4dc655f2f | ||
|
|
47a1173a80 | ||
|
|
6c22c0e708 | ||
|
|
0d756c4c97 | ||
|
|
d4664bad45 | ||
|
|
03e3eb9a81 | ||
|
|
9e4d36e6ed | ||
|
|
7c605d83cd | ||
|
|
a4f97d3814 | ||
|
|
c003b02153 | ||
|
|
f478f14de1 | ||
|
|
d0ffb4d90e | ||
|
|
6d62669a43 | ||
|
|
8c6478bfef | ||
|
|
9384436f7e | ||
|
|
bb3c85b0e1 | ||
|
|
d7068953a8 | ||
|
|
7d9ad0fce1 | ||
|
|
b3b2175c67 | ||
|
|
d8ea848e26 | ||
|
|
fb5054a1d7 | ||
|
|
25dc8d137a | ||
|
|
1d6fddf386 | ||
|
|
5dce997e3a | ||
|
|
3a6d0f38d7 | ||
|
|
17d877f44a | ||
|
|
c9a95e48fb | ||
|
|
5cced5aed8 | ||
|
|
fdba83bd91 | ||
|
|
8bcf46d6aa | ||
|
|
945d2abd1a | ||
|
|
4094fda71d | ||
|
|
aed64b09fe | ||
|
|
fa2800a391 | ||
|
|
4aaaddac92 | ||
|
|
c3220dcb60 | ||
|
|
996d5989e7 | ||
|
|
7681ac2e5a | ||
|
|
87e7f00aef | ||
|
|
0ac87fe86d | ||
|
|
ce2f294a3d | ||
|
|
2b2a033b7e | ||
|
|
f796a5863c | ||
|
|
57e3f1b2ac | ||
|
|
8f17305710 | ||
|
|
4b67527bd5 | ||
|
|
4f119d081e | ||
|
|
2e71a0bef1 | ||
|
|
1f9f07ac56 | ||
|
|
c4fe1a2fb0 | ||
|
|
b987e258b5 | ||
|
|
bdb6a08274 | ||
|
|
b2730926c8 | ||
|
|
393267b919 | ||
|
|
35f8454db3 | ||
|
|
1837101083 | ||
|
|
aa80f52838 | ||
|
|
a5e1906196 | ||
|
|
9dc607e7ee | ||
|
|
1a79b489ab | ||
|
|
3bdacd4b52 | ||
|
|
f08506d690 | ||
|
|
1d29dd8df4 | ||
|
|
25b953cebd | ||
|
|
ff2f5c89da | ||
|
|
2cd35cccd1 | ||
|
|
da399601e1 | ||
|
|
4ceec26a0e | ||
|
|
b765830584 | ||
|
|
44bff55a88 | ||
|
|
84c9ec1300 | ||
|
|
f1c09a5fa9 | ||
|
|
d39f6f7a17 | ||
|
|
190d1567ca | ||
|
|
e71dc0b5e5 | ||
|
|
7d6ad6336c | ||
|
|
087b9aac7e | ||
|
|
d61573d8af | ||
|
|
c7885e8726 | ||
|
|
ebd0a419df | ||
|
|
c96aec7ba3 | ||
|
|
ddcd1d0772 | ||
|
|
bcd706f477 | ||
|
|
c17d29075e | ||
|
|
25b65b08d5 | ||
|
|
0981dd216d | ||
|
|
20b580b76f | ||
|
|
bc4cc3ccce | ||
|
|
21797f3901 | ||
|
|
91f6dbcb5b | ||
|
|
d9edb40cd5 | ||
|
|
c752b13732 | ||
|
|
bad4b8630b | ||
|
|
4f4a82c3c4 | ||
|
|
25307dc46b | ||
|
|
d873597ea2 | ||
|
|
f92a5549b5 | ||
|
|
69bf4e1ad5 | ||
|
|
9d1aaf95ed | ||
|
|
bb925ac0c8 | ||
|
|
74cda25c63 | ||
|
|
54dcbe452e | ||
|
|
946abd2e92 | ||
|
|
73d4cca615 | ||
|
|
846dd7e250 | ||
|
|
6f3e94cf24 | ||
|
|
3cbb517d64 | ||
|
|
5d945d729b | ||
|
|
cda842a6c7 | ||
|
|
480ed7d000 | ||
|
|
881a103051 | ||
|
|
4172b0b355 | ||
|
|
f6b7c41666 | ||
|
|
3d1874c69b | ||
|
|
ccfe7901c9 | ||
|
|
17e85196ae | ||
|
|
ae605d5f70 | ||
|
|
e57839e8de | ||
|
|
09bcac1c14 | ||
|
|
f5073b83ed | ||
|
|
41bfc80c4e | ||
|
|
3d2e138f50 | ||
|
|
717f024c42 | ||
|
|
a70abb3945 | ||
|
|
a755b0b281 | ||
|
|
434f6751d0 | ||
|
|
dfecf3645b | ||
|
|
62a000b631 | ||
|
|
2673f4ee98 |
23
.gitignore
vendored
23
.gitignore
vendored
@@ -43,10 +43,25 @@ Thumbs.db
|
||||
|
||||
node_modules/*
|
||||
backend/node_modules/*
|
||||
backend/public/*
|
||||
YoutubeDL-Material/node_modules/*
|
||||
backend/video/*
|
||||
backend/audio/*
|
||||
backend/public/*
|
||||
backend/subscriptions/archives/*
|
||||
backend/subscriptions/playlists/*
|
||||
backend/subscriptions/channels/*
|
||||
backend/db.json
|
||||
backend/public/assets/i18n/*.xlf
|
||||
backend/public/assets/default.json
|
||||
backend/subscriptions/channels/*
|
||||
backend/subscriptions/playlists/*
|
||||
backend/subscriptions/archives/*
|
||||
backend/*.exe
|
||||
src/assets/default.json
|
||||
backend/appdata/db.json
|
||||
backend/appdata/archives/archive_audio.txt
|
||||
backend/appdata/archives/archive_video.txt
|
||||
backend/appdata/archives/blacklist_audio.txt
|
||||
backend/appdata/archives/blacklist_video.txt
|
||||
backend/appdata/logs/combined.log
|
||||
backend/appdata/logs/error.log
|
||||
backend/appdata/users.json
|
||||
backend/users/*
|
||||
backend/appdata/cookies.txt
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,21 +0,0 @@
|
||||
FROM alpine:3.11
|
||||
|
||||
RUN apk add --update npm python ffmpeg
|
||||
|
||||
# Change directory so that our commands run inside this new directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency definitions
|
||||
COPY ./ /app/
|
||||
|
||||
# Change directory to backend
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies on backend
|
||||
RUN npm install
|
||||
|
||||
# Expose the port the app runs in
|
||||
EXPOSE 17442
|
||||
|
||||
# Run the specified command within the container.
|
||||
CMD [ "node", "app.js" ]
|
||||
1700
Public API v1.yaml
Normal file
1700
Public API v1.yaml
Normal file
File diff suppressed because it is too large
Load Diff
57
README.md
57
README.md
@@ -1,6 +1,12 @@
|
||||
# YoutubeDL-Material
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||
[](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 8](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 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
|
||||
Now with [Docker](#Docker) support!
|
||||
|
||||
@@ -10,36 +16,40 @@ Check out the prerequisites, and go to the installation section. Easy as pie!
|
||||
|
||||
Here's an image of what it'll look like once you're done:
|
||||
|
||||

|
||||

|
||||
|
||||
With optional file management enabled (default):
|
||||
|
||||

|
||||

|
||||
|
||||
Dark mode:
|
||||
|
||||

|
||||

|
||||
|
||||
### Prerequisites
|
||||
|
||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||
|
||||
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
||||
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
||||
|
||||
```
|
||||
sudo apt-get install nodejs youtube-dl
|
||||
```
|
||||
|
||||
Optional dependencies:
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
|
||||
### Installing
|
||||
|
||||
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||
|
||||
2. Drag all the files in `youtubedl-material` to an easily accessible directory. Navigate to the `config` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
||||
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
||||
|
||||
NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup), this next step is not necessary
|
||||
|
||||
NOTE: If you are intending to use a reverse proxy, this next step is not necessary
|
||||
3. Port forward the port listed in `default.json`, which defaults to `17442`.
|
||||
|
||||
4. Once the configuration is done, run `npm install` to install all the backend dependencies. Once that is finished, type `nodejs app.js`. This will run the backend server, which serves the frontend as well. On your browser, navigate to to the server (url with the specified port). Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
|
||||
4. Once the configuration is done, run `npm install` to install all the backend dependencies. Once that is finished, type `npm start`. This will run the backend server, which serves the frontend as well. On your browser, navigate to to the server (url with the specified port). Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
|
||||
|
||||
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
|
||||
|
||||
@@ -69,29 +79,40 @@ Here is an explanation for the configuration entries. Check out the [default con
|
||||
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
|
||||
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
|
||||
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
|
||||
| allow_advanced_download | true if you want to use the Advanced download options - NOT FULLY IMPLEMENTED | false |
|
||||
| allow_advanced_download | true if you want to use the Advanced download options | false |
|
||||
|
||||
## Deployment
|
||||
## Build it yourself
|
||||
|
||||
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
|
||||
|
||||
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/backend/config`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` 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 `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
||||
|
||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `nodejs app.js`.
|
||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
||||
|
||||
Finally, port forward the port specified in the config (defaults to `17442`) and point it to the server's IP address. Make sure the port is also allowed through the server's firewall.
|
||||
Finally, if you want your instance to be available from outside your network, you can set up a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup).
|
||||
|
||||
Alternatively, you can port forward the port specified in the config (defaults to `17442`) and point it to the server's IP address. Make sure the port is also allowed through the server's firewall.
|
||||
|
||||
## Docker
|
||||
|
||||
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
||||
|
||||
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest release of `docker-compose.yml`, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
2. Modify the config items in the `environment` section of `docker-compose.yml` to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
||||
3. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||
4. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
||||
5. Make sure you can connect to the specified URL + port, and if so, you are done!
|
||||
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/youtubedl-material-docker.zip -o youtubedl-material-docker.zip` to download the latest Docker zip release, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
2. Unzip the `youtubedl-material-docker.zip` and navigate into the root folder.
|
||||
3. Modify the config items in the `appdata` folder to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
||||
4. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||
5. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
||||
6. Make sure you can connect to the specified URL + port, and if so, you are done!
|
||||
|
||||
## API
|
||||
|
||||
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
|
||||
|
||||
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
|
||||
|
||||
Once you have enabled the API and have the key, you can start sending requests by adding the query param `apiKey=API_KEY`. Replace `API_KEY` with your actual API key, and you should be good to go! Nearly all of the backend should be at your disposal. View available endpoints in the link above.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
23
angular.json
23
angular.json
@@ -7,11 +7,18 @@
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"es": "src/locale/messages.es.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"aot": true,
|
||||
"outputPath": "backend/public",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
@@ -30,6 +37,12 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
@@ -45,6 +58,9 @@
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"es": {
|
||||
"localize": ["es"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -56,6 +72,9 @@
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "youtube-dl-material:build:production"
|
||||
},
|
||||
"es": {
|
||||
"browserTarget": "youtube-dl-material:build:es"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -176,7 +195,7 @@
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "scss"
|
||||
"style": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
|
||||
7
app.json
Normal file
7
app.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "YoutubeDL-Material",
|
||||
"description": "An open-source and self-hosted YouTube downloader based on Google's Material Design specifications.",
|
||||
"repository": "https://github.com/Tzahi12345/YoutubeDL-Material",
|
||||
"logo": "https://i.imgur.com/GPzvPiU.png",
|
||||
"keywords": ["youtube-dl", "youtubedl-material", "nodejs"]
|
||||
}
|
||||
4
backend/.dockerignore
Normal file
4
backend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
*.exe
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
27
backend/Dockerfile
Normal file
27
backend/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM alpine:3.12
|
||||
|
||||
ENV UID=1000 \
|
||||
GID=1000 \
|
||||
USER=youtube
|
||||
|
||||
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ffmpeg \
|
||||
npm \
|
||||
python2 \
|
||||
su-exec \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
||||
atomicparsley
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=$UID:$GID [ "package.json", "package-lock.json", "/app/" ]
|
||||
|
||||
RUN npm install && chown -R $UID:$GID ./
|
||||
|
||||
COPY --chown=$UID:$GID [ "./", "/app/" ]
|
||||
|
||||
EXPOSE 17442
|
||||
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "node", "app.js" ]
|
||||
2781
backend/app.js
2781
backend/app.js
File diff suppressed because it is too large
Load Diff
57
backend/appdata/default.json
Normal file
57
backend/appdata/default.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,23 @@
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"title_top": "YoutubeDL-Material",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
@@ -28,10 +35,23 @@
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": false
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
4
backend/audio/.gitignore
vendored
Normal file
4
backend/audio/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
540
backend/authentication/auth.js
Normal file
540
backend/authentication/auth.js
Normal file
@@ -0,0 +1,540 @@
|
||||
const path = require('path');
|
||||
const config_api = require('../config');
|
||||
const consts = require('../consts');
|
||||
var subscriptions_api = require('../subscriptions')
|
||||
const fs = require('fs-extra');
|
||||
var jwt = require('jsonwebtoken');
|
||||
const { uuid } = require('uuidv4');
|
||||
var bcrypt = require('bcrypt');
|
||||
|
||||
|
||||
var LocalStrategy = require('passport-local').Strategy;
|
||||
var JwtStrategy = require('passport-jwt').Strategy,
|
||||
ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||
|
||||
// other required vars
|
||||
let logger = null;
|
||||
var users_db = null;
|
||||
let SERVER_SECRET = null;
|
||||
let JWT_EXPIRATION = null;
|
||||
let opts = null;
|
||||
let saltRounds = null;
|
||||
|
||||
exports.initialize = function(input_users_db, input_logger) {
|
||||
setLogger(input_logger)
|
||||
setDB(input_users_db);
|
||||
|
||||
/*************************
|
||||
* Authentication module
|
||||
************************/
|
||||
saltRounds = 10;
|
||||
|
||||
JWT_EXPIRATION = (60 * 60); // one hour
|
||||
|
||||
SERVER_SECRET = null;
|
||||
if (users_db.get('jwt_secret').value()) {
|
||||
SERVER_SECRET = users_db.get('jwt_secret').value();
|
||||
} else {
|
||||
SERVER_SECRET = uuid();
|
||||
users_db.set('jwt_secret', SERVER_SECRET).write();
|
||||
}
|
||||
|
||||
opts = {}
|
||||
opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
|
||||
opts.secretOrKey = SERVER_SECRET;
|
||||
/*opts.issuer = 'example.com';
|
||||
opts.audience = 'example.com';*/
|
||||
|
||||
exports.passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
|
||||
const user = users_db.get('users').find({uid: jwt_payload.user}).value();
|
||||
if (user) {
|
||||
return done(null, user);
|
||||
} else {
|
||||
return done(null, false);
|
||||
// or you could create a new account
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function setLogger(input_logger) {
|
||||
logger = input_logger;
|
||||
}
|
||||
|
||||
function setDB(input_users_db) {
|
||||
users_db = input_users_db;
|
||||
}
|
||||
|
||||
exports.passport = require('passport');
|
||||
|
||||
exports.passport.serializeUser(function(user, done) {
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
exports.passport.deserializeUser(function(user, done) {
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
/***************************************
|
||||
* Register user with hashed password
|
||||
**************************************/
|
||||
exports.registerUser = 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'))) {
|
||||
res.sendStatus(409);
|
||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
bcrypt.hash(plaintextPassword, saltRounds)
|
||||
.then(function(hash) {
|
||||
let new_user = {
|
||||
name: username,
|
||||
uid: userid,
|
||||
passhash: hash,
|
||||
files: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
subscriptions: [],
|
||||
created: Date.now(),
|
||||
role: userid === 'admin' ? 'admin' : 'user',
|
||||
permissions: [],
|
||||
permission_overrides: []
|
||||
};
|
||||
// check if user exists
|
||||
if (users_db.get('users').find({uid: userid}).value()) {
|
||||
// user id is taken!
|
||||
logger.error('Registration failed: UID is already taken!');
|
||||
res.status(409).send('UID is already taken!');
|
||||
} else if (users_db.get('users').find({name: username}).value()) {
|
||||
// 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
|
||||
users_db.get('users').push(new_user).write();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/***************************************
|
||||
* Login methods
|
||||
**************************************/
|
||||
|
||||
/*************************************************
|
||||
* This gets called when passport.authenticate()
|
||||
* gets called.
|
||||
*
|
||||
* This checks that the credentials are valid.
|
||||
* If so, passes the user info to the next middleware.
|
||||
************************************************/
|
||||
|
||||
|
||||
exports.passport.use(new LocalStrategy({
|
||||
usernameField: 'userid',
|
||||
passwordField: 'password'},
|
||||
function(username, password, done) {
|
||||
const user = users_db.get('users').find({name: username}).value();
|
||||
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
|
||||
if (user) {
|
||||
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
/*passport.use(new BasicStrategy(
|
||||
function(userid, plainTextPassword, done) {
|
||||
const user = users_db.get('users').find({name: userid}).value();
|
||||
if (user) {
|
||||
var hashedPwd = user.passhash;
|
||||
return bcrypt.compare(plainTextPassword, hashedPwd);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
));
|
||||
*/
|
||||
|
||||
/*************************************************************
|
||||
* This is a wrapper for auth.passport.authenticate().
|
||||
* We use this to change WWW-Authenticate header so
|
||||
* the browser doesn't pop-up challenge dialog box by default.
|
||||
* Browser's will pop-up up dialog when status is 401 and
|
||||
* "WWW-Authenticate:Basic..."
|
||||
*************************************************************/
|
||||
/*
|
||||
exports.authenticateViaPassport = function(req, res, next) {
|
||||
exports.passport.authenticate('basic',{session:false},
|
||||
function(err, user, info) {
|
||||
if(!user){
|
||||
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
|
||||
res.status(401).send('Invalid Authentication');
|
||||
} else {
|
||||
req.user = user;
|
||||
next();
|
||||
}
|
||||
}
|
||||
)(req, res, next);
|
||||
};
|
||||
*/
|
||||
|
||||
/**********************************
|
||||
* Generating/Signing a JWT token
|
||||
* And attaches the user info into
|
||||
* the payload to be sent on every
|
||||
* request.
|
||||
*********************************/
|
||||
exports.generateJWT = function(req, res, next) {
|
||||
var payload = {
|
||||
exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION
|
||||
, user: req.user.uid
|
||||
};
|
||||
req.token = jwt.sign(payload, SERVER_SECRET);
|
||||
next();
|
||||
}
|
||||
|
||||
exports.returnAuthResponse = function(req, res) {
|
||||
res.status(200).json({
|
||||
user: req.user,
|
||||
token: req.token,
|
||||
permissions: exports.userPermissions(req.user.uid),
|
||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
||||
});
|
||||
}
|
||||
|
||||
/***************************************
|
||||
* Authorization: middleware that checks the
|
||||
* JWT token for validity before allowing
|
||||
* the user to access anything.
|
||||
*
|
||||
* It also passes the user object to the next
|
||||
* middleware through res.locals
|
||||
**************************************/
|
||||
exports.ensureAuthenticatedElseError = function(req, res, next) {
|
||||
var token = getToken(req.query);
|
||||
if( token ) {
|
||||
try {
|
||||
var payload = jwt.verify(token, SERVER_SECRET);
|
||||
// console.log('payload: ' + JSON.stringify(payload));
|
||||
// check if user still exists in database if you'd like
|
||||
res.locals.user = payload.user;
|
||||
next();
|
||||
} catch(err) {
|
||||
res.status(401).send('Invalid Authentication');
|
||||
}
|
||||
} else {
|
||||
res.status(401).send('Missing Authorization header');
|
||||
}
|
||||
}
|
||||
|
||||
// change password
|
||||
exports.changeUserPassword = async function(user_uid, new_pass) {
|
||||
return new Promise(resolve => {
|
||||
bcrypt.hash(new_pass, saltRounds)
|
||||
.then(function(hash) {
|
||||
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
|
||||
resolve(true);
|
||||
}).catch(err => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// change user permissions
|
||||
exports.changeUserPermissions = function(user_uid, permission, new_value) {
|
||||
try {
|
||||
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
||||
user_db_obj.get('permissions').pull(permission).write();
|
||||
user_db_obj.get('permission_overrides').pull(permission).write();
|
||||
if (new_value === 'yes') {
|
||||
user_db_obj.get('permissions').push(permission).write();
|
||||
user_db_obj.get('permission_overrides').push(permission).write();
|
||||
} else if (new_value === 'no') {
|
||||
user_db_obj.get('permission_overrides').push(permission).write();
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// change role permissions
|
||||
exports.changeRolePermissions = function(role, permission, new_value) {
|
||||
try {
|
||||
const role_db_obj = users_db.get('roles').get(role);
|
||||
role_db_obj.get('permissions').pull(permission).write();
|
||||
if (new_value === 'yes') {
|
||||
role_db_obj.get('permissions').push(permission).write();
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
exports.adminExists = function() {
|
||||
return !!users_db.get('users').find({uid: 'admin'}).value();
|
||||
}
|
||||
|
||||
// video stuff
|
||||
|
||||
exports.getUserVideos = function(user_uid, type) {
|
||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
||||
return user['files'][type];
|
||||
}
|
||||
|
||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
||||
if (!type) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
||||
if (!file) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
||||
if (file) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (requireSharing && !file['sharingEnabled']) file = null;
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
exports.addPlaylist = function(user_uid, new_playlist, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.removePlaylist = function(user_uid, playlistID, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.getUserPlaylists = function(user_uid, type) {
|
||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
||||
return user['playlists'][type];
|
||||
}
|
||||
|
||||
exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
|
||||
let playlist = null;
|
||||
if (!type) {
|
||||
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
|
||||
if (!playlist) {
|
||||
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
|
||||
if (playlist) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
exports.registerUserFile = function(user_uid, file_object, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
}
|
||||
|
||||
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
|
||||
let success = false;
|
||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
if (file_obj) {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
|
||||
// close descriptors
|
||||
if (config_api.descriptors[file_obj.id]) {
|
||||
try {
|
||||
for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) {
|
||||
config_api.descriptors[file_obj.id][i].destroy();
|
||||
}
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.remove({
|
||||
uid: file_uid
|
||||
}).write();
|
||||
if (fs.existsSync(full_path)) {
|
||||
// remove json and file
|
||||
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
|
||||
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
|
||||
let youtube_id = null;
|
||||
if (fs.existsSync(json_path)) {
|
||||
youtube_id = fs.readJSONSync(json_path).id;
|
||||
fs.unlinkSync(json_path);
|
||||
} else if (fs.existsSync(alternate_json_path)) {
|
||||
youtube_id = fs.readJSONSync(alternate_json_path).id;
|
||||
fs.unlinkSync(alternate_json_path);
|
||||
}
|
||||
|
||||
fs.unlinkSync(full_path);
|
||||
|
||||
// do archive stuff
|
||||
|
||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
|
||||
|
||||
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
||||
if (fs.existsSync(archive_path)) {
|
||||
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
||||
if (blacklistMode && line) {
|
||||
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
|
||||
// adds newline to the beginning of the line
|
||||
line = '\n' + line;
|
||||
fs.appendFileSync(blacklistPath, line);
|
||||
}
|
||||
} else {
|
||||
logger.info(`Could not find archive file for ${type} files. Creating...`);
|
||||
fs.ensureFileSync(archive_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
} else {
|
||||
success = false;
|
||||
logger.warn(`User file ${file_uid} does not exist!`);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
|
||||
let success = false;
|
||||
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
||||
if (user_db_obj.value()) {
|
||||
const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
|
||||
if (file_db_obj.value()) {
|
||||
success = true;
|
||||
file_db_obj.assign({sharingEnabled: enabled}).write();
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.userHasPermission = function(user_uid, permission) {
|
||||
const user_obj = users_db.get('users').find({uid: user_uid}).value();
|
||||
const role = user_obj['role'];
|
||||
if (!role) {
|
||||
// role doesn't exist
|
||||
logger.error('Invalid role ' + role);
|
||||
return false;
|
||||
}
|
||||
const role_permissions = (users_db.get('roles').value())['permissions'];
|
||||
|
||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||
|
||||
// check if user has a negative/positive override
|
||||
if (user_has_explicit_permission && permission_in_overrides) {
|
||||
// positive override
|
||||
return true;
|
||||
} else if (!user_has_explicit_permission && permission_in_overrides) {
|
||||
// negative override
|
||||
return false;
|
||||
}
|
||||
|
||||
// no overrides, let's check if the role has the permission
|
||||
if (role_permissions.includes(permission)) {
|
||||
return true;
|
||||
} else {
|
||||
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
exports.userPermissions = function(user_uid) {
|
||||
let user_permissions = [];
|
||||
const user_obj = users_db.get('users').find({uid: user_uid}).value();
|
||||
const role = user_obj['role'];
|
||||
if (!role) {
|
||||
// role doesn't exist
|
||||
logger.error('Invalid role ' + role);
|
||||
return null;
|
||||
}
|
||||
const role_permissions = users_db.get('roles').get(role).get('permissions').value()
|
||||
|
||||
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
||||
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
||||
|
||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||
|
||||
// check if user has a negative/positive override
|
||||
if (user_has_explicit_permission && permission_in_overrides) {
|
||||
// positive override
|
||||
user_permissions.push(permission);
|
||||
} else if (!user_has_explicit_permission && permission_in_overrides) {
|
||||
// negative override
|
||||
continue;
|
||||
}
|
||||
|
||||
// no overrides, let's check if the role has the permission
|
||||
if (role_permissions.includes(permission)) {
|
||||
user_permissions.push(permission);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return user_permissions;
|
||||
}
|
||||
|
||||
function getToken(queryParams) {
|
||||
if (queryParams && queryParams.jwt) {
|
||||
var parted = queryParams.jwt.split(' ');
|
||||
if (parted.length === 2) {
|
||||
return parted[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,33 @@
|
||||
const fs = require('fs');
|
||||
|
||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
let configPath = 'config/default.json';
|
||||
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
||||
|
||||
var logger = null;
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_logger) {
|
||||
setLogger(input_logger);
|
||||
ensureConfigFileExists();
|
||||
ensureConfigItemsExist();
|
||||
}
|
||||
|
||||
function ensureConfigItemsExist() {
|
||||
const config_keys = Object.keys(CONFIG_ITEMS);
|
||||
for (let i = 0; i < config_keys.length; i++) {
|
||||
const config_key = config_keys[i];
|
||||
getConfigItem(config_key);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureConfigFileExists() {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
logger.info('Cannot find config file. Creating one with default values...');
|
||||
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
|
||||
Object.byString = function(o, s) {
|
||||
@@ -31,16 +56,26 @@ function getElementNameInConfig(path) {
|
||||
return elements[elements.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if config exists. If not, write default config to config path
|
||||
*/
|
||||
function configExistsCheck() {
|
||||
let exists = fs.existsSync(configPath);
|
||||
if (!exists) {
|
||||
setConfigFile(DEFAULT_CONFIG);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets config file and returns as a json
|
||||
*/
|
||||
function getConfigFile() {
|
||||
let raw_data = fs.readFileSync(configPath);
|
||||
try {
|
||||
let raw_data = fs.readFileSync(configPath);
|
||||
let parsed_data = JSON.parse(raw_data);
|
||||
return parsed_data;
|
||||
} catch(e) {
|
||||
console.log('ERROR: Failed to get config file');
|
||||
logger.error('Failed to get config file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -56,8 +91,17 @@ function setConfigFile(config) {
|
||||
|
||||
function getConfigItem(key) {
|
||||
let config_json = getConfigFile();
|
||||
if (!CONFIG_ITEMS[key]) console.log('cannot find config with key ' + key);
|
||||
if (!CONFIG_ITEMS[key]) {
|
||||
logger.error(`Config item with key '${key}' is not recognized.`);
|
||||
return null;
|
||||
}
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
const val = Object.byString(config_json, path);
|
||||
if (val === undefined && Object.byString(DEFAULT_CONFIG, path)) {
|
||||
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
||||
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
||||
return Object.byString(DEFAULT_CONFIG, path);
|
||||
}
|
||||
return Object.byString(config_json, path);
|
||||
};
|
||||
|
||||
@@ -65,16 +109,23 @@ function setConfigItem(key, value) {
|
||||
let success = false;
|
||||
let config_json = getConfigFile();
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
let parent_path = getParentPath(path);
|
||||
let element_name = getElementNameInConfig(path);
|
||||
|
||||
let parent_path = getParentPath(path);
|
||||
let parent_object = Object.byString(config_json, parent_path);
|
||||
if (!parent_object) {
|
||||
let parent_parent_path = getParentPath(parent_path);
|
||||
let parent_parent_object = Object.byString(config_json, parent_parent_path);
|
||||
let parent_path_arr = parent_path.split('.');
|
||||
let parent_parent_single_key = parent_path_arr[parent_path_arr.length-1];
|
||||
parent_parent_object[parent_parent_single_key] = {};
|
||||
parent_object = Object.byString(config_json, parent_path);
|
||||
}
|
||||
|
||||
if (value === 'false' || value === 'true') {
|
||||
parent_object[element_name] = (value === 'true');
|
||||
} else {
|
||||
parent_object[element_name] = value;
|
||||
}
|
||||
|
||||
success = setConfigFile(config_json);
|
||||
|
||||
return success;
|
||||
@@ -95,7 +146,7 @@ function setConfigItems(items) {
|
||||
let item_path = CONFIG_ITEMS[key]['path'];
|
||||
let item_parent_path = getParentPath(item_path);
|
||||
let item_element_name = getElementNameInConfig(item_path);
|
||||
|
||||
|
||||
let item_parent_object = Object.byString(config_json, item_parent_path);
|
||||
item_parent_object[item_element_name] = value;
|
||||
}
|
||||
@@ -104,10 +155,80 @@ function setConfigItems(items) {
|
||||
return success;
|
||||
}
|
||||
|
||||
function globalArgsRequiresSafeDownload() {
|
||||
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
|
||||
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||
return failedArgs && failedArgs.length > 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfigItem: getConfigItem,
|
||||
setConfigItem: setConfigItem,
|
||||
setConfigItems: setConfigItems,
|
||||
getConfigFile: getConfigFile,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS
|
||||
}
|
||||
setConfigFile: setConfigFile,
|
||||
configExistsCheck: configExistsCheck,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
initialize: initialize,
|
||||
descriptors: {},
|
||||
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
var config = require('config');
|
||||
|
||||
let CONFIG_ITEMS = {
|
||||
// Host
|
||||
'ytdl_url': {
|
||||
@@ -34,6 +32,18 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_video_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
||||
},
|
||||
'ytdl_use_youtubedl_archive': {
|
||||
'key': 'ytdl_use_youtubedl_archive',
|
||||
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
|
||||
},
|
||||
'ytdl_custom_args': {
|
||||
'key': 'ytdl_custom_args',
|
||||
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
||||
},
|
||||
'ytdl_safe_download_override': {
|
||||
'key': 'ytdl_safe_download_override',
|
||||
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
@@ -56,9 +66,24 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_allow_multi_download_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||
},
|
||||
|
||||
'ytdl_settings_pin_required': {
|
||||
'key': 'ytdl_settings_pin_required',
|
||||
'path': 'YoutubeDLMaterial.Extra.settings_pin_required'
|
||||
},
|
||||
'ytdl_enable_downloads_manager': {
|
||||
'key': 'ytdl_enable_downloads_manager',
|
||||
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
|
||||
},
|
||||
|
||||
// API
|
||||
'ytdl_use_api_key': {
|
||||
'key': 'ytdl_use_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.use_API_key'
|
||||
},
|
||||
'ytdl_api_key': {
|
||||
'key': 'ytdl_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.API_key'
|
||||
},
|
||||
'ytdl_use_youtube_api': {
|
||||
'key': 'ytdl_use_youtube_api',
|
||||
'path': 'YoutubeDLMaterial.API.use_youtube_API'
|
||||
@@ -78,6 +103,38 @@ let CONFIG_ITEMS = {
|
||||
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
|
||||
},
|
||||
|
||||
// Subscriptions
|
||||
'ytdl_allow_subscriptions': {
|
||||
'key': 'ytdl_allow_subscriptions',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.allow_subscriptions'
|
||||
},
|
||||
'ytdl_subscriptions_base_path': {
|
||||
'key': 'ytdl_subscriptions_base_path',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_base_path'
|
||||
},
|
||||
'ytdl_subscriptions_check_interval': {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_check_interval': {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_use_youtubedl_archive': {
|
||||
'key': 'ytdl_use_youtubedl_archive',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
|
||||
},
|
||||
|
||||
// Users
|
||||
'ytdl_users_base_path': {
|
||||
'key': 'ytdl_users_base_path',
|
||||
'path': 'YoutubeDLMaterial.Users.base_path'
|
||||
},
|
||||
'ytdl_allow_registration': {
|
||||
'key': 'ytdl_allow_registration',
|
||||
'path': 'YoutubeDLMaterial.Users.allow_registration'
|
||||
},
|
||||
|
||||
// Advanced
|
||||
'ytdl_use_default_downloading_agent': {
|
||||
'key': 'ytdl_use_default_downloading_agent',
|
||||
@@ -87,10 +144,35 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_custom_downloading_agent',
|
||||
'path': 'YoutubeDLMaterial.Advanced.custom_downloading_agent'
|
||||
},
|
||||
'ytdl_multi_user_mode': {
|
||||
'key': 'ytdl_multi_user_mode',
|
||||
'path': 'YoutubeDLMaterial.Advanced.multi_user_mode'
|
||||
},
|
||||
'ytdl_allow_advanced_download': {
|
||||
'key': 'ytdl_allow_advanced_download',
|
||||
'path': 'YoutubeDLMaterial.Advanced.allow_advanced_download'
|
||||
},
|
||||
'ytdl_use_cookies': {
|
||||
'key': 'ytdl_use_cookies',
|
||||
'path': 'YoutubeDLMaterial.Advanced.use_cookies'
|
||||
},
|
||||
'ytdl_logger_level': {
|
||||
'key': 'ytdl_logger_level',
|
||||
'path': 'YoutubeDLMaterial.Advanced.logger_level'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.CONFIG_ITEMS = CONFIG_ITEMS;
|
||||
AVAILABLE_PERMISSIONS = [
|
||||
'filemanager',
|
||||
'settings',
|
||||
'subscriptions',
|
||||
'sharing',
|
||||
'advanced_download',
|
||||
'downloads_manager'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||
CURRENT_VERSION: 'v4.0'
|
||||
}
|
||||
|
||||
119
backend/db.js
Normal file
119
backend/db.js
Normal file
@@ -0,0 +1,119 @@
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
const { uuid } = require('uuidv4');
|
||||
const config_api = require('./config');
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db }
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_db, input_users_db, input_logger) {
|
||||
setDB(input_db, input_users_db);
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
const file_id = file_path.substring(0, file_path.length-4);
|
||||
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// 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);
|
||||
|
||||
if (!sub) {
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
} else {
|
||||
// remove existing video if overwriting
|
||||
db.get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
db.get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
}
|
||||
} else {
|
||||
sub_db = null;
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
} else {
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
sub_db.get('videos').push(file_object).write();
|
||||
}
|
||||
|
||||
return file_object['uid'];
|
||||
}
|
||||
|
||||
function generateFileObject(id, type, customPath = null, sub = null) {
|
||||
if (!customPath && sub) {
|
||||
customPath = getAppendedBasePathSub(sub, config_api.getConfigItem('ytdl_subscriptions_base_path'));
|
||||
}
|
||||
var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
|
||||
if (!jsonobj) {
|
||||
return null;
|
||||
}
|
||||
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
||||
const file_path = utils.getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
|
||||
// console.
|
||||
var stats = fs.statSync(path.join(__dirname, file_path));
|
||||
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
function updatePlaylist(playlist, user_uid) {
|
||||
let playlistID = playlist.id;
|
||||
let type = playlist.type;
|
||||
let db_loc = null;
|
||||
if (user_uid) {
|
||||
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID});
|
||||
} else {
|
||||
db_loc = db.get(`playlists.${type}`).find({id: playlistID});
|
||||
}
|
||||
db_loc.assign(playlist).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
function getAppendedBasePathSub(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize: initialize,
|
||||
registerFileDB: registerFileDB,
|
||||
updatePlaylist: updatePlaylist
|
||||
}
|
||||
15
backend/docker-compose.yml
Normal file
15
backend/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: "2"
|
||||
services:
|
||||
ytdl_material:
|
||||
environment:
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
restart: always
|
||||
volumes:
|
||||
- ./appdata:/app/appdata
|
||||
- ./audio:/app/audio
|
||||
- ./video:/app/video
|
||||
- ./subscriptions:/app/subscriptions
|
||||
- ./users:/app/users
|
||||
ports:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:latest
|
||||
17
backend/entrypoint.sh
Executable file
17
backend/entrypoint.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CMD="node app.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
|
||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
||||
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' +
|
||||
exec su-exec "$UID:$GID" "$0" "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
2005
backend/package-lock.json
generated
2005
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,17 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app.js"
|
||||
"start": "nodemon -q app.js"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"*.js",
|
||||
"appdata/*",
|
||||
"public/*"
|
||||
],
|
||||
"watch": [
|
||||
"restart.json"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,12 +30,31 @@
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"bcrypt": "^4.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
"express": "^4.17.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lowdb": "^1.0.0",
|
||||
"md5": "^2.2.1",
|
||||
"merge-files": "^0.1.2",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-id3": "^0.1.14",
|
||||
"nodemon": "^2.0.2",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"progress": "^2.0.3",
|
||||
"read-last-lines": "^1.7.2",
|
||||
"shortid": "^2.2.15",
|
||||
"unzipper": "^0.10.10",
|
||||
"uuidv4": "^6.0.6",
|
||||
"winston": "^3.2.1",
|
||||
"youtube-dl": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
1
backend/public/1-es2015.6bc1f7cd24dfb6add92c.js
Normal file
1
backend/public/1-es2015.6bc1f7cd24dfb6add92c.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/1-es5.6bc1f7cd24dfb6add92c.js
Normal file
1
backend/public/1-es5.6bc1f7cd24dfb6add92c.js
Normal file
File diff suppressed because one or more lines are too long
794
backend/public/3rdpartylicenses.txt
Normal file
794
backend/public/3rdpartylicenses.txt
Normal file
@@ -0,0 +1,794 @@
|
||||
@angular-devkit/build-angular
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017 Google, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
@angular/animations
|
||||
MIT
|
||||
|
||||
@angular/cdk
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@angular/common
|
||||
MIT
|
||||
|
||||
@angular/compiler
|
||||
MIT
|
||||
|
||||
@angular/core
|
||||
MIT
|
||||
|
||||
@angular/forms
|
||||
MIT
|
||||
|
||||
@angular/localize
|
||||
MIT
|
||||
|
||||
@angular/material
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@angular/platform-browser
|
||||
MIT
|
||||
|
||||
@angular/platform-browser-dynamic
|
||||
MIT
|
||||
|
||||
@angular/router
|
||||
MIT
|
||||
|
||||
core-js
|
||||
MIT
|
||||
Copyright (c) 2014-2020 Denis Pushkarev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
file-saver
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright © 2016 [Eli Grey][1].
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
[1]: http://eligrey.com
|
||||
|
||||
|
||||
filesize
|
||||
BSD-3-Clause
|
||||
Copyright (c) 2020, Jason Mulligan
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of filesize nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
|
||||
fingerprintjs2
|
||||
MIT
|
||||
Fingerprintjs2 – Modern & flexible browser fingerprint library v2
|
||||
https://github.com/Valve/fingerprintjs2
|
||||
Copyright (c) 2018– Jonas Haag (jonas@lophus.org)
|
||||
Copyright (c) 2015– Valentin Vasilyev (valentin.vasilyev@outlook.com)
|
||||
Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
ng-lazyload-image
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Oskar Karlsson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
ngx-file-drop
|
||||
MIT
|
||||
|
||||
ngx-videogular
|
||||
MIT
|
||||
|
||||
regenerator-runtime
|
||||
MIT
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
rxjs
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
rxjs-compat
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
tslib
|
||||
Apache-2.0
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
uuid
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
web-animations-js
|
||||
Apache-2.0
|
||||
|
||||
webpack
|
||||
MIT
|
||||
Copyright JS Foundation and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
zone.js
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2020 Google LLC. http://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
198
backend/public/assets/i18n/messages.de.json
Normal file
198
backend/public/assets/i18n/messages.de.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Youtube-dl Argumente ändern",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulierte neue Argumente",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Argument hinzufügen",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Nach Kategorie filtern",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Argument-Wert verwenden",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Argument-Wert",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Argument hinzufügen",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Abbrechen",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Ändern",
|
||||
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "YouTube Downloader",
|
||||
"6d2ec8898344c8955542b0542c942038ef28bb80": "Bitte geben Sie eine gültige URL ein.",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "Qualität",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL verwenden",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Ansehen",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Nur Audio",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-Download Modus",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Download",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Abbrechen",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Erweitert",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulierter Befehl:",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Benutzerdefinierte Argumente verwenden",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Benutzerdefinierte Argumente",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Die URL muss nicht angegeben werden, sondern nur der Teil danach. Argumente werden mit zwei Kommata getrennt: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Benutzerdefinierte Ausgabe verwenden",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Benutzerdefinierte Ausgabe",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentation",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Der Pfad ist relativ zum Konfigurations-Download-Pfad. Dateiendung auslassen.",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Authentifizierung verwenden",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Benutzername",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
|
||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
|
||||
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
|
||||
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
|
||||
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
|
||||
"0f59c46ca29e9725898093c9ea6b586730d0624e": "Keine Playlisten verfügbar. Erstellen Sie eine aus heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Name:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Kanal:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Dateigröße:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Pfad:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Hochgeladen am:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Schließen",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Anzahl:",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Info",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Löschen",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Löschen und zur Blacklist hinzufügen",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Einstellungen",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL, über die auf diese Applikation zugegriffen wird, ohne Port.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Der gewünschte Port. Standard ist 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Multi-User Modus",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Benutzer Basispfad",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Basispfad für Benutzer und deren heruntergeladene Videos.",
|
||||
"cbe16a57be414e84b6a68309d08fad894df797d6": "Verschlüsselung verwenden",
|
||||
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Dateipfad zum Zertifikat",
|
||||
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Dateipfad zum Zertifikatsschlüssel",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Abonnements erlauben",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnements Basispfad",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Basispfad für Videos von abonnierten Kanälen und Wiedergabelisten. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Prüfintervall",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "Einheit ist Sekunden, nur Zahlen sind erlaubt.",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Youtube-DL Archiv verwenden",
|
||||
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Mit der Archivfunktion",
|
||||
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "werden Informationen über Videos, welche durch ein Abonnement heruntergeladen wurden, in einem Textdokument festgehalten. Diese befinden sich in dem Archiv Unterverzeichnis vom Abonnementsordner.",
|
||||
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Dadurch können Videos permanent gelöscht werden, ohne das Abonnement beenden zu müssen. Außerdem kann dadurch aufgezeichnet werden, welche Videos heruntergeladen wurden. Z. B. im Falle eines Datenverlusts.",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Design",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Standard",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Dunkel",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Designänderung erlauben",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Sprache",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Allgemein",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Audio Basispfad",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Dateipfad für Audio-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Qualitätsauswahl erlauben",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Nur Download Modus",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Multi-Download Modus erlauben",
|
||||
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Einstellungen durch PIN schützen",
|
||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Neuen PIN festlegen",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Öffentliche API aktivieren",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Öffentlicher API-Schlüssel",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Dokumentation ansehen",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generieren",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "YouTube API verwenden",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Youtube API-Schlüssel",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Schlüsselgeneration ist einfach!",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Hier klicken",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "um die offizielle YoutubeDL-Material Chrome-Erweiterung manuell herunterzuladen.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Die Erweiterung muss manuell installiert werden und in den Einstellungen der Erweiterung muss die Frontend-URL eingetragen werden.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "um die offizielle YoutubeDL-Material Firefox-Erweiterung direkt aus dem Firefox-Addon-Store zu installieren.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Detaillierte Anleitung.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Die Frontend-URL muss in den Einstellungen der Erweiterung eingetragen werden.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Der untenstehende Link muss nur in die Lesezeichenleiste gezogen werden. Auf einer unterstützten Webseite können Sie danach einfach auf das Lesezeichen klicken, um das Video herunterzuladen.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "'Nur Audio' Lesezeichen generieren",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Standard Download-Agent verwenden",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Downloader auswählen",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Erweiterte Download-Optionen aktivieren",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Erweitert",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Benutzerregistrierung zulassen",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Benutzer",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Sie sind nicht angemeldet.",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Anmelden",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Ausloggen",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Admin-Konto erstellen",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Es wurde kein Standard-Administratorkonto erkannt. Ein Administratorkonto mit dem Benutzernamen \"admin\" wird erstellt und ein Passwort wird festgelegt.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Erstellen",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Zeitstempel verwenden",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Sekunden",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "In die Zwischenablage kopieren",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Änderungen speichern",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Details",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Ein Fehler ist aufgetreten:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
|
||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Videos herunterladen aus den letzten",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Nur Streaming Modus",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonnieren",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Typ:",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archiv:",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Archiv exportieren",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Deabonnieren",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Ihre Abonnements",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
|
||||
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nein",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Rolle verwalten",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Benutzername",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten"
|
||||
}
|
||||
200
backend/public/assets/i18n/messages.es.json
Normal file
200
backend/public/assets/i18n/messages.es.json
Normal file
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"ccc7e92cbdd35e901acf9ad80941abee07bd8f60": "No es necesario incluir URL, solo todo después ",
|
||||
"f41145afc02fd47ef0576ac79acd2c47ebbf4901": "Argumentos personalizados globales para descargas en la página de inicio.",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una lista de reproducción",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nombre",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Archivos de sonido",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Archivos de video",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modificar args de youtube-dl",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Args nuevos simulados",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Añadir un arg",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Busqueda por categoria",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usar valor de arg",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor de arg",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Añadir arg",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancelar",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modificar",
|
||||
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Descargador de Youtube",
|
||||
"6d2ec8898344c8955542b0542c942038ef28bb80": "Por favor entre una URL válida",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": " Calidad ",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Usa URL",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": " Ver ",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Solo audio",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Descarga múltiple",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Descarga",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancelar",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avanzado",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Commando simulado:",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Usar argumentos personalizados",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Argumentos personalizados",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usar salida personalizada",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Salida personalizada",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentación",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "La ruta es relativa a la ruta de descarga de la config. No incluya el extensión.",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Usa autenticación",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nombre de usuario",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Contraseña",
|
||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Tus archivos de audio están aquí",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Listas de reproducción",
|
||||
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más.",
|
||||
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídeo",
|
||||
"960582a8b9d7942716866ecfb7718309728f2916": "Tus archivos de video son aquí",
|
||||
"0f59c46ca29e9725898093c9ea6b586730d0624e": "No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más.",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nombre:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Cargador:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Tamaño del archivo:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Ruta:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Subido:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Cerca",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Cuenta:",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Información",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Eliminar",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Eliminar y pones en la lista negra",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Configuraciones",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL desde la que se accederá a esta aplicación, sin el puerto.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Puerto",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Puerto deseado. El valor predeterminado es 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Modo multiusuario",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Ruta base de usuarios",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Ruta base para los usuarios y sus videos descargados.",
|
||||
"cbe16a57be414e84b6a68309d08fad894df797d6": "Usa cifrado",
|
||||
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Ruta del archivo de certificado",
|
||||
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Ruta de archivo de clave",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Permitir suscripciones",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Ruta base de suscripciones",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervalo de comprobación",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "La unidad es segundos, solo incluye números.",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Usa el archivo de youtube-dl",
|
||||
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Con la función de archivo de youtube-dl,",
|
||||
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones.",
|
||||
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos.",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Defecto",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Oscura",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permitir cambio de tema",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Ruta de la carpeta de audio",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Ruta de la carpeta de video",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material.",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,,",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Descargador",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Título superior",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Administrador de archivos habilitado",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Administrador de descargas habilitado",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permitir selección de calidad",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modo de solo descarga",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permitir el modo de descarga múltiple",
|
||||
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Requiere pin para la configuración",
|
||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Establecer nuevo pin",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilitar API pública",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clave API pública",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Ver documentación",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generar",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utilizar la API de YouTube",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Clave API de YouTube",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "¡Generar una clave es fácil!",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "¡Haga clic aquí",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instrucciones detalladas de configuración.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Generar bookmarklet solo de audio",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleccione un descargador",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avanzado",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permitir registro de usuario",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuarios",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Puede actualizar desde el menú de configuración.",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Tu perfil",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creado:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Usted no se ha identificado.",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identificarse",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Salir",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crear cuenta de administrador",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Crear",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Suscripciones",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Descargas",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Compartir lista de reproducción",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Compartir vídeo",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Compartir audio",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Habilitar compartir",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usar marca de tiempo",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segundos",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copiar al Portapapeles",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalles",
|
||||
"383000ab16bf415d5a1d61d7eb7b5959c72a9515": "Se ha producido un error:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Inicio de descarga:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin de descarga:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Ruta(s) del archivo:",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Suscríbase a la lista de reproducción o al canal",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "La lista de reproducción o la URL del canal",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nombre personalizado",
|
||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Esto es opcional",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Descargar todas las cargas",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Descargar videos subidos en el último",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Modo de solo transmisión",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Subscribe",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archivo:",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportar el archivo",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Darse de baja",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Sus suscripciones",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canales",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nombre no disponible. Recuperación de canales en progreso.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "No tienes suscripciones de canal.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Nombre no disponible. Recuperación de listas de reproducción en progreso.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "No tienes suscripciones a listas de reproducción.",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Buscar",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Duración:",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Eliminar y volver a descargar",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Borrar para siempre",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Seleccione una versión:",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrarse",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de sesión:",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "¡No hay descargas disponibles!",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrar un usuario",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nombre de usuario",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Administrar usuario",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID de usuario:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nueva contraseña",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Establecer nueva contraseña",
|
||||
"40da072004086c9ec00d125165da91eaade7f541": "Uso por defecto",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Si",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestionar rol",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": " Nombre de usuario ",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
|
||||
}
|
||||
BIN
backend/public/assets/images/GitHub-64px.png
Normal file
BIN
backend/public/assets/images/GitHub-64px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
backend/public/assets/images/logo_128px.png
Normal file
BIN
backend/public/assets/images/logo_128px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
backend/public/assets/images/logo_64px.png
Normal file
BIN
backend/public/assets/images/logo_64px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
backend/public/favicon.ico
Normal file
BIN
backend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
18
backend/public/index.html
Normal file
18
backend/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>YoutubeDLMaterial</title>
|
||||
<base href="./">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<script src="runtime-es2015.1f02852de81190376ba1.js" type="module"></script><script src="runtime-es5.1f02852de81190376ba1.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.0cbc545a4a3bee376826.js" type="module"></script><script src="main-es5.0cbc545a4a3bee376826.js" nomodule defer></script></body>
|
||||
</html>
|
||||
1
backend/public/main-es2015.0cbc545a4a3bee376826.js
Normal file
1
backend/public/main-es2015.0cbc545a4a3bee376826.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/main-es5.0cbc545a4a3bee376826.js
Normal file
1
backend/public/main-es5.0cbc545a4a3bee376826.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/polyfills-es2015.5b408f108bcea938a7e2.js
Normal file
1
backend/public/polyfills-es2015.5b408f108bcea938a7e2.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/polyfills-es5.7f923c8f5afda210edd3.js
Normal file
1
backend/public/polyfills-es5.7f923c8f5afda210edd3.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/runtime-es2015.1f02852de81190376ba1.js
Normal file
1
backend/public/runtime-es2015.1f02852de81190376ba1.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es2015."+{1:"6bc1f7cd24dfb6add92c"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);
|
||||
1
backend/public/runtime-es5.1f02852de81190376ba1.js
Normal file
1
backend/public/runtime-es5.1f02852de81190376ba1.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es5."+{1:"6bc1f7cd24dfb6add92c"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);
|
||||
3
backend/public/styles.5112d6db78cf21541598.css
Normal file
3
backend/public/styles.5112d6db78cf21541598.css
Normal file
File diff suppressed because one or more lines are too long
486
backend/subscriptions.js
Normal file
486
backend/subscriptions.js
Normal file
@@ -0,0 +1,486 @@
|
||||
const FileSync = require('lowdb/adapters/FileSync')
|
||||
|
||||
var fs = require('fs-extra');
|
||||
const { uuid } = require('uuidv4');
|
||||
var path = require('path');
|
||||
|
||||
var youtubedl = require('youtube-dl');
|
||||
const config_api = require('./config');
|
||||
var utils = require('./utils')
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
var db_api = null;
|
||||
|
||||
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_db, input_users_db, input_logger, input_db_api) {
|
||||
setDB(input_db, input_users_db, input_db_api);
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
async function subscribe(sub, user_uid = null) {
|
||||
const result_obj = {
|
||||
success: false,
|
||||
error: ''
|
||||
};
|
||||
return new Promise(async resolve => {
|
||||
// sub should just have url and name. here we will get isPlaylist and path
|
||||
sub.isPlaylist = sub.url.includes('playlist');
|
||||
sub.videos = [];
|
||||
|
||||
let url_exists = false;
|
||||
|
||||
if (user_uid)
|
||||
url_exists = !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({url: sub.url}).value()
|
||||
else
|
||||
url_exists = !!db.get('subscriptions').find({url: sub.url}).value();
|
||||
|
||||
if (url_exists) {
|
||||
logger.info('Sub already exists');
|
||||
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
|
||||
resolve(result_obj);
|
||||
return;
|
||||
}
|
||||
|
||||
// add sub to db
|
||||
let sub_db = null;
|
||||
if (user_uid) {
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
} else {
|
||||
db.get('subscriptions').push(sub).write();
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
let success = await getSubscriptionInfo(sub, user_uid);
|
||||
|
||||
if (success) {
|
||||
sub = sub_db.value();
|
||||
getVideosForSub(sub, user_uid);
|
||||
} else {
|
||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||
};
|
||||
|
||||
result_obj.success = success;
|
||||
result_obj.sub = sub;
|
||||
resolve(result_obj);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
return new Promise(resolve => {
|
||||
// get videos
|
||||
let downloadConfig = ['--dump-json', '--playlist-end', '1']
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
||||
}
|
||||
if (err) {
|
||||
logger.error(err.stderr);
|
||||
resolve(false);
|
||||
} else if (output) {
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
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) {
|
||||
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
|
||||
// if it's now valid, update
|
||||
if (sub.name) {
|
||||
if (user_uid)
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
|
||||
else
|
||||
db.get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
|
||||
}
|
||||
}
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
if (useArchive && !sub.archive) {
|
||||
// must create the archive
|
||||
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
|
||||
const archive_path = path.join(archive_dir, 'archive.txt');
|
||||
|
||||
// creates archive directory and text file if it doesn't exist
|
||||
fs.ensureDirSync(archive_dir);
|
||||
fs.ensureFileSync(archive_path);
|
||||
|
||||
// updates subscription
|
||||
sub.archive = archive_dir;
|
||||
if (user_uid)
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
|
||||
else
|
||||
db.get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
|
||||
}
|
||||
|
||||
// TODO: get even more info
|
||||
|
||||
resolve(true);
|
||||
}
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||
return new Promise(async resolve => {
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
let result_obj = { success: false, error: '' };
|
||||
|
||||
let id = sub.id;
|
||||
if (user_uid)
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
|
||||
else
|
||||
db.get('subscriptions').remove({id: id}).write();
|
||||
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
if (deleteMode && fs.existsSync(appendedBasePath)) {
|
||||
if (sub.archive && fs.existsSync(sub.archive)) {
|
||||
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
||||
// deletes archive if it exists
|
||||
if (fs.existsSync(archive_file_path)) {
|
||||
fs.unlinkSync(archive_file_path);
|
||||
}
|
||||
fs.rmdirSync(sub.archive);
|
||||
}
|
||||
deleteFolderRecursive(appendedBasePath);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
||||
let basePath = null;
|
||||
let sub_db = null;
|
||||
if (user_uid) {
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
} else {
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
const name = file;
|
||||
let retrievedID = null;
|
||||
sub_db.get('videos').remove({uid: file_uid}).write();
|
||||
return new Promise(resolve => {
|
||||
let filePath = appendedBasePath;
|
||||
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
||||
var videoFilePath = path.join(__dirname,filePath,name+ext);
|
||||
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||
|
||||
jsonExists = fs.existsSync(jsonPath);
|
||||
videoFileExists = fs.existsSync(videoFilePath);
|
||||
imageFileExists = fs.existsSync(imageFilePath);
|
||||
altImageFileExists = fs.existsSync(altImageFilePath);
|
||||
|
||||
if (jsonExists) {
|
||||
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
|
||||
fs.unlinkSync(jsonPath);
|
||||
}
|
||||
|
||||
if (imageFileExists) {
|
||||
fs.unlinkSync(imageFilePath);
|
||||
}
|
||||
|
||||
if (altImageFileExists) {
|
||||
fs.unlinkSync(altImageFilePath);
|
||||
}
|
||||
|
||||
if (videoFileExists) {
|
||||
fs.unlink(videoFilePath, function(err) {
|
||||
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
||||
const archive_path = path.join(sub.archive, 'archive.txt')
|
||||
// if archive exists, remove line with video ID
|
||||
if (fs.existsSync(archive_path)) {
|
||||
removeIDFromArchive(archive_path, retrievedID);
|
||||
}
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function getVideosForSub(sub, user_uid = null) {
|
||||
return new Promise(resolve => {
|
||||
if (!subExists(sub.id, user_uid)) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// get sub_db
|
||||
let sub_db = null;
|
||||
if (user_uid)
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
else
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
|
||||
// get basePath
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
|
||||
let appendedBasePath = null
|
||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
|
||||
let multiUserMode = null;
|
||||
if (user_uid) {
|
||||
multiUserMode = {
|
||||
user: user_uid,
|
||||
file_path: appendedBasePath
|
||||
}
|
||||
}
|
||||
|
||||
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||
|
||||
let fullOutput = appendedBasePath + '/%(title)s' + ext;
|
||||
if (sub.custom_output) {
|
||||
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
|
||||
}
|
||||
|
||||
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
|
||||
|
||||
let qualityPath = null;
|
||||
if (sub.type && sub.type === 'audio') {
|
||||
qualityPath = ['-f', 'bestaudio']
|
||||
qualityPath.push('-x');
|
||||
qualityPath.push('--audio-format', 'mp3');
|
||||
} else {
|
||||
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
|
||||
}
|
||||
|
||||
downloadConfig.push(...qualityPath)
|
||||
|
||||
if (sub.custom_args) {
|
||||
customArgsArray = sub.custom_args.split(',,');
|
||||
if (customArgsArray.indexOf('-f') !== -1) {
|
||||
// if custom args has a custom quality, replce the original quality with that of custom args
|
||||
const original_output_index = downloadConfig.indexOf('-f');
|
||||
downloadConfig.splice(original_output_index, 2);
|
||||
}
|
||||
downloadConfig.push(...customArgsArray);
|
||||
}
|
||||
|
||||
let archive_dir = null;
|
||||
let archive_path = null;
|
||||
|
||||
if (useArchive) {
|
||||
if (sub.archive) {
|
||||
archive_dir = sub.archive;
|
||||
archive_path = path.join(archive_dir, 'archive.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) {
|
||||
downloadConfig.push('--dateafter', sub.timerange);
|
||||
}
|
||||
|
||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||
if (useCookies) {
|
||||
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||
} else {
|
||||
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||
}
|
||||
}
|
||||
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr);
|
||||
if (err.stderr.includes('This video is unavailable')) {
|
||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||
try {
|
||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
for (let i = 0; i < outputs.length; i++) {
|
||||
const output = JSON.parse(outputs[i]);
|
||||
handleOutputJSON(sub, sub_db, 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
|
||||
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 (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
logger.verbose('No additional videos to download for ' + sub.name);
|
||||
resolve(true);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const reset_videos = i === 0;
|
||||
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
|
||||
|
||||
// TODO: Potentially store downloaded files in db?
|
||||
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
|
||||
if (sub.streamingOnly) {
|
||||
if (reset_videos) {
|
||||
sub_db.assign({videos: []}).write();
|
||||
}
|
||||
|
||||
// remove unnecessary info
|
||||
output_json.formats = null;
|
||||
|
||||
// add to db
|
||||
sub_db.get('videos').push(output_json).write();
|
||||
} else {
|
||||
// TODO: make multiUserMode obj
|
||||
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
|
||||
}
|
||||
}
|
||||
|
||||
function getAllSubscriptions(user_uid = null) {
|
||||
if (user_uid)
|
||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').value();
|
||||
else
|
||||
return db.get('subscriptions').value();
|
||||
}
|
||||
|
||||
function getSubscription(subID, user_uid = null) {
|
||||
if (user_uid)
|
||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
||||
else
|
||||
return db.get('subscriptions').find({id: subID}).value();
|
||||
}
|
||||
|
||||
function subExists(subID, user_uid = null) {
|
||||
if (user_uid)
|
||||
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
||||
else
|
||||
return !!db.get('subscriptions').find({id: subID}).value();
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
function getAppendedBasePath(sub, base_path) {
|
||||
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/32197381/8088021
|
||||
const deleteFolderRecursive = function(folder_to_delete) {
|
||||
if (fs.existsSync(folder_to_delete)) {
|
||||
fs.readdirSync(folder_to_delete).forEach((file, index) => {
|
||||
const curPath = path.join(folder_to_delete, file);
|
||||
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||
deleteFolderRecursive(curPath);
|
||||
} else { // delete file
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(folder_to_delete);
|
||||
}
|
||||
};
|
||||
|
||||
function removeIDFromArchive(archive_path, id) {
|
||||
let data = fs.readFileSync(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');
|
||||
fs.writeFileSync(archive_path, updatedData);
|
||||
if (line) return line;
|
||||
if (err) throw err;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
subscribe : subscribe,
|
||||
unsubscribe : unsubscribe,
|
||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||
getVideosForSub : getVideosForSub,
|
||||
removeIDFromArchive : removeIDFromArchive,
|
||||
setLogger : setLogger,
|
||||
initialize : initialize
|
||||
}
|
||||
4
backend/subscriptions/channels/.gitignore
vendored
Normal file
4
backend/subscriptions/channels/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
4
backend/subscriptions/playlists/.gitignore
vendored
Normal file
4
backend/subscriptions/playlists/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
97
backend/utils.js
Normal file
97
backend/utils.js
Normal file
@@ -0,0 +1,97 @@
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
const config_api = require('./config');
|
||||
|
||||
const is_windows = process.platform === 'win32';
|
||||
|
||||
function getTrueFileName(unfixed_path, type) {
|
||||
let fixed_path = unfixed_path;
|
||||
|
||||
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||
let unfixed_parts = unfixed_path.split('.');
|
||||
const old_ext = unfixed_parts[unfixed_parts.length-1];
|
||||
|
||||
|
||||
if (old_ext !== new_ext) {
|
||||
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
||||
fixed_path = unfixed_parts.join('.');
|
||||
}
|
||||
return fixed_path;
|
||||
}
|
||||
|
||||
function getJSONMp4(name, customPath, openReadPerms = false) {
|
||||
var obj = null; // output
|
||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
var jsonPath = path.join(customPath, name + ".info.json");
|
||||
var alternateJsonPath = path.join(customPath, name + ".mp4.info.json");
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
} else if (fs.existsSync(alternateJsonPath)) {
|
||||
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||
}
|
||||
else obj = 0;
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getJSONMp3(name, customPath, openReadPerms = false) {
|
||||
var obj = null;
|
||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
var jsonPath = path.join(customPath, name + ".info.json");
|
||||
var alternateJsonPath = path.join(customPath, name + ".mp3.info.json");
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
}
|
||||
else if (fs.existsSync(alternateJsonPath)) {
|
||||
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||
}
|
||||
else
|
||||
obj = 0;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
if (is_windows) return;
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
|
||||
const files_to_fix = [
|
||||
// JSONs
|
||||
path.join(customPath, name + '.info.json'),
|
||||
path.join(customPath, name + ext + '.info.json'),
|
||||
// Thumbnails
|
||||
path.join(customPath, name + '.webp'),
|
||||
path.join(customPath, name + '.jpg')
|
||||
];
|
||||
|
||||
for (const file of files_to_fix) {
|
||||
if (!fs.existsSync(file)) continue;
|
||||
fs.chmodSync(file, 0o644);
|
||||
}
|
||||
}
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
this.isAudio = isAudio;
|
||||
this.duration = duration;
|
||||
this.url = url;
|
||||
this.uploader = uploader;
|
||||
this.size = size;
|
||||
this.path = path;
|
||||
this.upload_date = upload_date;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getJSONMp3: getJSONMp3,
|
||||
getJSONMp4: getJSONMp4,
|
||||
getTrueFileName: getTrueFileName,
|
||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||
File: File
|
||||
}
|
||||
4
backend/video/.gitignore
vendored
Normal file
4
backend/video/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
Binary file not shown.
Binary file not shown.
7
chrome-extension/css/bootstrap.min.css
vendored
Normal file
7
chrome-extension/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
chrome-extension/js/bootstrap.min.js
vendored
Normal file
7
chrome-extension/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
chrome-extension/js/jquery-3.4.1.min.js
vendored
Normal file
2
chrome-extension/js/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
chrome-extension/js/popper.min.js
vendored
Normal file
5
chrome-extension/js/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "YoutubeDL-Material",
|
||||
"version": "0.1",
|
||||
"description": "The official chrome extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||
"version": "0.3",
|
||||
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
@@ -16,5 +16,11 @@
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": false
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "IsaacMGrynsztein@gmail.com",
|
||||
"strict_min_version": "57.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>YoutubeDL-Material Extension Options</title></head>
|
||||
<head>
|
||||
<title>YoutubeDL-Material Extension Options</title>
|
||||
<!-- Scripts -->
|
||||
<script src="js/jquery-3.4.1.min.js"></script>
|
||||
<script src="js/popper.min.js"></script>
|
||||
<script src="js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Cascading Style Sheets -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Settings</h2>
|
||||
|
||||
<div>
|
||||
<h4>Frontend URL</h4>
|
||||
<input placeholder="Frontend URL" type="text" id="frontend_url">
|
||||
<div style="width: 95%; margin: 0 auto;">
|
||||
<div class="form-group">
|
||||
<label for="frontend_url">Frontend URL:</label>
|
||||
<input class="form-control" type="text" id="frontend_url">
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="audio_only">
|
||||
Audio only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
<button style="margin-bottom: 10px;" class="btn btn-primary" data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample" id="save">Save</button>
|
||||
|
||||
<div class="collapse" id="collapseExample">
|
||||
<div class="card card-body">
|
||||
Settings successfully saved!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" id="audio_only">
|
||||
Audio only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div id="status"></div>
|
||||
<button id="save">Save</button>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,11 +7,10 @@ function save_options() {
|
||||
audio_only: audio_only
|
||||
}, function() {
|
||||
// Update status to let user know options were saved.
|
||||
var status = document.getElementById('status');
|
||||
status.textContent = 'Options saved.';
|
||||
$('#collapseExample').collapse('show');
|
||||
setTimeout(function() {
|
||||
status.textContent = '';
|
||||
}, 750);
|
||||
$('#collapseExample').collapse('hide');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
BIN
chrome-extension/youtubedl-material-chrome-extension.zip
Normal file
BIN
chrome-extension/youtubedl-material-chrome-extension.zip
Normal file
Binary file not shown.
BIN
chrome-extension/youtubedl-material-firefox-extension.zip
Normal file
BIN
chrome-extension/youtubedl-material-firefox-extension.zip
Normal file
Binary file not shown.
@@ -1,33 +0,0 @@
|
||||
|
||||
version: "2"
|
||||
services:
|
||||
ytdl_material:
|
||||
build: .
|
||||
environment:
|
||||
# config items
|
||||
ytdl_url: http://localhost:8998
|
||||
ytdl_port: '17442'
|
||||
ytdl_use_encryption: 'false'
|
||||
ytdl_cert_file_path: /etc/letsencrypt/live/example.com/fullchain.pem
|
||||
ytdl_key_file_path: /etc/letsencrypt/live/example.com/privkey.pem
|
||||
ytdl_audio_folder_path: audio/
|
||||
ytdl_video_folder_path: video/
|
||||
ytdl_title_top: Youtube Downloader
|
||||
ytdl_file_manager_enabled: 'true'
|
||||
ytdl_allow_quality_select: 'true'
|
||||
ytdl_download_only_mode: 'false'
|
||||
ytdl_allow_multi_download_mode: 'true'
|
||||
ytdl_use_youtube_api: 'false'
|
||||
ytdl_youtube_api_key: 'false'
|
||||
ytdl_default_theme: default
|
||||
ytdl_allow_theme_change: 'true'
|
||||
ytdl_use_default_downloading_agent: 'true'
|
||||
ytdl_custom_downloading_agent: 'false'
|
||||
ytdl_allow_advanced_download: 'false'
|
||||
# do not touch this
|
||||
write_ytdl_config: 'true'
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
restart: always
|
||||
ports:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:3.3
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd backend
|
||||
|
||||
# Start the first process
|
||||
node app.js &
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Failed to start my_first_process: $status"
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start the second process
|
||||
apachectl -DFOREGROUND
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Failed to start my_second_process: $status"
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Naive check runs checks once a minute to see if either of the processes exited.
|
||||
# This illustrates part of the heavy lifting you need to do if you want to run
|
||||
# more than one service in a container. The container will exit with an error
|
||||
# if it detects that either of the processes has exited.
|
||||
# Otherwise it will loop forever, waking up every 60 seconds
|
||||
|
||||
while /bin/true; do
|
||||
ps aux |grep node\ app.js # |grep -q -v grep
|
||||
PROCESS_1_STATUS=$?
|
||||
ps aux |grep apache2 # |grep -q -v grep
|
||||
PROCESS_2_STATUS=$?
|
||||
# If the greps above find anything, they will exit with 0 status
|
||||
# If they are not both 0, then something is wrong
|
||||
if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
|
||||
echo "One of the processes has already exited."
|
||||
exit -1
|
||||
fi
|
||||
sleep 60
|
||||
done
|
||||
10279
package-lock.json
generated
10279
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "0.0.0",
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"heroku-postbuild": "npm install --prefix backend",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
@@ -17,41 +18,43 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "^8.3.12",
|
||||
"@angular/animations": "^8.2.11",
|
||||
"@angular/cdk": "^8.2.3",
|
||||
"@angular/common": "^8.2.11",
|
||||
"@angular/compiler": "^8.2.11",
|
||||
"@angular/core": "^8.2.11",
|
||||
"@angular/forms": "^8.2.11",
|
||||
"@angular/http": "^7.2.15",
|
||||
"@angular/material": "^8.2.3",
|
||||
"@angular/platform-browser": "^8.2.11",
|
||||
"@angular/platform-browser-dynamic": "^8.2.11",
|
||||
"@angular/router": "^8.2.11",
|
||||
"@angular-devkit/core": "^9.0.6",
|
||||
"@angular/animations": "^9.1.0",
|
||||
"@angular/cdk": "^9.2.0",
|
||||
"@angular/common": "^9.1.0",
|
||||
"@angular/compiler": "^9.1.0",
|
||||
"@angular/core": "^9.0.7",
|
||||
"@angular/forms": "^9.1.0",
|
||||
"@angular/localize": "^9.1.0",
|
||||
"@angular/material": "^9.2.0",
|
||||
"@angular/platform-browser": "^9.1.0",
|
||||
"@angular/platform-browser-dynamic": "^9.1.0",
|
||||
"@angular/router": "^9.1.0",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^6.1.0",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
"nan": "^2.14.1",
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ng4-configure": "^0.1.7",
|
||||
"ngx-content-loading": "^0.1.3",
|
||||
"ngx-file-drop": "^9.0.1",
|
||||
"ngx-videogular": "^9.0.1",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"videogular2": "^7.0.1",
|
||||
"typescript": "~3.7.5",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.9.1",
|
||||
"typescript": "~3.5.3"
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.803.24",
|
||||
"@angular/cli": "^8.3.12",
|
||||
"@angular/compiler-cli": "^8.2.11",
|
||||
"@angular/language-service": "^8.2.11",
|
||||
"@angular-devkit/build-angular": "^0.901.0",
|
||||
"@angular/cli": "^9.0.7",
|
||||
"@angular/compiler-cli": "^9.0.7",
|
||||
"@angular/language-service": "^9.0.7",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "^5.0.1",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"electron": "^8.0.1",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
@@ -63,7 +66,6 @@
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.0.4",
|
||||
"tslint": "~5.3.2",
|
||||
"typescript": "~3.5.3"
|
||||
"tslint": "~5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
releases/youtubedl-material-latest.zip
Normal file
BIN
releases/youtubedl-material-latest.zip
Normal file
Binary file not shown.
@@ -2,10 +2,20 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||
import { SubscriptionComponent } from './subscription/subscription/subscription.component';
|
||||
import { PostsService } from './posts.services';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { DownloadsComponent } from './components/downloads/downloads.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'home', component: MainComponent },
|
||||
{ path: 'player', component: PlayerComponent},
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'home', component: MainComponent, canActivate: [PostsService] },
|
||||
{ path: 'player', component: PlayerComponent, canActivate: [PostsService]},
|
||||
{ path: 'subscriptions', component: SubscriptionsComponent, canActivate: [PostsService] },
|
||||
{ path: 'subscription', component: SubscriptionComponent, canActivate: [PostsService] },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'downloads', component: DownloadsComponent },
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -10,4 +10,19 @@
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.theme-slide-toggle {
|
||||
top: 2px;
|
||||
left: 10px;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidenav-container {
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
.top-toolbar {
|
||||
height: 64px;
|
||||
}
|
||||
@@ -1,17 +1,54 @@
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
|
||||
<div class="mat-elevation-z3" style="position: relative; z-index: 10;">
|
||||
<mat-toolbar color="primary" class="sticky-toolbar top-toolbar">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button #hamburgerMenu style="outline: none" *ngIf="router.url.split(';')[0] !== '/player'" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div style="font-size: 22px; text-shadow: #141414 0.25px 0.25px 1px;">
|
||||
{{topBarTitle}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #menuSettings="matMenu">
|
||||
<button (click)="openProfileDialog()" *ngIf="postsService.isLoggedIn" mat-menu-item>
|
||||
<mat-icon>person</mat-icon>
|
||||
<span i18n="Profile menu label">Profile</span>
|
||||
</button>
|
||||
<button (click)="themeMenuItemClicked($event)" *ngIf="allowThemeChange" mat-menu-item>
|
||||
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
|
||||
<span i18n="Dark mode toggle label">Dark</span>
|
||||
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
||||
</button>
|
||||
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('settings')" (click)="openSettingsDialog()" mat-menu-item>
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span i18n="Settings menu label">Settings</span>
|
||||
</button>
|
||||
<button (click)="openAboutDialog()" mat-menu-item>
|
||||
<mat-icon>info</mat-icon>
|
||||
<span i18n="About menu label">About</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
</mat-toolbar>
|
||||
</div>
|
||||
<div class="sidenav-container" style="height: calc(100% - 64px)">
|
||||
<mat-sidenav-container style="height: 100%">
|
||||
<mat-sidenav #sidenav>
|
||||
<mat-nav-list>
|
||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||
<router-outlet></router-outlet>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,9 @@ import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
@@ -15,9 +17,14 @@ import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
|
||||
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
||||
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
|
||||
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -33,28 +40,56 @@ export class AppComponent implements OnInit {
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
defaultTheme = null;
|
||||
allowThemeChange = null;
|
||||
allowSubscriptions = false;
|
||||
enableDownloadsManager = false;
|
||||
// defaults to true to prevent attack
|
||||
settingsPinRequired = true;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
@ViewChild('sidenav') sidenav: MatSidenav;
|
||||
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
|
||||
navigator: string = null;
|
||||
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar, private dialog: MatDialog,
|
||||
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
||||
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
||||
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
||||
this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true;
|
||||
|
||||
// sets theme to config default if it doesn't exist
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
this.navigator = localStorage.getItem('player_navigator');
|
||||
// runs on navigate, captures the route that navigated to the player (if needed)
|
||||
this.router.events.subscribe((e) => { if (e instanceof NavigationStart) {
|
||||
this.navigator = localStorage.getItem('player_navigator');
|
||||
} else if (e instanceof NavigationEnd) {
|
||||
// blurs hamburger menu if it exists, as the sidenav likes to focus on it after closing
|
||||
if (this.hamburgerMenuButton && this.hamburgerMenuButton.nativeElement) {
|
||||
this.hamburgerMenuButton.nativeElement.blur();
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.postsService.config_reloaded.subscribe(changed => {
|
||||
if (changed) {
|
||||
this.loadConfig();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
toggleSidenav() {
|
||||
this.sidenav.toggle();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
// loading config
|
||||
this.topBarTitle = this.postsService.config['Extra']['title_top'];
|
||||
this.settingsPinRequired = this.postsService.config['Extra']['settings_pin_required'];
|
||||
const themingExists = this.postsService.config['Themes'];
|
||||
this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default';
|
||||
this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true;
|
||||
this.allowSubscriptions = this.postsService.config['Subscriptions']['allow_subscriptions'];
|
||||
this.enableDownloadsManager = this.postsService.config['Extra']['enable_downloads_manager'];
|
||||
|
||||
// sets theme to config default if it doesn't exist
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
}
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
@@ -105,17 +140,77 @@ onSetTheme(theme, old_theme) {
|
||||
}
|
||||
}
|
||||
|
||||
themeMenuItemClicked(event) {
|
||||
this.flipTheme();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
} else {
|
||||
//
|
||||
}
|
||||
this.postsService.open_create_default_admin_dialog.subscribe(open => {
|
||||
if (open) {
|
||||
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
|
||||
dialogRef.afterClosed().subscribe(success => {
|
||||
if (success) {
|
||||
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
|
||||
} else {
|
||||
console.error('Failed to create default admin account. See logs for details.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/home']);
|
||||
if (!this.navigator) {
|
||||
this.router.navigate(['/home']);
|
||||
} else {
|
||||
this.router.navigateByUrl(this.navigator);
|
||||
}
|
||||
}
|
||||
|
||||
openSettingsDialog() {
|
||||
if (this.settingsPinRequired) {
|
||||
this.openPinDialog();
|
||||
} else {
|
||||
this.actuallyOpenSettingsDialog();
|
||||
}
|
||||
}
|
||||
|
||||
actuallyOpenSettingsDialog() {
|
||||
const dialogRef = this.dialog.open(SettingsComponent, {
|
||||
width: '80vw'
|
||||
});
|
||||
}
|
||||
|
||||
openPinDialog() {
|
||||
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
if (res) {
|
||||
this.actuallyOpenSettingsDialog();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
openAboutDialog() {
|
||||
const dialogRef = this.dialog.open(AboutDialogComponent, {
|
||||
width: '80vw'
|
||||
});
|
||||
}
|
||||
|
||||
openProfileDialog() {
|
||||
const dialogRef = this.dialog.open(UserProfileDialogComponent, {
|
||||
width: '60vw'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,80 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
|
||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||
MatProgressBarModule, MatExpansionModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule} from '@angular/material';
|
||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgModule, LOCALE_ID } from '@angular/core';
|
||||
import { registerLocaleData, CommonModule } from '@angular/common';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { FileCardComponent } from './file-card/file-card.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import {VgCoreModule} from 'videogular2/compiled/core';
|
||||
import {VgControlsModule} from 'videogular2/compiled/controls';
|
||||
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
||||
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||
import { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
|
||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||
import { NgxContentLoadingModule } from 'ngx-content-loading';
|
||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
|
||||
import { DownloadItemComponent } from './download-item/download-item.component';
|
||||
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||
import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component';
|
||||
import { SubscriptionComponent } from './subscription//subscription/subscription.component';
|
||||
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
|
||||
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||
|
||||
import es from '@angular/common/locales/es';
|
||||
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
||||
import { VideoInfoDialogComponent } from './dialogs/video-info-dialog/video-info-dialog.component';
|
||||
import { ArgModifierDialogComponent, HighlightPipe } from './dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
import { UpdaterComponent } from './updater/updater.component';
|
||||
import { UpdateProgressDialogComponent } from './dialogs/update-progress-dialog/update-progress-dialog.component';
|
||||
import { ShareMediaDialogComponent } from './dialogs/share-media-dialog/share-media-dialog.component';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { DownloadsComponent } from './components/downloads/downloads.component';
|
||||
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
|
||||
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
|
||||
import { ModifyUsersComponent } from './components/modify-users/modify-users.component';
|
||||
import { AddUserDialogComponent } from './dialogs/add-user-dialog/add-user-dialog.component';
|
||||
import { ManageUserComponent } from './components/manage-user/manage-user.component';
|
||||
import { ManageRoleComponent } from './components/manage-role/manage-role.component';
|
||||
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
||||
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
|
||||
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
|
||||
|
||||
registerLocaleData(es, 'es');
|
||||
|
||||
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
|
||||
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
|
||||
@@ -41,9 +88,35 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
PlayerComponent,
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent,
|
||||
DownloadItemComponent
|
||||
DownloadItemComponent,
|
||||
SubscriptionsComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionComponent,
|
||||
SubscriptionFileCardComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent,
|
||||
CheckOrSetPinDialogComponent,
|
||||
AboutDialogComponent,
|
||||
VideoInfoDialogComponent,
|
||||
ArgModifierDialogComponent,
|
||||
HighlightPipe,
|
||||
UpdaterComponent,
|
||||
UpdateProgressDialogComponent,
|
||||
ShareMediaDialogComponent,
|
||||
LoginComponent,
|
||||
DownloadsComponent,
|
||||
UserProfileDialogComponent,
|
||||
SetDefaultAdminDialogComponent,
|
||||
ModifyUsersComponent,
|
||||
AddUserDialogComponent,
|
||||
ManageUserComponent,
|
||||
ManageRoleComponent,
|
||||
CookiesUploaderDialogComponent,
|
||||
LogsViewerComponent,
|
||||
ModifyPlaylistComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
MatNativeDateModule,
|
||||
@@ -52,7 +125,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
MatToolbarModule,
|
||||
MatCardModule,
|
||||
@@ -67,22 +139,44 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatRippleModule,
|
||||
MatMenuModule,
|
||||
MatDialogModule,
|
||||
MatSlideToggleModule,
|
||||
MatMenuModule,
|
||||
MatAutocompleteModule,
|
||||
MatTabsModule,
|
||||
MatTooltipModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
MatChipsModule,
|
||||
DragDropModule,
|
||||
ClipboardModule,
|
||||
NgxFileDropModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
VgBufferingModule,
|
||||
LazyLoadImageModule.forRoot({ isVisible }),
|
||||
NgxContentLoadingModule,
|
||||
RouterModule,
|
||||
AppRoutingModule,
|
||||
],
|
||||
entryComponents: [
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent
|
||||
CreatePlaylistComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent,
|
||||
CheckOrSetPinDialogComponent
|
||||
],
|
||||
providers: [
|
||||
PostsService
|
||||
],
|
||||
exports: [
|
||||
HighlightPipe
|
||||
],
|
||||
providers: [PostsService],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
||||
export class AppModule { }
|
||||
|
||||
27
src/app/components/downloads/downloads.component.html
Normal file
27
src/app/components/downloads/downloads.component.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div style="padding: 20px;">
|
||||
<div *ngFor="let session_downloads of downloads | keyvalue">
|
||||
<ng-container *ngIf="keys(session_downloads.value).length > 0">
|
||||
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
|
||||
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container> {{session_downloads.key}}
|
||||
<span *ngIf="session_downloads.key === postsService.session_id"> <ng-container i18n="Current session">(current)</ng-container></span>
|
||||
</h4>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div *ngFor="let download of session_downloads.value | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
|
||||
<mat-card *ngIf="download.value" class="mat-elevation-z3">
|
||||
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads.key, download.value.uid)"></app-download-item>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn">Clear all downloads</button>
|
||||
</div>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="downloads && !downloadsValid()">
|
||||
<h4 style="text-align: center;" i18n="No downloads label">No downloads available!</h4>
|
||||
</div>
|
||||
</div>
|
||||
25
src/app/components/downloads/downloads.component.spec.ts
Normal file
25
src/app/components/downloads/downloads.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DownloadsComponent } from './downloads.component';
|
||||
|
||||
describe('DownloadsComponent', () => {
|
||||
let component: DownloadsComponent;
|
||||
let fixture: ComponentFixture<DownloadsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DownloadsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
171
src/app/components/downloads/downloads.component.ts
Normal file
171
src/app/components/downloads/downloads.component.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Component, OnInit, ViewChildren, QueryList, ElementRef, OnDestroy } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-downloads',
|
||||
templateUrl: './downloads.component.html',
|
||||
styleUrls: ['./downloads.component.scss'],
|
||||
animations: [
|
||||
// nice stagger effect when showing existing elements
|
||||
trigger('list', [
|
||||
transition(':enter', [
|
||||
// child animation selector + stagger
|
||||
query('@items',
|
||||
stagger(100, animateChild()), { optional: true }
|
||||
)
|
||||
]),
|
||||
]),
|
||||
trigger('items', [
|
||||
// cubic-bezier for a tiny bouncing feel
|
||||
transition(':enter', [
|
||||
style({ transform: 'scale(0.5)', opacity: 0 }),
|
||||
animate('500ms cubic-bezier(.8,-0.6,0.2,1.5)',
|
||||
style({ transform: 'scale(1)', opacity: 1 }))
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({ transform: 'scale(1)', opacity: 1, height: '*' }),
|
||||
animate('1s cubic-bezier(.8,-0.6,0.2,1.5)',
|
||||
style({ transform: 'scale(0.5)', opacity: 0, height: '0px', margin: '0px' }))
|
||||
]),
|
||||
])
|
||||
],
|
||||
})
|
||||
export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
|
||||
downloads_check_interval = 1000;
|
||||
downloads = {};
|
||||
interval_id = null;
|
||||
|
||||
keys = Object.keys;
|
||||
|
||||
valid_sessions_length = 0;
|
||||
|
||||
sort_downloads = (a, b) => {
|
||||
const result = b.value.timestamp_start - a.value.timestamp_start;
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getCurrentDownloads();
|
||||
this.interval_id = setInterval(() => {
|
||||
this.getCurrentDownloads();
|
||||
}, this.downloads_check_interval);
|
||||
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
if (!this.postsService.config['Extra']['enable_downloads_manager']) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.interval_id) { clearInterval(this.interval_id) }
|
||||
}
|
||||
|
||||
getCurrentDownloads() {
|
||||
this.postsService.getCurrentDownloads().subscribe(res => {
|
||||
if (res['downloads']) {
|
||||
this.assignNewValues(res['downloads']);
|
||||
} else {
|
||||
// failed to get downloads
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearDownload(session_id, download_uid) {
|
||||
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
|
||||
if (res['success']) {
|
||||
// this.downloads = res['downloads'];
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearDownloads(session_id) {
|
||||
this.postsService.clearDownloads(false, session_id).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.downloads = res['downloads'];
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAllDownloads() {
|
||||
this.postsService.clearDownloads(true).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.downloads = res['downloads'];
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assignNewValues(new_downloads_by_session) {
|
||||
const session_keys = Object.keys(new_downloads_by_session);
|
||||
|
||||
// remove missing session IDs
|
||||
const current_session_ids = Object.keys(this.downloads);
|
||||
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
|
||||
|
||||
for (const missing_session_id of missing_session_ids) {
|
||||
delete this.downloads[missing_session_id];
|
||||
}
|
||||
|
||||
// loop through sessions
|
||||
for (let i = 0; i < session_keys.length; i++) {
|
||||
const session_id = session_keys[i];
|
||||
const session_downloads_by_id = new_downloads_by_session[session_id];
|
||||
const session_download_ids = Object.keys(session_downloads_by_id);
|
||||
|
||||
if (this.downloads[session_id]) {
|
||||
// remove missing download IDs
|
||||
const current_download_ids = Object.keys(this.downloads[session_id]);
|
||||
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
|
||||
|
||||
for (const missing_download_id of missing_download_ids) {
|
||||
console.log('removing missing download id');
|
||||
delete this.downloads[session_id][missing_download_id];
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.downloads[session_id]) {
|
||||
this.downloads[session_id] = session_downloads_by_id;
|
||||
} else {
|
||||
for (let j = 0; j < session_download_ids.length; j++) {
|
||||
const download_id = session_download_ids[j];
|
||||
const download = new_downloads_by_session[session_id][download_id]
|
||||
if (!this.downloads[session_id][download_id]) {
|
||||
this.downloads[session_id][download_id] = download;
|
||||
} else {
|
||||
const download_to_update = this.downloads[session_id][download_id];
|
||||
download_to_update['percent_complete'] = download['percent_complete'];
|
||||
download_to_update['complete'] = download['complete'];
|
||||
download_to_update['timestamp_end'] = download['timestamp_end'];
|
||||
download_to_update['downloading'] = download['downloading'];
|
||||
download_to_update['error'] = download['error'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadsValid() {
|
||||
let valid = false;
|
||||
const keys = this.keys(this.downloads);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = this.downloads[key];
|
||||
if (this.keys(value).length > 0) {
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
}
|
||||
39
src/app/components/login/login.component.html
Normal file
39
src/app/components/login/login.component.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<mat-card class="login-card">
|
||||
<mat-tab-group [(selectedIndex)]="selectedTabIndex">
|
||||
<mat-tab label="Login">
|
||||
<div style="margin-top: 10px;">
|
||||
<mat-form-field>
|
||||
<input [(ngModel)]="loginUsernameInput" matInput placeholder="User name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input [(ngModel)]="loginPasswordInput" (keyup.enter)="login()" type="password" matInput placeholder="Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; margin-top: 10px;">
|
||||
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="registrationEnabled" label="Register">
|
||||
<div style="margin-top: 10px;">
|
||||
<mat-form-field>
|
||||
<input [(ngModel)]="registrationUsernameInput" matInput placeholder="User name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input [(ngModel)]="registrationPasswordInput" type="password" matInput placeholder="Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input [(ngModel)]="registrationPasswordConfirmationInput" type="password" matInput placeholder="Confirm Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; margin-top: 10px;">
|
||||
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-card>
|
||||
6
src/app/components/login/login.component.scss
Normal file
6
src/app/components/login/login.component.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.login-card {
|
||||
max-width: 600px;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
25
src/app/components/login/login.component.spec.ts
Normal file
25
src/app/components/login/login.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
115
src/app/components/login/login.component.ts
Normal file
115
src/app/components/login/login.component.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
|
||||
selectedTabIndex = 0;
|
||||
|
||||
// login
|
||||
loginUsernameInput = '';
|
||||
loginPasswordInput = '';
|
||||
loggingIn = false;
|
||||
|
||||
// registration
|
||||
registrationEnabled = false;
|
||||
registrationUsernameInput = '';
|
||||
registrationPasswordInput = '';
|
||||
registrationPasswordConfirmationInput = '';
|
||||
registering = false;
|
||||
|
||||
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.isLoggedIn) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
if (!this.postsService.config['Advanced']['multi_user_mode']) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
this.registrationEnabled = this.postsService.config['Users'] && this.postsService.config['Users']['allow_registration'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
login() {
|
||||
if (this.loginPasswordInput === '') {
|
||||
return;
|
||||
}
|
||||
this.loggingIn = true;
|
||||
this.postsService.login(this.loginUsernameInput, this.loginPasswordInput).subscribe(res => {
|
||||
this.loggingIn = false;
|
||||
if (res['token']) {
|
||||
this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
|
||||
} else {
|
||||
this.openSnackBar('Login failed, unknown error.');
|
||||
}
|
||||
}, err => {
|
||||
this.loggingIn = false;
|
||||
const error_code = err.status;
|
||||
if (error_code === 401) {
|
||||
this.openSnackBar('User name or password is incorrect!');
|
||||
} else if (error_code === 404) {
|
||||
this.openSnackBar('Login failed, cannot connect to the server.');
|
||||
} else {
|
||||
this.openSnackBar('Login failed, unknown error.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
register() {
|
||||
if (!this.registrationUsernameInput || this.registrationUsernameInput === '') {
|
||||
this.openSnackBar('User name is required!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.registrationPasswordInput || this.registrationPasswordInput === '') {
|
||||
this.openSnackBar('Password is required!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.registrationPasswordConfirmationInput || this.registrationPasswordConfirmationInput === '') {
|
||||
this.openSnackBar('Password confirmation is required!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registrationPasswordInput !== this.registrationPasswordConfirmationInput) {
|
||||
this.openSnackBar('Password confirmation is incorrect!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.registering = true;
|
||||
this.postsService.register(this.registrationUsernameInput, this.registrationPasswordInput).subscribe(res => {
|
||||
this.registering = false;
|
||||
if (res && res['user']) {
|
||||
this.openSnackBar(`User ${res['user']['name']} successfully registered.`);
|
||||
this.loginUsernameInput = res['user']['name'];
|
||||
this.selectedTabIndex = 0;
|
||||
} else {
|
||||
this.openSnackBar('Failed to register user, unknown error.');
|
||||
}
|
||||
}, err => {
|
||||
this.registering = false;
|
||||
if (err && err.error && typeof err.error === 'string') {
|
||||
this.openSnackBar(err.error);
|
||||
} else {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
21
src/app/components/logs-viewer/logs-viewer.component.html
Normal file
21
src/app/components/logs-viewer/logs-viewer.component.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<div style="height: 100%">
|
||||
<div *ngIf="logs_loading" style="position: absolute; top: 40%; left: 50%">
|
||||
<mat-spinner [diameter]="32"></mat-spinner>
|
||||
</div>
|
||||
<textarea style="height: 275px" matInput readonly [(ngModel)]="logs" placeholder="Logs will appear here" i18n-placeholder="Logs placeholder"></textarea>
|
||||
<div>
|
||||
<button style="margin-top: 12px;" [cdkCopyToClipboard]="logs" (click)="copiedLogsToClipboard()" mat-flat-button color="primary"><ng-container i18n="Copy to clipboard button text">Copy to clipboard</ng-container></button>
|
||||
<div style="float: right">
|
||||
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>
|
||||
<mat-form-field style="width: 75px;">
|
||||
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
|
||||
<mat-option [value]="10">10</mat-option>
|
||||
<mat-option [value]="25">25</mat-option>
|
||||
<mat-option [value]="50">50</mat-option>
|
||||
<mat-option [value]="100">100</mat-option>
|
||||
<mat-option [value]="0">All</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
25
src/app/components/logs-viewer/logs-viewer.component.spec.ts
Normal file
25
src/app/components/logs-viewer/logs-viewer.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogsViewerComponent } from './logs-viewer.component';
|
||||
|
||||
describe('LogsViewerComponent', () => {
|
||||
let component: LogsViewerComponent;
|
||||
let fixture: ComponentFixture<LogsViewerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LogsViewerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogsViewerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
40
src/app/components/logs-viewer/logs-viewer.component.ts
Normal file
40
src/app/components/logs-viewer/logs-viewer.component.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Component, OnInit, AfterViewInit } from '@angular/core';
|
||||
import { PostsService } from '../../posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logs-viewer',
|
||||
templateUrl: './logs-viewer.component.html',
|
||||
styleUrls: ['./logs-viewer.component.scss']
|
||||
})
|
||||
export class LogsViewerComponent implements OnInit {
|
||||
|
||||
logs: string = null;
|
||||
requested_lines = 50;
|
||||
logs_loading = false;
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLogs();
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
if (!this.logs) { this.logs_loading = true; } // only show loading spinner at the first load
|
||||
this.postsService.getLogs(this.requested_lines !== 0 ? this.requested_lines : null).subscribe(res => {
|
||||
this.logs_loading = false;
|
||||
if (res['logs']) {
|
||||
this.logs = res['logs'];
|
||||
} else {
|
||||
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||
}
|
||||
}, err => {
|
||||
this.logs_loading = false;
|
||||
console.error(err);
|
||||
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||
});
|
||||
}
|
||||
|
||||
copiedLogsToClipboard() {
|
||||
this.postsService.openSnackBar('Logs copied to clipboard!');
|
||||
}
|
||||
|
||||
}
|
||||
19
src/app/components/manage-role/manage-role.component.html
Normal file
19
src/app/components/manage-role/manage-role.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h4 *ngIf="role" mat-dialog-title><ng-container i18n="Manage role dialog title">Manage role</ng-container> - {{role.name}}</h4>
|
||||
|
||||
<mat-dialog-content *ngIf="role">
|
||||
<mat-list>
|
||||
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
|
||||
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
|
||||
<span matLine>
|
||||
<mat-radio-group [disabled]="permission === 'settings' && role.name === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
|
||||
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
|
||||
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,4 @@
|
||||
.mat-radio-button {
|
||||
margin-right: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
25
src/app/components/manage-role/manage-role.component.spec.ts
Normal file
25
src/app/components/manage-role/manage-role.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ManageRoleComponent } from './manage-role.component';
|
||||
|
||||
describe('ManageRoleComponent', () => {
|
||||
let component: ManageRoleComponent;
|
||||
let fixture: ComponentFixture<ManageRoleComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ManageRoleComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ManageRoleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
61
src/app/components/manage-role/manage-role.component.ts
Normal file
61
src/app/components/manage-role/manage-role.component.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-role',
|
||||
templateUrl: './manage-role.component.html',
|
||||
styleUrls: ['./manage-role.component.scss']
|
||||
})
|
||||
export class ManageRoleComponent implements OnInit {
|
||||
|
||||
role = null;
|
||||
available_permissions = null;
|
||||
permissions = null;
|
||||
|
||||
permissionToLabel = {
|
||||
'filemanager': 'File manager',
|
||||
'settings': 'Settings access',
|
||||
'subscriptions': 'Subscriptions',
|
||||
'sharing': 'Share files',
|
||||
'advanced_download': 'Use advanced download mode',
|
||||
'downloads_manager': 'Use downloads manager'
|
||||
}
|
||||
|
||||
constructor(public postsService: PostsService, private dialogRef: MatDialogRef<ManageRoleComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
if (this.data) {
|
||||
this.role = this.data.role;
|
||||
this.available_permissions = this.postsService.available_permissions;
|
||||
this.parsePermissions();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
parsePermissions() {
|
||||
this.permissions = {};
|
||||
for (let i = 0; i < this.available_permissions.length; i++) {
|
||||
const permission = this.available_permissions[i];
|
||||
if (this.role.permissions.includes(permission)) {
|
||||
this.permissions[permission] = 'yes';
|
||||
} else {
|
||||
this.permissions[permission] = 'no';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeRolePermissions(change, permission) {
|
||||
this.postsService.setRolePermission(this.role.name, permission, change.value).subscribe(res => {
|
||||
if (res['success']) {
|
||||
|
||||
} else {
|
||||
this.permissions[permission] = this.permissions[permission] === 'yes' ? 'no' : 'yes';
|
||||
}
|
||||
}, err => {
|
||||
this.permissions[permission] = this.permissions[permission] === 'yes' ? 'no' : 'yes';
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
31
src/app/components/manage-user/manage-user.component.html
Normal file
31
src/app/components/manage-user/manage-user.component.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<h4 *ngIf="user" mat-dialog-title><ng-container i18n="Manage user dialog title">Manage user</ng-container> - {{user.name}}</h4>
|
||||
|
||||
<mat-dialog-content *ngIf="user">
|
||||
<p><ng-container i18n="User UID">User UID:</ng-container> {{user.uid}}</p>
|
||||
|
||||
<div>
|
||||
<mat-form-field style="margin-right: 15px;">
|
||||
<input matInput [(ngModel)]="newPasswordInput" type="password" placeholder="New password" i18n-placeholder="New password placeholder">
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="accent" (click)="setNewPassword()" [disabled]="newPasswordInput.length === 0"><ng-container i18n="Set new password">Set new password</ng-container></button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-list>
|
||||
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
|
||||
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
|
||||
<span matLine>
|
||||
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
|
||||
<mat-radio-button value="default"><ng-container i18n="Use default">Use default</ng-container></mat-radio-button>
|
||||
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
|
||||
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,4 @@
|
||||
.mat-radio-button {
|
||||
margin-right: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
25
src/app/components/manage-user/manage-user.component.spec.ts
Normal file
25
src/app/components/manage-user/manage-user.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ManageUserComponent } from './manage-user.component';
|
||||
|
||||
describe('ManageUserComponent', () => {
|
||||
let component: ManageUserComponent;
|
||||
let fixture: ComponentFixture<ManageUserComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ManageUserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ManageUserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
69
src/app/components/manage-user/manage-user.component.ts
Normal file
69
src/app/components/manage-user/manage-user.component.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-user',
|
||||
templateUrl: './manage-user.component.html',
|
||||
styleUrls: ['./manage-user.component.scss']
|
||||
})
|
||||
export class ManageUserComponent implements OnInit {
|
||||
|
||||
user = null;
|
||||
newPasswordInput = '';
|
||||
available_permissions = null;
|
||||
permissions = null;
|
||||
|
||||
permissionToLabel = {
|
||||
'filemanager': 'File manager',
|
||||
'settings': 'Settings access',
|
||||
'subscriptions': 'Subscriptions',
|
||||
'sharing': 'Share files',
|
||||
'advanced_download': 'Use advanced download mode',
|
||||
'downloads_manager': 'Use downloads manager'
|
||||
}
|
||||
|
||||
settingNewPassword = false;
|
||||
|
||||
constructor(public postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
if (this.data) {
|
||||
this.user = this.data.user;
|
||||
this.available_permissions = this.postsService.available_permissions;
|
||||
this.parsePermissions();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
parsePermissions() {
|
||||
this.permissions = {};
|
||||
for (let i = 0; i < this.available_permissions.length; i++) {
|
||||
const permission = this.available_permissions[i];
|
||||
if (this.user.permission_overrides.includes(permission)) {
|
||||
if (this.user.permissions.includes(permission)) {
|
||||
this.permissions[permission] = 'yes';
|
||||
} else {
|
||||
this.permissions[permission] = 'no';
|
||||
}
|
||||
} else {
|
||||
this.permissions[permission] = 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeUserPermissions(change, permission) {
|
||||
this.postsService.setUserPermission(this.user.uid, permission, change.value).subscribe(res => {
|
||||
// console.log(res);
|
||||
});
|
||||
}
|
||||
|
||||
setNewPassword() {
|
||||
this.settingNewPassword = true;
|
||||
this.postsService.changeUserPassword(this.user.uid, this.newPasswordInput).subscribe(res => {
|
||||
this.newPasswordInput = '';
|
||||
this.settingNewPassword = false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
107
src/app/components/modify-users/modify-users.component.html
Normal file
107
src/app/components/modify-users/modify-users.component.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<div *ngIf="dataSource; else loading">
|
||||
<div style="padding: 15px">
|
||||
<div class="row">
|
||||
<div class="table table-responsive px-5 pb-4 pt-2">
|
||||
<div class="example-header">
|
||||
<mat-form-field>
|
||||
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="example-container mat-elevation-z8">
|
||||
|
||||
<mat-table #table [dataSource]="dataSource" matSort>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Username users table header"> User name </ng-container></mat-header-cell>
|
||||
|
||||
<mat-cell *matCellDef="let row">
|
||||
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingname">
|
||||
<span style="width: 80%;">
|
||||
<mat-form-field>
|
||||
<input matInput [(ngModel)]="constructedObject['name']" type="text" style="font-size: 12px">
|
||||
</mat-form-field>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #noteditingname>
|
||||
{{row.name}}
|
||||
</ng-template>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Email Column -->
|
||||
<ng-container matColumnDef="role">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Role users table header"> Role </ng-container></mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingemail">
|
||||
<span style="width: 80%;">
|
||||
<mat-form-field>
|
||||
<mat-select [(ngModel)]="constructedObject['role']">
|
||||
<mat-option value="admin">Admin</mat-option>
|
||||
<mat-option value="user">User</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #noteditingemail>
|
||||
{{row.role}}
|
||||
</ng-template>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<span *ngIf="editObject && editObject.uid === row.uid; else notediting">
|
||||
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Finish editing user">
|
||||
<mat-icon>done</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel editing user">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
<ng-template #notediting>
|
||||
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user">
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;">
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator #paginator [length]="length"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions">
|
||||
</mat-paginator>
|
||||
|
||||
<button color="primary" [disabled]="!this.users" mat-raised-button (click)="openAddUserDialog()" style="float: left; top: -45px; left: 15px">
|
||||
<ng-container i18n="Add users button">Add Users</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
|
||||
<mat-menu #edit_roles_menu="matMenu">
|
||||
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.name}}</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="position: absolute" class="centered">
|
||||
<ng-template #loading>
|
||||
<mat-spinner></mat-spinner>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
.edit-role {
|
||||
position: relative;
|
||||
top: -50px;
|
||||
left: 35px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModifyUsersComponent } from './modify-users.component';
|
||||
|
||||
describe('ModifyUsersComponent', () => {
|
||||
let component: ModifyUsersComponent;
|
||||
let fixture: ComponentFixture<ModifyUsersComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyUsersComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ModifyUsersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
219
src/app/components/modify-users/modify-users.component.ts
Normal file
219
src/app/components/modify-users/modify-users.component.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { Component, OnInit, Input, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AddUserDialogComponent } from 'app/dialogs/add-user-dialog/add-user-dialog.component';
|
||||
import { ManageUserComponent } from '../manage-user/manage-user.component';
|
||||
import { ManageRoleComponent } from '../manage-role/manage-role.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modify-users',
|
||||
templateUrl: './modify-users.component.html',
|
||||
styleUrls: ['./modify-users.component.scss']
|
||||
})
|
||||
export class ModifyUsersComponent implements OnInit, AfterViewInit {
|
||||
|
||||
displayedColumns = ['name', 'role', 'actions'];
|
||||
dataSource = new MatTableDataSource();
|
||||
|
||||
deleteDialogContentSubstring = 'Are you sure you want delete user ';
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
// MatPaginator Inputs
|
||||
length = 100;
|
||||
@Input() pageSize = 5;
|
||||
pageSizeOptions: number[] = [5, 10, 25, 100];
|
||||
|
||||
// MatPaginator Output
|
||||
pageEvent: PageEvent;
|
||||
users: any;
|
||||
editObject = null;
|
||||
constructedObject = {};
|
||||
roles = null;
|
||||
|
||||
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar, public dialog: MatDialog,
|
||||
private dialogRef: MatDialogRef<ModifyUsersComponent>) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getArray();
|
||||
this.getRoles();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the paginator and sort after the view init since this component will
|
||||
* be able to query its view for the initialized paginator and sort.
|
||||
*/
|
||||
afterGetData() {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
setPageSizeOptions(setPageSizeOptionsInput: string) {
|
||||
this.pageSizeOptions = setPageSizeOptionsInput.split(',').map(str => +str);
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string) {
|
||||
filterValue = filterValue.trim(); // Remove whitespace
|
||||
filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches
|
||||
this.dataSource.filter = filterValue;
|
||||
}
|
||||
|
||||
private getArray() {
|
||||
this.postsService.getUsers().subscribe(res => {
|
||||
this.users = res['users'];
|
||||
this.createAndSortData();
|
||||
this.afterGetData();
|
||||
});
|
||||
}
|
||||
|
||||
getRoles() {
|
||||
this.postsService.getRoles().subscribe(res => {
|
||||
this.roles = [];
|
||||
const roles = res['roles'];
|
||||
const role_names = Object.keys(roles);
|
||||
for (let i = 0; i < role_names.length; i++) {
|
||||
const role_name = role_names[i];
|
||||
this.roles.push({
|
||||
name: role_name,
|
||||
permissions: roles[role_name]['permissions']
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openAddUserDialog() {
|
||||
const dialogRef = this.dialog.open(AddUserDialogComponent);
|
||||
dialogRef.afterClosed().subscribe(user => {
|
||||
if (user && !user.error) {
|
||||
this.openSnackBar('Successfully added user ' + user.name);
|
||||
this.getArray();
|
||||
} else if (user && user.error) {
|
||||
this.openSnackBar('Failed to add user');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
finishEditing(user_uid) {
|
||||
let has_finished = false;
|
||||
if (this.constructedObject && this.constructedObject['name'] && this.constructedObject['role']) {
|
||||
if (!isEmptyOrSpaces(this.constructedObject['name']) && !isEmptyOrSpaces(this.constructedObject['role'])) {
|
||||
has_finished = true;
|
||||
const index_of_object = this.indexOfUser(user_uid);
|
||||
this.users[index_of_object] = this.constructedObject;
|
||||
this.constructedObject = {};
|
||||
this.editObject = null;
|
||||
this.setUser(this.users[index_of_object]);
|
||||
this.createAndSortData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableEditMode(user_uid) {
|
||||
if (this.uidInUserList(user_uid) && this.indexOfUser(user_uid) > -1) {
|
||||
const users_index = this.indexOfUser(user_uid);
|
||||
this.editObject = this.users[users_index];
|
||||
this.constructedObject['name'] = this.users[users_index].name;
|
||||
this.constructedObject['uid'] = this.users[users_index].uid;
|
||||
this.constructedObject['role'] = this.users[users_index].role;
|
||||
}
|
||||
}
|
||||
|
||||
disableEditMode() {
|
||||
this.editObject = null;
|
||||
}
|
||||
|
||||
// checks if user is in users array by name
|
||||
uidInUserList(user_uid) {
|
||||
for (let i = 0; i < this.users.length; i++) {
|
||||
if (this.users[i].uid === user_uid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// gets index of user in users array by name
|
||||
indexOfUser(user_uid) {
|
||||
for (let i = 0; i < this.users.length; i++) {
|
||||
if (this.users[i].uid === user_uid) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
setUser(change_obj) {
|
||||
this.postsService.changeUser(change_obj).subscribe(res => {
|
||||
this.getArray();
|
||||
});
|
||||
}
|
||||
|
||||
manageUser(user_uid) {
|
||||
const index_of_object = this.indexOfUser(user_uid);
|
||||
const user_obj = this.users[index_of_object];
|
||||
this.dialog.open(ManageUserComponent, {
|
||||
data: {
|
||||
user: user_obj
|
||||
},
|
||||
width: '65vw'
|
||||
});
|
||||
}
|
||||
|
||||
removeUser(user_uid) {
|
||||
this.postsService.deleteUser(user_uid).subscribe(res => {
|
||||
this.getArray();
|
||||
}, err => {
|
||||
this.getArray();
|
||||
});
|
||||
}
|
||||
|
||||
createAndSortData() {
|
||||
// Sorts the data by last finished
|
||||
this.users.sort((a, b) => b.name > a.name);
|
||||
|
||||
const filteredData = [];
|
||||
for (let i = 0; i < this.users.length; i++) {
|
||||
filteredData.push(JSON.parse(JSON.stringify(this.users[i])));
|
||||
}
|
||||
|
||||
// Assign the data to the data source for the table to render
|
||||
this.dataSource.data = filteredData;
|
||||
}
|
||||
|
||||
openModifyRole(role) {
|
||||
const dialogRef = this.dialog.open(ManageRoleComponent, {
|
||||
data: {
|
||||
role: role
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(success => {
|
||||
this.getRoles();
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isEmptyOrSpaces(str){
|
||||
return str === null || str.match(/^ *$/) !== null;
|
||||
}
|
||||
1
src/app/consts.ts
Normal file
1
src/app/consts.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const CURRENT_VERSION = 'v4.0';
|
||||
@@ -1,13 +1,14 @@
|
||||
<h4 mat-dialog-title>Create a playlist</h4>
|
||||
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
|
||||
<form>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>{{(type === 'audio') ? 'Audio files' : 'Videos'}}</mat-label>
|
||||
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
|
||||
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
|
||||
<mat-select [formControl]="filesSelect" multiple required aria-required>
|
||||
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
|
||||
</mat-select>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
|
||||
32
src/app/dialogs/about-dialog/about-dialog.component.html
Normal file
32
src/app/dialogs/about-dialog/about-dialog.component.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<h4 style="position: relative" mat-dialog-title><ng-container i18n="About dialog title">About YoutubeDL-Material</ng-container>
|
||||
<span class="logo-image">
|
||||
<a [href]="projectLink" target="_blank">
|
||||
<img style="width: 32px;" src="assets/images/GitHub-64px.png">
|
||||
</a>
|
||||
<img style="width: 32px; margin-left: 15px;" src="assets/images/logo_128px.png">
|
||||
</span>
|
||||
</h4>
|
||||
<mat-dialog-content>
|
||||
<div style="margin-bottom: 5px;">
|
||||
<p>
|
||||
<i>YoutubeDL-Material</i> <ng-container i18n="About first paragraph">is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos.</ng-container>
|
||||
</p>
|
||||
<p>
|
||||
<i>YoutubeDL-Material</i> <ng-container i18n="About second paragraph">has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above.</ng-container>
|
||||
</p>
|
||||
<mat-divider></mat-divider>
|
||||
<h5 style="margin-top: 10px;">Installation details:</h5>
|
||||
<p>
|
||||
<ng-container i18n="Version label">Installed version:</ng-container> {{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner> <ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
|
||||
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon> <a *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag" [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container>
|
||||
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
|
||||
</p>
|
||||
<p>
|
||||
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
||||
</p>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close>Close</button>
|
||||
</mat-dialog-actions>
|
||||
23
src/app/dialogs/about-dialog/about-dialog.component.scss
Normal file
23
src/app/dialogs/about-dialog/about-dialog.component.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
i {
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.version-spinner {
|
||||
top: 4px;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.version-checked-icon {
|
||||
top: 5px;
|
||||
margin-left: 2px;
|
||||
position: relative;
|
||||
margin-right: -3px;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
}
|
||||
25
src/app/dialogs/about-dialog/about-dialog.component.spec.ts
Normal file
25
src/app/dialogs/about-dialog/about-dialog.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AboutDialogComponent } from './about-dialog.component';
|
||||
|
||||
describe('AboutDialogComponent', () => {
|
||||
let component: AboutDialogComponent;
|
||||
let fixture: ComponentFixture<AboutDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AboutDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AboutDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
33
src/app/dialogs/about-dialog/about-dialog.component.ts
Normal file
33
src/app/dialogs/about-dialog/about-dialog.component.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { CURRENT_VERSION } from 'app/consts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-dialog',
|
||||
templateUrl: './about-dialog.component.html',
|
||||
styleUrls: ['./about-dialog.component.scss']
|
||||
})
|
||||
export class AboutDialogComponent implements OnInit {
|
||||
|
||||
projectLink = 'https://github.com/Tzahi12345/YoutubeDL-Material';
|
||||
issuesLink = 'https://github.com/Tzahi12345/YoutubeDL-Material/issues';
|
||||
latestUpdateLink = 'https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest'
|
||||
latestGithubRelease = null;
|
||||
checking_for_updates = true;
|
||||
|
||||
current_version_tag = CURRENT_VERSION;
|
||||
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLatestGithubRelease()
|
||||
}
|
||||
|
||||
getLatestGithubRelease() {
|
||||
this.postsService.getLatestGithubRelease().subscribe(res => {
|
||||
this.checking_for_updates = false;
|
||||
this.latestGithubRelease = res;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user