Compare commits

..

138 Commits

Author SHA1 Message Date
Isaac Abadi
004a234b02 Downloads are now properly assigned a filename 2020-09-21 00:27:49 -04:00
Isaac Abadi
daca715d1b Fixes bug where playlists could not have download progress tracked
- downloads are now treated as playlists for cleaner logic
2020-09-20 23:01:43 -04:00
Isaac Abadi
8fdc231f08 Updated new home page UI to support file manager disabling and permissions
- file manager enabled state is now cached for faster loading
2020-09-18 11:22:45 -04:00
Isaac Abadi
ae8f7a2a33 Fixed bug that prevented playlists from being navigated to 2020-09-18 11:05:13 -04:00
Tzahi12345
d0782bb444 Update README.md
Updated API docs

Fixes #213
2020-09-18 00:46:42 -04:00
Isaac Abadi
49210abb49 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-09-17 03:15:29 -04:00
Isaac Abadi
851bfb81ba File cards are now properly centered 2020-09-17 03:12:09 -04:00
Isaac Abadi
35d0d439fa Control-clicking file cards will now open the player in a new tab 2020-09-17 03:11:52 -04:00
Tzahi12345
ded3ad6dfc Merge pull request #212 from Tzahi12345/dependabot/npm_and_yarn/backend/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1 in /backend
2020-09-12 17:07:53 -04:00
dependabot[bot]
61daf26641 Bump node-fetch from 2.6.0 to 2.6.1 in /backend
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-12 20:55:44 +00:00
Tzahi12345
95e53b9549 Fixed bug where unix paths would improperly parsed while importing unregistered files 2020-09-07 16:06:25 -04:00
Tzahi12345
46ed0fe992 Fixed bug in import unregistered logic where files in subfolders could not be found 2020-09-07 00:39:27 -04:00
Isaac Abadi
082252ab1e Updated sidenav logic for "side" mode, where it will now autoclose in the player, be open everywhere else 2020-08-31 15:21:58 -04:00
Tzahi12345
5eccaa13e5 Merge pull request #206 from Tzahi12345/downloader-improvements
Downloader improvements - updated system and bug fixes
2020-08-31 15:17:40 -04:00
Isaac Abadi
71633950b2 Comments cleanup 2020-08-31 15:03:04 -04:00
Isaac Abadi
f31dad0215 JSON metadata files are no longer kept if the associated setting is not enabled 2020-08-30 05:56:25 -04:00
Isaac Abadi
7efbe40bb2 Added setting for including metadata/thumbnails in the UI 2020-08-30 05:55:50 -04:00
Isaac Abadi
5b768b5bda JSON blobs were accidentally inserted into DB, stringifying then parsing the video file object fixes this 2020-08-30 05:42:52 -04:00
Isaac Abadi
365cbc3ffa Mkv/webm formats are now included for quality select (will get merged into mp4 at the end) 2020-08-29 23:08:23 -04:00
Isaac Abadi
44647f3306 Download progress is now shown when downloads are 1% complete or more (it was 15% before) 2020-08-29 23:06:40 -04:00
Isaac Abadi
8a7409478a Added the ability to download videos at higher resolutions than the highest mp4 (fixes #76)
Deprecates normal downloading method. The "safe" method is now always used, and download progress is now estimated using the predicted end file size

Thumbnails are now auto downloaded along with the other metadata
2020-08-29 23:05:37 -04:00
Tzahi12345
70159813e5 Merge pull request #205 from Tzahi12345/add-ldap-auth
Added ability to register/login through LDAP
2020-08-26 04:30:43 -04:00
Isaac Abadi
babba9aa30 Added ability to register/login through LDAP
- Added ability to edit LDAP settings and whether to use LDAP or not in the users tab in the settings
2020-08-26 04:18:29 -04:00
Tzahi12345
d292275956 Unfinished subscriptions will no longer cause an error during server startup 2020-08-24 05:13:27 -04:00
Tzahi12345
ba2acedb94 Files are now reloaded when you navigate back home 2020-08-24 05:13:01 -04:00
Tzahi12345
aa0558b770 Subscriptions are now reloaded on subscribe/unsubscribe in PostsService 2020-08-24 05:11:56 -04:00
Tzahi12345
d7f04fc90a Text for file duration in the unified file card component is now always black 2020-08-24 05:11:04 -04:00
Tzahi12345
087c9f1bb1 Added public directory to the gitignore 2020-08-24 02:44:52 -04:00
Tzahi12345
f874617965 Fixes bug where cached JWT token could prevent default admin creation 2020-08-24 02:44:39 -04:00
Tzahi12345
8fb8543829 Merge pull request #203 from Tzahi12345/arm-autobuild-test
Fix ARM autobuild
2020-08-24 02:20:19 -04:00
Tzahi12345
70d89d310c Removed unneeded hooks 2020-08-24 02:18:39 -04:00
Tzahi12345
c48aaaf13c Possible fix for arm autobuild (2) 2020-08-24 00:25:59 -04:00
Tzahi12345
6cf7ea193a Possible fix for arm autobuild 2020-08-24 00:21:10 -04:00
Isaac Abadi
c9016f446d Ghosting colors for loading videos is now customizable in an internal config, allowing dark mode to have different colors for the ghosting 2020-08-22 18:55:24 -04:00
Isaac Abadi
919e2a649a Updated styling of recent videos
Added content loader module which will add ghosting to the recent videos component while the files are loading

Updated custom playlists component to support large sized cards
2020-08-22 18:34:28 -04:00
Isaac Abadi
dc6dd5f5a2 Added support for "large" sized cards 2020-08-22 00:47:18 -04:00
Tzahi12345
2e4ef3b224 Moves hooks back into their proper directory and updated arm dockerfile to avoid installing curl just go get qemu 2020-08-18 16:43:52 -04:00
Tzahi12345
0e35b2ca1b Proxy now forces safe download mode 2020-08-18 15:34:03 -04:00
Isaac Abadi
9937bc9bb6 Updated ARM dockerfile to fix auto build 2020-08-15 18:31:57 -04:00
Tzahi12345
178a61c381 Merge pull request #198 from Tzahi12345/docker-frontend
Rework Dockerfile to build the frontend (with improved frontend build)
2020-08-15 15:38:34 -04:00
Isaac Abadi
b455d8a900 ARM dockerfile now builds frontend using Angular CLI 2020-08-15 14:07:06 -04:00
Isaac Abadi
72fa439569 Dockerfile now uses angular CLI directly for building process 2020-08-15 13:43:14 -04:00
Sandro Jäckel
33b1affa73 Rework Dockerfile to build the frontend
which removes the commited build files from the repo
2020-08-15 18:32:57 +02:00
Isaac Abadi
da73e47f08 Fixes bug where an error would occur on startup if a user's file folder was missing (such as "{user}/audio" if they had not downloaded any audio files) 2020-08-14 20:04:40 -04:00
Tzahi12345
3faf715b88 Merge pull request #196 from Tzahi12345/remove-use-encryption-settings
Removed "use encryption" options
2020-08-14 18:01:23 -04:00
Isaac Abadi
eda75e9a19 Updated docs and removed deprecated encrypted.json file from repo 2020-08-14 18:00:14 -04:00
Isaac Abadi
560aaadca1 Removed "use encryption" options, if you'd like to encrypt your web page, use a reverse proxy 2020-08-14 17:56:44 -04:00
Isaac Abadi
eb7fdb649d Updated frontend binaries 2020-08-14 17:48:51 -04:00
Isaac Abadi
533dd49ed0 Subscription buttons are now fixed 2020-08-14 17:42:51 -04:00
Isaac Abadi
001d907c3a Fixed UI bug where quality options spinner would clip through the URL input card 2020-08-14 17:42:31 -04:00
Isaac Abadi
4302625858 Updated styling of recent videos component, the search/sort options now look good on mobile 2020-08-14 17:41:55 -04:00
Isaac Abadi
e2cec9321e Importing of videos during startup now uses standard registering of videos into db process and refactored registering to support aforementioned feature
Removed erroneous console log
2020-08-14 02:44:27 -04:00
Tzahi12345
2add31af6d Merge pull request #195 from Tzahi12345/import-files-on-startup
Import files on startup
2020-08-14 02:18:04 -04:00
Isaac Abadi
5de37f6fbf Importing of unregistered files now happens on startup
recFindByExt in app.js removed, now uses utils.recFindByExt

Minor code cleanup
2020-08-14 02:10:40 -04:00
Isaac Abadi
7aace85ef4 Added ability to import unregistered files into the db if they are missing from the db but exist in their expected folder 2020-08-14 02:08:51 -04:00
Isaac Abadi
736c3f5cab Added ability to discover existing files regardless of type in a directory
- added recFindByExt helper function to utils.js
2020-08-14 02:07:39 -04:00
Isaac Abadi
c3c7667c17 Added quotations to existing sub error message 2020-08-13 18:34:57 -04:00
Isaac Abadi
52bee8b280 Subscriptions with the same URL can now be added as long as the new subscription is named 2020-08-13 18:34:30 -04:00
Isaac Abadi
945ba268fb Fixed bug where non-shared videos could be viewed by others
Fixed bug where non-users couldn't download a shared video
2020-08-12 16:23:28 -04:00
Isaac Abadi
d49a67dfd0 Updated styling for videos in video player 2020-08-11 21:04:28 -04:00
Isaac Abadi
96c52f2d5b Fixed bug where subscription videos could not be downloaded from the player 2020-08-11 14:45:37 -04:00
Isaac Abadi
61c03b6681 Sidepanel defaults to open when in 'side' mode AND in the home page 2020-08-11 01:25:33 -04:00
Isaac Abadi
7c349163b6 Updated max height for video player 2020-08-10 14:37:52 -04:00
Isaac Abadi
a0acdd1d86 Updated frontend binaries 2020-08-10 14:34:12 -04:00
Isaac Abadi
25bfb9e518 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-08-10 14:27:46 -04:00
Isaac Abadi
c9b615c659 Added option to change JWT expiration time called "Login expiration" in the Advanced tab 2020-08-10 14:26:59 -04:00
Tzahi12345
c4f21dc1cc Merge pull request #192 from Tzahi12345/homepage-redesign
Homepage redesign (v1)
2020-08-09 21:01:00 -04:00
Isaac Abadi
e678f4b476 Rebuilt frontend binaries 2020-08-09 21:00:10 -04:00
Isaac Abadi
356f31343e Adjusted positioning of add playlist button on home screen 2020-08-09 20:47:23 -04:00
Isaac Abadi
38c46b5be5 Updated frontend binaries 2020-08-09 20:43:18 -04:00
Isaac Abadi
12dcdfcb3c Sidepanel mode and card size is now configurable and can be set from the about dialog (temp location) 2020-08-09 20:39:59 -04:00
Isaac Abadi
47c19c0cdc Updated player styling 2020-08-09 20:17:01 -04:00
Isaac Abadi
d0eff42f2a Removed unneeded comments 2020-08-09 20:16:49 -04:00
Isaac Abadi
4472aae3e9 Removed redundant header in main component 2020-08-09 20:16:27 -04:00
Isaac Abadi
c55d3de9a0 Recent videos component now includes header, search, and sort capabilities 2020-08-09 20:16:10 -04:00
Isaac Abadi
1cdc1640ac Unified file card now supports playlists
Added custom playlists component

Removed legacy file manager from home screen
2020-08-09 19:24:29 -04:00
Isaac Abadi
fcaf8b5a62 Updated create playlist dialog to not require a type to be set prior
- duration and registered variables are now set for playlists
2020-08-09 19:23:29 -04:00
Isaac Abadi
8c7b2dfc79 Added ability to delete files in the recent videos component w/ archive support 2020-08-09 14:08:22 -04:00
Isaac Abadi
59ad74ed79 Fixed bug where subscriptions may register the same file multiple times 2020-08-09 14:07:36 -04:00
Isaac Abadi
690871f6b2 Moved hooks to their proper directory 2020-08-08 16:55:04 -04:00
Isaac Abadi
5088ce0291 Potentially fixed autobuild issue for ARM images 2020-08-08 16:02:41 -04:00
Isaac Abadi
576e2109d7 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-08-08 16:00:09 -04:00
Isaac Abadi
c6c80579df Updated package.json to add ngx-avatar dependency 2020-08-08 15:59:44 -04:00
Isaac Abadi
0ab6535fec Added ability to download files for recent videos component
Updated styling for unified file card (elevation on hover)
2020-08-08 15:59:29 -04:00
Isaac Abadi
d7aa39599d Removed subscriptions_use_youtubedl_archive setting, to use youtube-dl archive functionality, there is now just one setting for both subscription and non-subscription videos 2020-08-08 15:58:48 -04:00
Tzahi12345
ee169cd7ce If chown fails during container setup, then a warning will be shown 2020-08-05 21:29:03 -04:00
Tzahi12345
835790e69c Chown that fails will not crash startup anymore for Docker 2020-08-03 20:07:02 -04:00
Isaac Abadi
68037613d8 Added icon for file type (audio/video) next to the download date 2020-08-02 18:25:41 -04:00
Isaac Abadi
3df384de22 Recent videos now supports 2 card sizes 2020-08-02 18:03:50 -04:00
Isaac Abadi
f0c4ed4590 Unified file card now supports small and medium size
Duration styling/position updated and added download date time
2020-08-02 18:03:29 -04:00
Isaac Abadi
fd35153721 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into homepage-redesign 2020-08-02 16:24:07 -04:00
Tzahi12345
d0aed8f144 Merge pull request #184 from Tzahi12345/edit-subscriptions
Adds ability to edit subscriptions
2020-08-01 21:31:46 -04:00
Isaac Abadi
d17b68d76e Updated frontend binaries 2020-08-01 21:28:49 -04:00
Isaac Abadi
a481869166 Slightly update subscribe dialog styling 2020-08-01 21:28:22 -04:00
Isaac Abadi
eb4ed32fcb Added edit button to subscription 2020-08-01 21:27:35 -04:00
Isaac Abadi
9aee6e91cd Added API to update subscription
Edit subscription component now works
2020-08-01 21:26:47 -04:00
Isaac Abadi
37d3e9326c Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into edit-subscriptions 2020-08-01 20:10:47 -04:00
Isaac Abadi
dbf8f9ebfd Updated package-locks 2020-08-01 20:09:29 -04:00
Tzahi12345
7858e26b15 Update README.md
Updated Docker section in README to show how to use a custom UID/GID
2020-08-01 14:06:47 -04:00
Isaac Abadi
057ad67672 Fixed bug where subscribing to a private playlist failed 2020-08-01 06:55:25 -04:00
Isaac Abadi
3ebc903ce9 Fixed bug where non-admins in multi user mode could change the settings if they could open the dialog 2020-07-27 22:09:15 -04:00
Isaac Abadi
21c5795f1c Rebuilt frontend binaries 2020-07-27 22:02:23 -04:00
Isaac Abadi
f12ea017bc Re-added parameter to checkAdminCreationStatus 2020-07-27 22:01:47 -04:00
Isaac Abadi
cd18bce509 Fixed bug where settings menu could be accessed from the login menu in multi user mode 2020-07-27 21:55:01 -04:00
Isaac Abadi
5ef4388d73 Fixed bug where checking admin creation status would not run 2020-07-27 21:51:31 -04:00
Isaac Abadi
a78c0cb56c Updated frontend binaries 2020-07-25 16:13:09 -04:00
Isaac Abadi
ae9e4e6857 Updated styling in settings and about page for a cleaner look 2020-07-25 16:02:36 -04:00
Isaac Abadi
333556c305 Removed erroneous code and added the ability to kill all downlaods 2020-07-25 15:57:10 -04:00
Isaac Abadi
c5b0a7a697 Updated confirm dialog to support async requests with loading spinner 2020-07-25 15:55:52 -04:00
Isaac Abadi
d7631360cc Erroneous quality args are no longer added when using then normal downloading method 2020-07-23 23:29:05 -04:00
Isaac Abadi
c800308b9d Removed all instances of res.end() as it caused errors on debian-based systems and is a redundant call 2020-07-22 21:58:35 -04:00
Isaac Abadi
c1c57135ba Fixed bug where deleting an audio file would result in an error 2020-07-22 21:57:09 -04:00
Isaac Grynsztein
8384b73c4c Added support for navigating to files in recent videos (subscription and not). No support for download-only mode yet
Added navigate to subscription menu item for the files

Sidenav mode is "side" now for testing, likely not a permanent change and will be optional in the future
2020-07-18 20:37:43 -04:00
Isaac Grynsztein
834ac00694 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-07-15 23:48:34 -04:00
Isaac Grynsztein
935ae3452c Repo cleanup 2020-07-15 23:46:25 -04:00
Isaac Grynsztein
cc189a3abd Unified videos videos are now properly retrieved from the server 2020-07-15 23:37:00 -04:00
Isaac Grynsztein
335b588c3a Added edit subscription dialog (WIP) 2020-07-15 22:58:17 -04:00
Tzahi12345
493abc3a4c Merge pull request #170 from Tzahi12345/dependabot/npm_and_yarn/backend/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19 in /backend
2020-07-15 17:37:18 -04:00
dependabot[bot]
238abc1686 Bump lodash from 4.17.15 to 4.17.19 in /backend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-15 21:35:09 +00:00
Tzahi12345
fbfad6c3e2 Merge pull request #168 from fluxtendu/master
Switch from bcrypt to bcryptjs to keep compatibility with Raspberry Pi and a Dockerfile-armhf bonus
2020-07-15 17:34:34 -04:00
fluxtendu
a6ff65f004 Merge branch 'master' of github.com:fluxtendu/YoutubeDL-Material 2020-07-15 00:02:06 +02:00
fluxtendu
89cd969fcb Adapted for Raspberry Pi:
- Added Dockerfile-armhf (arm32v7 alpine)
 - Switched from bcrypt to bcrypt.js

new file : Dockerfile-armhf
modified : authentication/auth.js
modified : package.json
2020-07-14 23:00:08 +02:00
Isaac Grynsztein
4ebb2d4297 Created unified file card component, recent videos component (not done) and started scaffolding work on the backend 2020-07-13 22:09:48 -04:00
Isaac Grynsztein
d371ccf094 Fixed bug in about page where "You can update from the settings menu" was erroneously displayed 2020-07-12 03:41:23 -04:00
Tzahi12345
e7181b57c7 Update README.md
Updated contributing section to include the Contributing wiki page
2020-07-11 22:03:13 -04:00
Tzahi12345
38fa39d765 Merge pull request #159 from UnlimitedCookies/patch-1
Make more items translatable
2020-07-11 19:26:16 -04:00
UnlimitedCookies
9e5de88675 Even more internationalization improvements :) 2020-07-10 15:00:54 +02:00
Isaac Grynsztein
9cf4949c30 Updated frontend binaries 2020-07-09 22:37:24 -04:00
Isaac Grynsztein
dd80c51f16 Removed pin setting functionality
- Simplifies security options: use multi user mode if you want to restrict access to the settings menu
2020-07-09 22:33:07 -04:00
UnlimitedCookies
84d83f228e Make close button in About Dialog translatable 2020-07-10 01:31:59 +02:00
Isaac Grynsztein
14bd82c508 Fixed bug where navigating home would reset the cached volume setting 2020-07-07 15:27:15 -04:00
Isaac Grynsztein
0b6606cafb Updated spanish translations 2020-07-05 22:18:08 -04:00
Isaac Grynsztein
e97e9ec717 Logs viewer will now color-code logs based on type (error, warning, info, etc.)
You can also clear logs from the logs viewer as well
2020-07-05 22:17:54 -04:00
Isaac Grynsztein
990b3d4037 Added confirm dialog component to help with confirming actions 2020-07-05 22:12:07 -04:00
Isaac Grynsztein
2e7b1c2d53 Fixed wording for youtube-dl archive setting in the downloader tab 2020-07-04 21:34:47 -04:00
Isaac Grynsztein
a4eb7fb745 Updated README to reflect new changes to Docker installation process and removed config items due to clutter and out-of-dateness 2020-07-04 12:17:16 -04:00
Isaac Grynsztein
2442067ca0 Manually updated spanish translations 2020-07-04 12:07:22 -04:00
Isaac Grynsztein
41d4dfeba1 Updated version to 4.1 2020-07-03 17:57:47 -04:00
98 changed files with 3196 additions and 2586 deletions

1
.gitignore vendored
View File

@@ -65,3 +65,4 @@ backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt
backend/public

43
Dockerfile Normal file
View File

@@ -0,0 +1,43 @@
FROM alpine:3.12 as frontend
RUN apk add --no-cache \
npm
RUN npm install -g @angular/cli
WORKDIR /build
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN ng build --prod
#--------------#
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 [ "backend/package.json", "backend/package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

View File

@@ -43,7 +43,7 @@ Optional dependencies:
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file. 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.
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
@@ -53,34 +53,6 @@ NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi1234
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.
### Configuration
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
| Config item | Description | Default |
| ------------- | ------------- | ------------- |
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
| port | Desired port for YoutubeDL-Material | "17442" |
| use-encryption | true if you intend to use SSL encryption (https) | false |
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
| path-audio | Path to audio folder for saved mp3s | "audio/" |
| path-video | Path to video folder for saved mp4s | "video/" |
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
| file_manager_enabled | true if you want to use the file manager | true |
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
| download_only_mode | true if you want files to directly download to the client with no media player | false |
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
| 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 | false |
## 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.
@@ -97,18 +69,28 @@ Alternatively, you can port forward the port specified in the config (defaults t
## Docker
### Setup
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/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!
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
4. Make sure you can connect to the specified URL + port, and if so, you are done!
### Custom UID/GID
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
```
environment:
UID: YOUR_UID
GID: YOUR_GID
```
## API
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
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.
@@ -116,12 +98,20 @@ Once you have enabled the API and have the key, you can start sending requests b
## Contributing
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
If you're interested in contributing, first: awesome! Second, please refer to the guidelines/setup information located in the [Contributing](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Contributing) wiki page, it's a helpful way to get you on your feet and coding away.
Pull requests are always appreciated! If you're a bit rusty with coding, that's no problem: we can always help you learn. And if that's too scary, that's OK too! You can create issues for features you'd like to see or bugs you encounter, it all helps this project grow.
If you're interested in translating the app into a new language, check out the [Translate](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Translate) wiki page.
## Authors
* **Isaac Grynsztein** (me!) - *Initial work*
Official translators:
* Spanish - tzahi12345
* German - UnlimitedCookies
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
## License

49
armhf.Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
FROM alpine:3.12 as frontend
RUN apk add --no-cache \
npm \
curl
RUN npm install -g @angular/cli
WORKDIR /build
RUN curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN ng build --prod
#--------------#
FROM arm32v7/alpine:3.12
COPY --from=frontend /build/qemu-arm-static /usr/bin
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 [ "backend/package.json", "backend/package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

View File

@@ -1,27 +0,0 @@
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" ]

View File

@@ -7,7 +7,7 @@ var path = require('path');
var youtubedl = require('youtube-dl');
var ffmpeg = require('fluent-ffmpeg');
var compression = require('compression');
var https = require('https');
var glob = require("glob")
var multer = require('multer');
var express = require("express");
var bodyParser = require("body-parser");
@@ -30,6 +30,7 @@ var subscriptions_api = require('./subscriptions')
const CONSTS = require('./consts')
const { spawn } = require('child_process')
const read_last_lines = require('read-last-lines');
var ps = require('ps-node');
const is_windows = process.platform === 'win32';
@@ -96,7 +97,6 @@ db.defaults(
configWriteFlag: false,
downloads: {},
subscriptions: [],
pin_md5: '',
files_to_db_migration_complete: false
}).write();
@@ -128,7 +128,6 @@ users_db.defaults(
var frontendUrl = null;
var backendUrl = null;
var backendPort = null;
var usingEncryption = null;
var basePath = null;
var audioFolderPath = null;
var videoFolderPath = null;
@@ -140,7 +139,6 @@ var subscriptionsCheckInterval = null;
var archivePath = path.join(__dirname, 'appdata', 'archives');
// other needed values
var options = null; // encryption options
var url_domain = null;
var updaterStatus = null;
@@ -256,18 +254,10 @@ async function startServer() {
// set config to port
await setPortItemFromENV();
}
if (usingEncryption)
{
https.createServer(options, app).listen(backendPort, function() {
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on port ${backendPort} - using SSL`);
});
}
else
{
app.listen(backendPort,function(){
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`);
});
}
app.listen(backendPort,function(){
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`);
});
}
async function restartServer() {
@@ -504,6 +494,43 @@ async function getLatestVersion() {
});
}
async function killAllDownloads() {
return new Promise(resolve => {
ps.lookup({
command: 'youtube-dl',
}, function(err, resultList ) {
if (err) {
// failed to get list of processes
logger.error('Failed to get a list of running youtube-dl processes.');
logger.error(err);
resolve({
details: err,
success: false
});
}
// processes that contain the string 'youtube-dl' in the name will be looped
resultList.forEach(function( process ){
if (process) {
ps.kill(process.pid, 'SIGKILL', function( err ) {
if (err) {
// failed to kill, process may have ended on its own
logger.warn(`Failed to kill process with PID ${process.pid}`);
logger.warn(err);
}
else {
logger.verbose(`Process ${process.pid} has been killed!`);
}
});
}
});
resolve({
success: true
});
});
});
}
async function setPortItemFromENV() {
return new Promise(resolve => {
config_api.setConfigItem('ytdl_port', backendPort.toString());
@@ -548,6 +575,8 @@ async function loadConfig() {
}, subscriptionsCheckInterval * 1000);
}
db_api.importUnregisteredFiles();
// check migrations
await checkMigrations();
@@ -565,7 +594,6 @@ async function loadConfig() {
function loadConfigValues() {
url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200';
backendPort = config_api.getConfigItem('ytdl_port');
usingEncryption = config_api.getConfigItem('ytdl_use_encryption');
audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
@@ -580,20 +608,6 @@ function loadConfigValues() {
customDownloadingAgent = null;
}
if (usingEncryption)
{
var certFilePath = path.resolve(config_api.getConfigItem('ytdl_cert_file_path'));
var keyFilePath = path.resolve(config_api.getConfigItem('ytdl_key_file_path'));
var certKeyFile = fs.readFileSync(keyFilePath);
var certFile = fs.readFileSync(certFilePath);
options = {
key: certKeyFile,
cert: certFile
};
}
// empty url defaults to default URL
if (!url || url === '') url = 'http://example.com'
url_domain = new URL(url);
@@ -693,7 +707,7 @@ function generateEnvVarConfigItem(key) {
function getMp3s() {
let mp3s = [];
var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
var files = utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
for (let i = 0; i < files.length; i++) {
let file = files[i];
var file_path = file.substring(audioFolderPath.length, file.length);
@@ -722,7 +736,7 @@ function getMp3s() {
function getMp4s(relative_path = true) {
let mp4s = [];
var files = recFindByExt(videoFolderPath, 'mp4');
var files = utils.recFindByExt(videoFolderPath, 'mp4');
for (let i = 0; i < files.length; i++) {
let file = files[i];
var file_path = file.substring(videoFolderPath.length, file.length);
@@ -881,14 +895,16 @@ async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvid
}
async function deleteAudioFile(name, blacklistMode = false) {
async function deleteAudioFile(name, customPath = null, blacklistMode = false) {
return new Promise(resolve => {
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
var jsonPath = path.join(audioFolderPath,name+'.mp3.info.json');
var altJSONPath = path.join(audioFolderPath,name+'.info.json');
var audioFilePath = path.join(audioFolderPath,name+'.mp3');
let filePath = customPath ? customPath : audioFolderPath;
var jsonPath = path.join(filePath,name+'.mp3.info.json');
var altJSONPath = path.join(filePath,name+'.info.json');
var audioFilePath = path.join(filePath,name+'.mp3');
var thumbnailPath = path.join(filePath,name+'.webp');
var altThumbnailPath = path.join(filePath,name+'.jpg');
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
audioFilePath = path.join(__dirname, audioFilePath);
@@ -928,7 +944,7 @@ async function deleteAudioFile(name, blacklistMode = false) {
// get ID from JSON
var jsonobj = utils.getJSONMp3(name, audioFolderPath);
var jsonobj = utils.getJSONMp3(name, filePath);
let id = null;
if (jsonobj) id = jsonobj.id;
@@ -964,10 +980,12 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
return new Promise(resolve => {
let filePath = customPath ? customPath : videoFolderPath;
var jsonPath = path.join(filePath,name+'.info.json');
var altJSONPath = path.join(filePath,name+'.mp4.info.json');
var videoFilePath = path.join(filePath,name+'.mp4');
var thumbnailPath = path.join(filePath,name+'.webp');
var altThumbnailPath = path.join(filePath,name+'.jpg');
jsonPath = path.join(__dirname, jsonPath);
videoFilePath = path.join(__dirname, videoFilePath);
@@ -1005,7 +1023,7 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
// get ID from JSON
var jsonobj = utils.getJSONMp4(name, videoFolderPath);
var jsonobj = utils.getJSONMp4(name, filePath);
let id = null;
if (jsonobj) id = jsonobj.id;
@@ -1037,88 +1055,6 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
});
}
function recFindByExt(base,ext,files,result)
{
files = files || fs.readdirSync(base)
result = result || []
files.forEach(
function (file) {
var newbase = path.join(base,file)
if ( fs.statSync(newbase).isDirectory() )
{
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
}
else
{
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
{
result.push(newbase)
}
}
}
)
return result
}
/*
function registerFileDB(file_path, type, multiUserMode = null) {
const file_id = file_path.substring(0, file_path.length-4);
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
return false;
}
// 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 (multiUserMode) {
auth_api.registerUserFile(multiUserMode.user, file_object, type);
} else if (type === 'audio' || type === 'video') {
// remove existing video if overwriting
db.get(`files.${type}`)
.remove({
path: file_object['path']
}).write();
db.get(`files.${type}`)
.push(file_object)
.write();
} else if (type == 'subscription') {
}
return file_object['uid'];
}
function generateFileObject(id, type, customPath = null) {
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 = 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;
}
*/
// replaces .webm with appropriate extension
function getTrueFileName(unfixed_path, type) {
let fixed_path = unfixed_path;
@@ -1194,6 +1130,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
options.customFileFolderPath = fileFolderPath;
}
options.downloading_method = 'exec';
const downloadConfig = await generateArgs(url, type, options);
// adds download to download helper
@@ -1209,12 +1146,34 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
type: type,
percent_complete: 0,
is_playlist: url.includes('playlist'),
timestamp_start: Date.now()
timestamp_start: Date.now(),
filesize: null
};
const download = downloads[session][download_uid];
updateDownloads();
// get video info prior to download
const info = await getVideoInfoByURL(url, downloadConfig, download);
if (!info) {
resolve(false);
return;
} else {
// store info in download for future use
if (Array.isArray(info)) {
download['fileNames'] = [];
for (let info_obj of info) download['fileNames'].push(info_obj['_filename']);
} else {
download['_filename'] = info['_filename'];
}
download['filesize'] = utils.getExpectedFileSize(info);
}
const download_checker = setInterval(() => checkDownloadPercent(download), 1000);
// download file
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
clearInterval(download_checker); // stops the download checker from running as the download finished (or errored)
download['downloading'] = false;
download['timestamp_end'] = Date.now();
var file_uid = null;
@@ -1227,7 +1186,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
download['error'] = err.stderr;
updateDownloads();
resolve(false);
throw err;
return;
} else if (output) {
if (output.length === 0 || output[0].length === 0) {
download['error'] = 'No output. Check if video already exists in your archive.';
@@ -1329,6 +1288,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
options.customFileFolderPath = fileFolderPath;
}
options.downloading_method = 'normal';
const downloadConfig = await generateArgs(url, type, options);
// adds download to download helper
@@ -1469,24 +1429,24 @@ async function generateArgs(url, type, options) {
var youtubePassword = options.youtubePassword;
let downloadConfig = null;
let qualityPath = (is_audio && !options.skip_audio_args) ? '-f bestaudio' :'-f best[ext=mp4]';
let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4'];
const is_youtube = url.includes('youtu');
if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format
qualityPath = null;
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
qualityPath = '-f bestvideo+bestaudio'
qualityPath = ['-f', 'bestvideo+bestaudio']
}
if (customArgs) {
downloadConfig = customArgs.split(',,');
} else {
if (customQualityConfiguration) {
qualityPath = `-f ${customQualityConfiguration}`;
qualityPath = ['-f', customQualityConfiguration];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = `-f '(mp4)[height=${selectedHeight}]'`;
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (maxBitrate && is_audio) {
qualityPath = `--audio-quality ${maxBitrate}`
qualityPath = ['--audio-quality', maxBitrate]
}
if (customOutput) {
@@ -1495,7 +1455,7 @@ async function generateArgs(url, type, options) {
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
}
if (qualityPath) downloadConfig.push(qualityPath);
if (qualityPath && options.downloading_method === 'exec') downloadConfig.push(...qualityPath);
if (is_audio && !options.skip_audio_args) {
downloadConfig.push('-x');
@@ -1547,6 +1507,10 @@ async function generateArgs(url, type, options) {
downloadConfig.push('--download-archive', merged_path);
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
if (globalArgs && globalArgs !== '') {
// adds global args
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
@@ -1559,11 +1523,36 @@ async function generateArgs(url, type, options) {
}
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
// downloadConfig.map((arg) => `"${arg}"`);
resolve(downloadConfig);
});
}
async function getVideoInfoByURL(url, args = [], download = null) {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
new_args.splice(archiveArgIndex, 2);
}
// actually get info
youtubedl.getInfo(url, new_args, (err, output) => {
if (output) {
resolve(output);
} else {
logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`);
if (download) {
download['error'] = `Failed pre-check for video info: ${err}`;
updateDownloads();
}
resolve(null);
}
});
});
}
// currently only works for single urls
async function getUrlInfos(urls) {
let startDate = Date.now();
@@ -1621,47 +1610,36 @@ function updateDownloads() {
db.assign({downloads: downloads}).write();
}
/*
function checkDownloads() {
for (let [session_id, session_downloads] of Object.entries(downloads)) {
for (let [download_uid, download_obj] of Object.entries(session_downloads)) {
if (download_obj && !download_obj['complete'] && !download_obj['error']
&& download_obj.timestamp_start > timestamp_server_start) {
// download is still running (presumably)
download_obj.percent_complete = getDownloadPercent(download_obj);
}
}
}
}
*/
function checkDownloadPercent(download) {
/*
This is more of an art than a science, we're just selecting files that start with the file name,
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
function getDownloadPercent(download_obj) {
if (!download_obj.final_size) {
if (fs.existsSync(download_obj.expected_json_path)) {
const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8'));
let calculated_filesize = null;
if (file_json['format_id']) {
calculated_filesize = 0;
const formats_used = file_json['format_id'].split('+');
for (let i = 0; i < file_json['formats'].length; i++) {
if (formats_used.includes(file_json['formats'][i]['format_id'])) {
calculated_filesize += file_json['formats'][i]['filesize'];
}
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
be divided by the "total expected bytes."
*/
// assume it's a playlist for logic reasons
const fileNames = Array.isArray(download['fileNames']) ? download['fileNames']
: [path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)))];
const resulting_file_size = download['filesize'];
let sum_size = 0;
let glob_str = '';
glob(`{${fileNames.join(',')}, }*`, (err, files) => {
files.forEach(file => {
try {
const file_stats = fs.statSync(file);
if (file_stats && file_stats.size) {
sum_size += file_stats.size;
}
} catch (e) {
}
download_obj.final_size = calculated_filesize;
} else {
console.log('could not find json file');
}
}
if (fs.existsSync(download_obj.expected_path)) {
const stats = fs.statSync(download_obj.expected_path);
const size = stats.size;
return (size / download_obj.final_size)*100;
} else {
console.log('could not find file');
return 0;
}
});
download['percent_complete'] = (sum_size/resulting_file_size * 100).toFixed(2);
updateDownloads();
});
}
// youtube-dl functions
@@ -1823,7 +1801,8 @@ const optionalJwt = function (req, res, next) {
const uuid = using_body ? req.body.uuid : req.query.uuid;
const uid = using_body ? req.body.uid : req.query.uid;
const type = using_body ? req.body.type : req.query.type;
const is_shared = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true) : auth_api.getUserPlaylist(uuid, req.query.id, null, true);
const file = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, req.query.id, null, true);
const is_shared = file ? file['sharingEnabled'] : false;
if (is_shared) {
req.can_watch = true;
return next();
@@ -1849,7 +1828,7 @@ app.get('/api/config', function(req, res) {
});
});
app.post('/api/setConfig', function(req, res) {
app.post('/api/setConfig', optionalJwt, function(req, res) {
let new_config_file = req.body.new_config_file;
if (new_config_file && new_config_file['YoutubeDLMaterial']) {
let success = config_api.setConfigFile(new_config_file);
@@ -1864,27 +1843,6 @@ app.post('/api/setConfig', function(req, res) {
});
app.get('/api/using-encryption', function(req, res) {
res.send(usingEncryption);
});
app.post('/api/logs', async function(req, res) {
let logs = null;
let lines = req.body.lines;
logs_path = path.join('appdata', 'logs', 'combined.log')
if (fs.existsSync(logs_path)) {
if (lines) logs = await read_last_lines.read(logs_path, lines);
else logs = fs.readFileSync(logs_path, 'utf8');
}
else
logger.error(`Failed to find logs file at the expected location: ${logs_path}`)
res.send({
logs: logs,
success: !!logs
});
});
app.post('/api/tomp3', optionalJwt, async function(req, res) {
var url = req.body.url;
var options = {
@@ -1903,7 +1861,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
const is_playlist = url.includes('playlist');
let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID);
else
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
@@ -1912,11 +1870,10 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
} else {
res.sendStatus(500);
}
res.end("yes");
});
app.post('/api/tomp4', optionalJwt, async function(req, res) {
req.setTimeout(0); // remove timeout in case of long videos
var url = req.body.url;
var options = {
customArgs: req.body.customArgs,
@@ -1934,7 +1891,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
const is_playlist = url.includes('playlist');
let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID);
else
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
@@ -1943,50 +1900,11 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
} else {
res.sendStatus(500);
}
res.end("yes");
});
// gets the status of the mp3 file that's being downloaded
app.post('/api/fileStatusMp3', function(req, res) {
var name = decodeURIComponent(req.body.name + "");
var exists = "";
var fullpath = audioFolderPath + name + ".mp3";
if (fs.existsSync(fullpath)) {
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
}
else
{
var percent = 0;
var size = getFileSizeMp3(name);
var downloaded = getAmountDownloadedMp3(name);
if (size > 0)
percent = downloaded/size;
exists = ["failed", getFileSizeMp3(name), percent];
}
//logger.info(exists + " " + name);
res.send(exists);
res.end("yes");
});
// gets the status of the mp4 file that's being downloaded
app.post('/api/fileStatusMp4', function(req, res) {
var name = decodeURIComponent(req.body.name);
var exists = "";
var fullpath = videoFolderPath + name + ".mp4";
if (fs.existsSync(fullpath)) {
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
} else {
var percent = 0;
var size = getFileSizeMp4(name);
var downloaded = getAmountDownloadedMp4(name);
if (size > 0)
percent = downloaded/size;
exists = ["failed", getFileSizeMp4(name), percent];
}
//logger.info(exists + " " + name);
res.send(exists);
res.end("yes");
app.post('/api/killAllDownloads', optionalJwt, async function(req, res) {
const result_obj = await killAllDownloads();
res.send(result_obj);
});
// gets all download mp3s
@@ -2001,6 +1919,14 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) {
playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
}
mp3s = JSON.parse(JSON.stringify(mp3s));
// add thumbnails if present
mp3s.forEach(mp3 => {
if (mp3['thumbnailPath'] && fs.existsSync(mp3['thumbnailPath']))
mp3['thumbnailBlob'] = fs.readFileSync(mp3['thumbnailPath']);
});
res.send({
mp3s: mp3s,
playlists: playlists
@@ -2020,6 +1946,14 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) {
playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
}
mp4s = JSON.parse(JSON.stringify(mp4s));
// add thumbnails if present
mp4s.forEach(mp4 => {
if (mp4['thumbnailPath'] && fs.existsSync(mp4['thumbnailPath']))
mp4['thumbnailBlob'] = fs.readFileSync(mp4['thumbnailPath']);
});
res.send({
mp4s: mp4s,
playlists: playlists
@@ -2064,6 +1998,60 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
}
});
app.post('/api/getAllFiles', optionalJwt, function (req, res) {
// these are returned
let files = [];
let playlists = [];
let subscription_files = [];
let videos = null;
let audios = null;
let audio_playlists = null;
let video_playlists = null;
let subscriptions = subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null);
// get basic info depending on multi-user mode being enabled
if (req.isAuthenticated()) {
videos = auth_api.getUserVideos(req.user.uid, 'video');
audios = auth_api.getUserVideos(req.user.uid, 'audio');
audio_playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
video_playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
} else {
videos = db.get('files.audio').value();
audios = db.get('files.video').value();
audio_playlists = db.get('playlists.audio').value();
video_playlists = db.get('playlists.video').value();
}
files = videos.concat(audios);
playlists = video_playlists.concat(audio_playlists);
// loop through subscriptions and add videos
for (let i = 0; i < subscriptions.length; i++) {
sub = subscriptions[i];
if (!sub.videos) continue;
// add sub id for UI
for (let j = 0; j < sub.videos.length; j++) {
sub.videos[j].sub_id = sub.id;
}
files = files.concat(sub.videos);
}
files = JSON.parse(JSON.stringify(files));
// add thumbnails if present
files.forEach(file => {
if (file['thumbnailPath'] && fs.existsSync(file['thumbnailPath']))
file['thumbnailBlob'] = fs.readFileSync(file['thumbnailPath']);
});
res.send({
files: files,
playlists: playlists
});
});
// video sharing
app.post('/api/enableSharing', optionalJwt, function(req, res) {
var type = req.body.type;
@@ -2260,7 +2248,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/');
let files;
try {
files = recFindByExt(appended_base_path, 'mp4');
files = utils.recFindByExt(appended_base_path, 'mp4');
} catch(e) {
files = null;
logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
@@ -2325,6 +2313,16 @@ app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) =>
});
});
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
let updated_sub = req.body.subscription;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
let success = subscriptions_api.updateSubscription(updated_sub, user_uid);
res.send({
success: success
});
});
app.post('/api/getAllSubscriptions', optionalJwt, async (req, res) => {
let user_uid = req.isAuthenticated() ? req.user.uid : null;
@@ -2341,13 +2339,16 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let fileNames = req.body.fileNames;
let type = req.body.type;
let thumbnailURL = req.body.thumbnailURL;
let duration = req.body.duration;
let new_playlist = {
'name': playlistName,
name: playlistName,
fileNames: fileNames,
id: shortid.generate(),
thumbnailURL: thumbnailURL,
type: type
type: type,
registered: Date.now(),
duration: duration
};
if (req.isAuthenticated()) {
@@ -2473,11 +2474,10 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
var wasDeleted = false;
if (fs.existsSync(fullpath))
{
deleteAudioFile(name, blacklistMode);
deleteAudioFile(name, null, blacklistMode);
db.get('files.audio').remove({uid: uid}).write();
wasDeleted = true;
res.send(wasDeleted);
res.end("yes");
} else if (audio_obj) {
db.get('files.audio').remove({uid: uid}).write();
wasDeleted = true;
@@ -2509,7 +2509,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
db.get('files.video').remove({uid: uid}).write();
// wasDeleted = true;
res.send(wasDeleted);
res.end("yes");
} else if (video_obj) {
db.get('files.video').remove({uid: uid}).write();
wasDeleted = true;
@@ -2517,7 +2516,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
} else {
wasDeleted = false;
res.send(wasDeleted);
res.end("yes");
}
});
@@ -2538,9 +2536,10 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
let base_path = fileFolderPath;
let usersFileFolder = null;
if (req.isAuthenticated()) {
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
if (multiUserMode && req.body.uuid) {
usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
base_path = path.join(usersFileFolder, req.user.uid, type);
base_path = path.join(usersFileFolder, req.body.uuid, type);
}
if (!subscriptionName) {
file = path.join(__dirname, base_path, fileNames + ext);
@@ -2551,7 +2550,7 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
file = path.join(__dirname, basePath, (subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
file = path.join(__dirname, basePath, (subscriptionPlaylist === true || subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
}
} else {
for (let i = 0; i < fileNames.length; i++) {
@@ -2640,49 +2639,6 @@ app.post('/api/updateServer', async (req, res) => {
});
// Pin API calls
app.post('/api/isPinSet', async (req, res) => {
let stored_pin = db.get('pin_md5').value();
let is_set = false;
if (!stored_pin || stored_pin.length === 0) {
} else {
is_set = true;
}
res.send({
is_set: is_set
});
});
app.post('/api/setPin', async (req, res) => {
let unhashed_pin = req.body.pin;
let hashed_pin = md5(unhashed_pin);
db.set('pin_md5', hashed_pin).write();
res.send({
success: true
});
});
app.post('/api/checkPin', async (req, res) => {
let input_pin = req.body.input_pin;
let input_pin_md5 = md5(input_pin);
let stored_pin = db.get('pin_md5').value();
let successful = false;
if (input_pin_md5 === stored_pin) {
successful = true;
}
res.send({
success: successful
});
});
// API Key API calls
app.post('/api/generateNewAPIKey', function (req, res) {
@@ -2868,6 +2824,42 @@ app.get('/api/audio/:id', optionalJwt, function(req , res){
res.send({success: success, downloads: downloads});
});
// logs management
app.post('/api/logs', async function(req, res) {
let logs = null;
let lines = req.body.lines;
logs_path = path.join('appdata', 'logs', 'combined.log')
if (fs.existsSync(logs_path)) {
if (lines) logs = await read_last_lines.read(logs_path, lines);
else logs = fs.readFileSync(logs_path, 'utf8');
}
else
logger.error(`Failed to find logs file at the expected location: ${logs_path}`)
res.send({
logs: logs,
success: !!logs
});
});
app.post('/api/clearAllLogs', async function(req, res) {
logs_path = path.join('appdata', 'logs', 'combined.log');
logs_err_path = path.join('appdata', 'logs', 'error.log');
let success = false;
try {
fs.writeFileSync(logs_path, '');
fs.writeFileSync(logs_err_path, '');
success = true;
} catch(e) {
logger.error(e);
}
res.send({
success: success
});
});
app.post('/api/getVideoInfos', async (req, res) => {
let fileNames = req.body.fileNames;
let urlMode = !!req.body.urlMode;
@@ -2894,8 +2886,7 @@ app.post('/api/auth/register'
, optionalJwt
, auth_api.registerUser);
app.post('/api/auth/login'
, auth_api.passport.authenticate('local', {})
, auth_api.passport.authorize('local')
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
, auth_api.generateJWT
, auth_api.returnAuthResponse
);

View File

@@ -4,17 +4,14 @@
"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
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -22,7 +19,6 @@
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {
@@ -38,12 +34,19 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",
"allow_registration": true
"allow_registration": true,
"auth_method": "internal",
"ldap_config": {
"url": "ldap://localhost:389",
"bindDN": "cn=root",
"bindCredentials": "secret",
"searchBase": "ou=passport-ldapauth",
"searchFilter": "(uid={{username}})"
}
},
"Advanced": {
"use_default_downloading_agent": true,
@@ -51,6 +54,7 @@
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"jwt_expiration": 86400,
"logger_level": "info"
}
}

View File

@@ -1,57 +0,0 @@
{
"YoutubeDLMaterial": {
"Host": {
"url": "https://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": true,
"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"
}
}
}

View File

@@ -5,10 +5,11 @@ var subscriptions_api = require('../subscriptions')
const fs = require('fs-extra');
var jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4');
var bcrypt = require('bcrypt');
var bcrypt = require('bcryptjs');
var LocalStrategy = require('passport-local').Strategy;
var LdapStrategy = require('passport-ldapauth');
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
@@ -29,7 +30,7 @@ exports.initialize = function(input_users_db, input_logger) {
************************/
saltRounds = 10;
JWT_EXPIRATION = (60 * 60); // one hour
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
SERVER_SECRET = null;
if (users_db.get('jwt_secret').value()) {
@@ -90,24 +91,7 @@ exports.registerUser = function(req, res) {
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: []
};
let new_user = generateUserObject(userid, username, hash);
// check if user exists
if (users_db.get('users').find({uid: userid}).value()) {
// user id is taken!
@@ -153,52 +137,43 @@ exports.registerUser = function(req, res) {
exports.passport.use(new LocalStrategy({
usernameField: 'userid',
usernameField: 'username',
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.auth_method && user.auth_method !== 'internal') { 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;
var getLDAPConfiguration = function(req, callback) {
const ldap_config = config_api.getConfigItem('ytdl_ldap_config');
const opts = {server: ldap_config};
callback(null, opts);
};
exports.passport.use(new LdapStrategy(getLDAPConfiguration,
function(user, done) {
// check if ldap auth is enabled
const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap';
if (!ldap_enabled) return done(null, false);
const user_uid = user.uid;
let db_user = users_db.get('users').find({uid: user_uid}).value();
if (!db_user) {
// generate DB user
let new_user = generateUserObject(user_uid, user_uid, null, 'ldap');
users_db.get('users').push(new_user).write();
db_user = new_user;
logger.verbose(`Generated new user ${user_uid} using LDAP`);
}
return done(null, db_user);
}
));
*/
/*************************************************************
* 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
@@ -538,3 +513,26 @@ function getToken(queryParams) {
return null;
}
};
function generateUserObject(userid, username, hash, auth_method = 'internal') {
let new_user = {
name: username,
uid: userid,
passhash: auth_method === 'internal' ? hash : null,
files: {
audio: [],
video: []
},
playlists: {
audio: [],
video: []
},
subscriptions: [],
created: Date.now(),
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
permissions: [],
permission_overrides: [],
auth_method: auth_method
};
return new_user;
}

View File

@@ -157,7 +157,7 @@ function setConfigItems(items) {
function globalArgsRequiresSafeDownload() {
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
return failedArgs && failedArgs.length > 0;
}
@@ -181,17 +181,14 @@ DEFAULT_CONFIG = {
"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
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -199,7 +196,6 @@ DEFAULT_CONFIG = {
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {
@@ -215,12 +211,19 @@ DEFAULT_CONFIG = {
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",
"allow_registration": true
"allow_registration": true,
"auth_method": "internal",
"ldap_config": {
"url": "ldap://localhost:389",
"bindDN": "cn=root",
"bindCredentials": "secret",
"searchBase": "ou=passport-ldapauth",
"searchFilter": "(uid={{username}})"
}
},
"Advanced": {
"use_default_downloading_agent": true,
@@ -228,6 +231,7 @@ DEFAULT_CONFIG = {
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"jwt_expiration": 86400,
"logger_level": "info"
}
}

View File

@@ -9,20 +9,6 @@ let CONFIG_ITEMS = {
'path': 'YoutubeDLMaterial.Host.port'
},
// Encryption
'ytdl_use_encryption': {
'key': 'ytdl_use_encryption',
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
},
'ytdl_cert_file_path': {
'key': 'ytdl_cert_file_path',
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
},
'ytdl_key_file_path': {
'key': 'ytdl_key_file_path',
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
},
// Downloader
'ytdl_audio_folder_path': {
'key': 'ytdl_audio_folder_path',
@@ -44,6 +30,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
},
'ytdl_include_thumbnail': {
'key': 'ytdl_include_thumbnail',
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
},
'ytdl_include_metadata': {
'key': 'ytdl_include_metadata',
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
},
// Extra
'ytdl_title_top': {
@@ -66,10 +60,6 @@ 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'
@@ -120,10 +110,6 @@ let CONFIG_ITEMS = {
'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': {
@@ -134,6 +120,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_registration',
'path': 'YoutubeDLMaterial.Users.allow_registration'
},
'ytdl_auth_method': {
'key': 'ytdl_auth_method',
'path': 'YoutubeDLMaterial.Users.auth_method'
},
'ytdl_ldap_config': {
'key': 'ytdl_ldap_config',
'path': 'YoutubeDLMaterial.Users.ldap_config'
},
// Advanced
'ytdl_use_default_downloading_agent': {
@@ -156,6 +150,10 @@ let CONFIG_ITEMS = {
'key': 'ytdl_use_cookies',
'path': 'YoutubeDLMaterial.Advanced.use_cookies'
},
'ytdl_jwt_expiration': {
'key': 'ytdl_jwt_expiration',
'path': 'YoutubeDLMaterial.Advanced.jwt_expiration'
},
'ytdl_logger_level': {
'key': 'ytdl_logger_level',
'path': 'YoutubeDLMaterial.Advanced.logger_level'
@@ -174,5 +172,5 @@ AVAILABLE_PERMISSIONS = [
module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.0'
CURRENT_VERSION: 'v4.1'
}

View File

@@ -16,6 +16,7 @@ function initialize(input_db, input_users_db, input_logger) {
}
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
let db_path = 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) {
@@ -25,45 +26,47 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
if (!sub) {
if (multiUserMode) {
const user_uid = multiUserMode.user;
db_path = users_db.get('users').find({uid: user_uid}).get(`files.${type}`);
} else {
db_path = db.get(`files.${type}`)
}
} else {
if (multiUserMode) {
const user_uid = multiUserMode.user;
db_path = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).get('videos');
} else {
db_path = db.get('subscriptions').find({id: sub.id}).get('videos');
}
}
const file_uid = registerFileDBManual(db_path, file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_uid;
}
function registerFileDBManual(db_path, file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
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();
}
// remove duplicate(s)
db_path.remove({path: file_object['path']}).write();
// add new file to db
db_path.push(file_object).write();
return file_object['uid'];
}
@@ -112,8 +115,93 @@ function getAppendedBasePathSub(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
async function importUnregisteredFiles() {
let dirs_to_check = [];
let subscriptions_to_check = [];
const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode
const multi_user_mode = config_api.getConfigItem('ytdl_multi_user_mode');
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptions_enabled = config_api.getConfigItem('ytdl_allow_subscriptions');
if (multi_user_mode) {
let users = users_db.get('users').value();
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (subscriptions_enabled) subscriptions_to_check = subscriptions_to_check.concat(users[i]['subscriptions']);
// add user's audio dir to check list
dirs_to_check.push({
basePath: path.join(usersFileFolder, user.uid, 'audio'),
dbPath: users_db.get('users').find({uid: user.uid}).get('files.audio'),
type: 'audio'
});
// add user's video dir to check list
dirs_to_check.push({
basePath: path.join(usersFileFolder, user.uid, 'video'),
dbPath: users_db.get('users').find({uid: user.uid}).get('files.video'),
type: 'video'
});
}
} else {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const subscriptions = db.get('subscriptions').value();
if (subscriptions_enabled && subscriptions) subscriptions_to_check = subscriptions_to_check.concat(subscriptions);
// add audio dir to check list
dirs_to_check.push({
basePath: audioFolderPath,
dbPath: db.get('files.audio'),
type: 'audio'
});
// add video dir to check list
dirs_to_check.push({
basePath: videoFolderPath,
dbPath: db.get('files.video'),
type: 'video'
});
}
// add subscriptions to check list
for (let i = 0; i < subscriptions_to_check.length; i++) {
let subscription_to_check = subscriptions_to_check[i];
if (!subscription_to_check.name) {
// TODO: Remove subscription as it'll never complete
continue;
}
dirs_to_check.push({
basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
dbPath: multi_user_mode ? users_db.get('users').find({uid: subscription_to_check.user_uid}).get('subscriptions').find({id: subscription_to_check.id}).get('videos')
: db.get('subscriptions').find({id: subscription_to_check.id}).get('videos'),
type: subscription_to_check.type
});
}
// run through check list and check each file to see if it's missing from the db
dirs_to_check.forEach(dir_to_check => {
// recursively get all files in dir's path
const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
files.forEach(file => {
// check if file exists in db, if not add it
const file_is_registered = !!(dir_to_check.dbPath.find({id: file.id}).value())
if (!file_is_registered) {
// add additional info
registerFileDBManual(dir_to_check.dbPath, file);
logger.verbose(`Added discovered file to the database: ${file.id}`);
}
});
});
}
module.exports = {
initialize: initialize,
registerFileDB: registerFileDB,
updatePlaylist: updatePlaylist
updatePlaylist: updatePlaylist,
importUnregisteredFiles: importUnregisteredFiles
}

View File

@@ -10,7 +10,7 @@ fi
# chown current working directory to current user
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' +
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
exec su-exec "$UID:$GID" "$0" "$@"
fi

View File

@@ -4,6 +4,89 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz",
"integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==",
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz",
"integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==",
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/ldapjs": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.9.tgz",
"integrity": "sha512-3PvY7Drp1zoLbcGlothCAkoc5o6Jp9KvUPwHadlHyKp3yPvyeIh7w2zQc9UXMzgDRkoeGXUEODtbEs5XCh9ZyA==",
"requires": {
"@types/node": "*"
}
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
},
"@types/node": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz",
"integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA=="
},
"@types/passport": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz",
"integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==",
"requires": {
"@types/express": "*"
}
},
"@types/qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ=="
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/serve-static": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
"integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -50,6 +133,11 @@
"color-convert": "^1.9.0"
}
},
"any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
},
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@@ -64,11 +152,6 @@
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"archiver": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz",
@@ -126,31 +209,6 @@
}
}
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -194,6 +252,14 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -204,15 +270,6 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"bcrypt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz",
"integrity": "sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ==",
"requires": {
"node-addon-api": "^2.0.0",
"node-pre-gyp": "0.14.0"
}
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -221,6 +278,11 @@
"tweetnacl": "^0.14.3"
}
},
"bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
@@ -344,6 +406,17 @@
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
},
"bunyan": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz",
"integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.19.3",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@@ -434,11 +507,6 @@
"readdirp": "~3.3.0"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
@@ -449,11 +517,6 @@
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM="
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
@@ -614,10 +677,10 @@
"xdg-basedir": "^3.0.0"
}
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
"connected-domain": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/connected-domain/-/connected-domain-1.0.0.tgz",
"integrity": "sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM="
},
"content-disposition": {
"version": "0.5.3",
@@ -718,11 +781,6 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -733,11 +791,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"diagnostics": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
@@ -788,6 +841,15 @@
"is-obj": "^1.0.0"
}
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
"optional": true,
"requires": {
"nan": "^2.14.0"
}
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -1051,14 +1113,6 @@
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1081,54 +1135,6 @@
"rimraf": "2"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
@@ -1228,11 +1234,6 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"hashish": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz",
@@ -1301,14 +1302,6 @@
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
},
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
},
"import-lazy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
@@ -1590,15 +1583,81 @@
}
}
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
},
"ldapauth-fork": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-4.3.3.tgz",
"integrity": "sha512-x76VpQ5ZqkwAJmqwcD6KIwDiNEbgIGIPGwC/eA17e1dxWhlTx36w0DlLOFwjTuZ2iuaLTsZsUprlVqvSlwc/1Q==",
"requires": {
"@types/ldapjs": "^1.0.0",
"@types/node": "*",
"bcryptjs": "^2.4.0",
"ldapjs": "^1.0.2",
"lru-cache": "^5.1.1"
},
"dependencies": {
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"yallist": "^3.0.2"
}
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"bunyan": "^1.8.3",
"dashdash": "^1.14.0",
"dtrace-provider": "~0.8",
"ldap-filter": "0.2.2",
"once": "^1.4.0",
"vasync": "^1.6.4",
"verror": "^1.8.1"
},
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
}
}
},
"listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.defaults": {
"version": "4.2.0",
@@ -1782,30 +1841,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
},
"dependencies": {
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
@@ -1814,6 +1849,12 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==",
"optional": true
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1859,50 +1900,77 @@
}
}
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
}
}
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"requires": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
"optional": true
},
"nanoid": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
},
"needle": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-addon-api": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-id3": {
"version": "0.1.16",
@@ -1912,34 +1980,6 @@
"iconv-lite": "^0.4.15"
}
},
"node-pre-gyp": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz",
"integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4.4.2"
},
"dependencies": {
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
}
}
},
"nodemon": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz",
@@ -1985,29 +2025,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -2016,22 +2033,6 @@
"path-key": "^3.0.0"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -2076,25 +2077,6 @@
"mimic-fn": "^2.1.0"
}
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"p-finally": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
@@ -2166,6 +2148,17 @@
}
}
},
"passport-ldapauth": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-2.1.4.tgz",
"integrity": "sha512-VeVL69ZK+cpJe0DKMSGuwcf7k+V4dr0U0Y7ZhXL785pcRb5gRA6qYZfIH+XTsAzwqTK9l0Dn3Ds4weOZ1jKkLQ==",
"requires": {
"@types/node": "*",
"@types/passport": "^1.0.0",
"ldapauth-fork": "^4.3.2",
"passport-strategy": "^1.0.0"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
@@ -2219,6 +2212,11 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
@@ -2243,6 +2241,14 @@
"ipaddr.js": "1.9.1"
}
},
"ps-node": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/ps-node/-/ps-node-0.1.6.tgz",
"integrity": "sha1-mvZ6mdex0BMuUaUDCZ04qNKs4sM=",
"requires": {
"table-parser": "^0.1.3"
}
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -2311,6 +2317,14 @@
"strip-json-comments": "~2.0.1"
}
},
"read-last-lines": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/read-last-lines/-/read-last-lines-1.7.2.tgz",
"integrity": "sha512-K0yUvTYAYn6qpyLJufaJ7yC6BeL23qpgZ8SKM7/fA1R1rHotCDxB/zDp9i1I2JHvexWBW6/35jkt07iiIKKp4g==",
"requires": {
"mz": "^2.7.0"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -2398,16 +2412,17 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -2459,11 +2474,6 @@
"send": "0.17.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -2603,25 +2613,12 @@
"has-flag": "^3.0.0"
}
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"table-parser": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
"integrity": "sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
},
"dependencies": {
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
"connected-domain": "^1.0.0"
}
},
"tar-stream": {
@@ -2724,6 +2721,22 @@
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"requires": {
"any-promise": "^1.0.0"
}
},
"thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
"requires": {
"thenify": ">= 3.1.0 < 4"
}
},
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
@@ -2921,6 +2934,29 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
},
"dependencies": {
"extsprintf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
}
}
}
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -2939,14 +2975,6 @@
"isexe": "^2.0.0"
}
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"widest-line": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",

View File

@@ -30,26 +30,29 @@
"dependencies": {
"archiver": "^3.1.1",
"async": "^3.1.0",
"bcrypt": "^4.0.1",
"bcryptjs": "^2.4.0",
"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",
"glob": "^7.1.6",
"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-fetch": "^2.6.1",
"node-id3": "^0.1.14",
"nodemon": "^2.0.2",
"passport": "^0.4.1",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"passport-ldapauth": "^2.1.4",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"ps-node": "^0.1.6",
"read-last-lines": "^1.7.2",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,794 +0,0 @@
@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.

View File

@@ -1,198 +0,0 @@
{
"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"
}

View File

@@ -1,200 +0,0 @@
{
"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"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,18 +0,0 @@
<!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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
!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()}([]);

View File

@@ -1 +0,0 @@
!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()}([]);

File diff suppressed because one or more lines are too long

View File

@@ -40,9 +40,9 @@ async function subscribe(sub, user_uid = null) {
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!';
if (!sub.name && url_exists) {
logger.error(`Sub with the same URL "${sub.url}" already exists -- please provide a custom name for this new subscription.`);
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists! Custom name is required.';
resolve(result_obj);
return;
}
@@ -81,7 +81,15 @@ async function getSubscriptionInfo(sub, user_uid = null) {
return new Promise(resolve => {
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1']
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
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.');
}
}
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
@@ -115,7 +123,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
}
}
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useArchive && !sub.archive) {
// must create the archive
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
@@ -158,6 +166,11 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
else
db.get('subscriptions').remove({id: id}).write();
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && fs.existsSync(appendedBasePath)) {
if (sub.archive && fs.existsSync(sub.archive)) {
@@ -184,7 +197,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
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 useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
const appendedBasePath = getAppendedBasePath(sub, basePath);
const name = file;
let retrievedID = null;
@@ -260,7 +273,7 @@ async function getVideosForSub(sub, user_uid = null) {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
let appendedBasePath = null
appendedBasePath = getAppendedBasePath(sub, basePath);
@@ -332,6 +345,10 @@ async function getVideosForSub(sub, user_uid = null) {
}
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
@@ -398,7 +415,6 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_
// 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);
}
}
@@ -417,6 +433,15 @@ function getSubscription(subID, user_uid = null) {
return db.get('subscriptions').find({id: subID}).value();
}
function updateSubscription(sub, user_uid = null) {
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
} else {
db.get('subscriptions').find({id: sub.id}).assign(sub).write();
}
return true;
}
function subExists(subID, user_uid = null) {
if (user_uid)
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
@@ -476,6 +501,7 @@ function removeIDFromArchive(archive_path, id) {
module.exports = {
getSubscription : getSubscription,
getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe,
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,

View File

@@ -19,6 +19,39 @@ function getTrueFileName(unfixed_path, type) {
return fixed_path;
}
function getDownloadedFilesByType(basePath, type) {
// return empty array if the path doesn't exist
if (!fs.existsSync(basePath)) return [];
let files = [];
const ext = type === 'audio' ? 'mp3' : 'mp4';
var located_files = recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) {
let file = located_files[i];
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
var stats = fs.statSync(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = getJSONByType(type, id, basePath);
if (!jsonobj) continue;
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)}` : null;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var size = stats.size;
var isaudio = type === 'audio';
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
files.push(file_obj);
}
return files;
}
function getJSONMp4(name, customPath, openReadPerms = false) {
var obj = null; // output
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
@@ -51,6 +84,53 @@ function getJSONMp3(name, customPath, openReadPerms = false) {
return obj;
}
function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
}
function getDownloadedThumbnail(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
let jpgPath = path.join(customPath, name + '.jpg');
let webpPath = path.join(customPath, name + '.webp');
let pngPath = path.join(customPath, name + '.png');
if (fs.existsSync(jpgPath))
return jpgPath;
else if (fs.existsSync(webpPath))
return webpPath;
else if (fs.existsSync(pngPath))
return pngPath;
else
return null;
}
function getExpectedFileSize(input_info_jsons) {
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
let expected_filesize = 0;
info_jsons.forEach(info_json => {
if (info_json['filesize']) {
expected_filesize += info_json['filesize'];
return;
}
const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0;
formats.forEach(format_id => {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
individual_expected_filesize += available_format.filesize;
}
});
});
expected_filesize += individual_expected_filesize;
});
return expected_filesize;
}
function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
@@ -73,6 +153,43 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
}
}
function deleteJSONFile(name, type, customPath = null) {
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';
let json_path = path.join(customPath, name + '.info.json');
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
function recFindByExt(base,ext,files,result)
{
files = files || fs.readdirSync(base)
result = result || []
files.forEach(
function (file) {
var newbase = path.join(base,file)
if ( fs.statSync(newbase).isDirectory() )
{
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
}
else
{
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
{
result.push(newbase)
}
}
}
)
return result
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
@@ -92,6 +209,11 @@ module.exports = {
getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4,
getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
getDownloadedFilesByType: getDownloadedFilesByType,
recFindByExt: recFindByExt,
File: File
}

View File

@@ -1,6 +0,0 @@
{
"playlists": {
"audio": [],
"video": []
}
}

View File

36
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.0.0",
"version": "4.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1404,6 +1404,21 @@
}
}
},
"@ngneat/content-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@ngneat/content-loader/-/content-loader-5.0.0.tgz",
"integrity": "sha512-XrS53rsiJoQIOy2BwmOHkvgPv0a5cTzXbbM+/3IpgezPFdNqDunpZW+AciWQffVOfgbmL+7cYp1DVb6WM15LhQ==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"@ngtools/webpack": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.1.0.tgz",
@@ -6588,6 +6603,11 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-retina": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
"integrity": "sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
@@ -9137,6 +9157,15 @@
"resolved": "https://registry.npmjs.org/ng-lazyload-image/-/ng-lazyload-image-7.1.0.tgz",
"integrity": "sha512-1fip2FdPBDRnjGyBokI/DupBxOnrKh2lbtT8X8N1oPbE3KBZXXl82VIKcK2Sx+XQD67/+VtFzlISmrgsatzYuw=="
},
"ngx-avatar": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ngx-avatar/-/ngx-avatar-4.0.0.tgz",
"integrity": "sha512-Uk40UXl26RvDy1ori9NDsGFB+f84AaxMnsIwZA6JPJK0pLcbo3F4vZTmzLZeOusOw1Qtgk5IzF630jo06keXwQ==",
"requires": {
"is-retina": "^1.0.3",
"ts-md5": "^1.2.4"
}
},
"ngx-file-drop": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-9.0.1.tgz",
@@ -13457,6 +13486,11 @@
"utf8-byte-length": "^1.0.1"
}
},
"ts-md5": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.7.tgz",
"integrity": "sha512-emODogvKGWi1KO1l9c6YxLMBn6CEH3VrH5mVPIyOtxBG52BvV4jP3GWz6bOZCz61nLgBc3ffQYE4+EHfCD+V7w=="
},
"ts-node": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.0.6.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.0.0",
"version": "4.1.0",
"license": "MIT",
"scripts": {
"ng": "ng",
@@ -30,12 +30,14 @@
"@angular/platform-browser": "^9.1.0",
"@angular/platform-browser-dynamic": "^9.1.0",
"@angular/router": "^9.1.0",
"@ngneat/content-loader": "^5.0.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",
"ngx-avatar": "^4.0.0",
"ngx-file-drop": "^9.0.1",
"ngx-videogular": "^9.0.1",
"rxjs": "^6.5.3",

View File

@@ -23,7 +23,7 @@
<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>
<button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
<mat-icon>settings</mat-icon>
<span i18n="Settings menu label">Settings</span>
</button>
@@ -38,12 +38,16 @@
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav #sidenav>
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #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)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" 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>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" 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)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
<mat-divider></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
</ng-container>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
import {PostsService} from './posts.services';
import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
@@ -21,7 +21,6 @@ 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';
@@ -31,19 +30,19 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
export class AppComponent implements OnInit, AfterViewInit {
@HostBinding('class') componentCssClass;
THEMES_CONFIG = THEMES_CONFIG;
window = window;
// config items
topBarTitle = 'Youtube Downloader';
defaultTheme = null;
allowThemeChange = null;
allowSubscriptions = false;
enableDownloadsManager = false;
// defaults to true to prevent attack
settingsPinRequired = true;
@ViewChild('sidenav') sidenav: MatSidenav;
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
@@ -72,6 +71,29 @@ export class AppComponent implements OnInit {
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
}
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.');
}
});
}
});
}
ngAfterViewInit() {
this.postsService.sidenav = this.sidenav;
}
toggleSidenav() {
this.sidenav.toggle();
}
@@ -79,7 +101,6 @@ export class AppComponent implements OnInit {
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;
@@ -90,6 +111,11 @@ export class AppComponent implements OnInit {
if (!localStorage.getItem('theme')) {
this.setTheme(themingExists ? this.defaultTheme : 'default');
}
// gets the subscriptions
if (this.allowSubscriptions) {
this.postsService.reloadSubscriptions();
}
}
// theme stuff
@@ -121,9 +147,9 @@ export class AppComponent implements OnInit {
this.postsService.setTheme(theme);
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
}
}
onSetTheme(theme, old_theme) {
onSetTheme(theme, old_theme) {
if (old_theme) {
document.body.classList.remove(old_theme);
this.overlayContainer.getContainerElement().classList.remove(old_theme);
@@ -145,24 +171,8 @@ onSetTheme(theme, old_theme) {
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.');
}
});
}
});
getSubscriptions() {
}
@@ -175,31 +185,11 @@ onSetTheme(theme, old_theme) {
}
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'

View File

@@ -51,9 +51,10 @@ import { SubscriptionComponent } from './subscription//subscription/subscription
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 { AvatarModule } from 'ngx-avatar';
import { ContentLoaderModule } from '@ngneat/content-loader';
import es from '@angular/common/locales/es';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
@@ -73,6 +74,11 @@ import { ManageRoleComponent } from './components/manage-role/manage-role.compon
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';
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
registerLocaleData(es, 'es');
@@ -95,7 +101,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
SubscriptionFileCardComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent,
AboutDialogComponent,
VideoInfoDialogComponent,
ArgModifierDialogComponent,
@@ -113,7 +118,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
ManageRoleComponent,
CookiesUploaderDialogComponent,
LogsViewerComponent,
ModifyPlaylistComponent
ModifyPlaylistComponent,
ConfirmDialogComponent,
UnifiedFileCardComponent,
RecentVideosComponent,
EditSubscriptionDialogComponent,
CustomPlaylistsComponent
],
imports: [
CommonModule,
@@ -143,7 +153,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatMenuModule,
MatDialogModule,
MatSlideToggleModule,
MatMenuModule,
MatAutocompleteModule,
MatTabsModule,
MatTooltipModule,
@@ -154,6 +163,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
DragDropModule,
ClipboardModule,
NgxFileDropModule,
AvatarModule,
ContentLoaderModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
@@ -167,8 +178,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
CreatePlaylistComponent,
SubscribeDialogComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent
SettingsComponent
],
providers: [
PostsService

View File

@@ -0,0 +1,13 @@
<div *ngIf="playlists && playlists.length > 0">
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>
</div>
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
No playlists available. Create one from your downloading files by clicking the blue plus button.
</div>
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>

View File

@@ -0,0 +1,18 @@
.add-playlist-button {
float: right;
position: relative;
bottom: 15px;
right: 15px;
}
.large-col {
max-width: 320px;
}
.medium-col {
max-width: 240px;
}
.small-col {
max-width: 240px;
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomPlaylistsComponent } from './custom-playlists.component';
describe('CustomPlaylistsComponent', () => {
let component: CustomPlaylistsComponent;
let fixture: ComponentFixture<CustomPlaylistsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CustomPlaylistsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CustomPlaylistsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,113 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
@Component({
selector: 'app-custom-playlists',
templateUrl: './custom-playlists.component.html',
styleUrls: ['./custom-playlists.component.scss']
})
export class CustomPlaylistsComponent implements OnInit {
playlists = null;
playlists_received = false;
downloading_content = {'video': {}, 'audio': {}};
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog) { }
ngOnInit(): void {
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllPlaylists();
}
});
}
getAllPlaylists() {
this.playlists_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.playlists = res['playlists'];
this.playlists_received = true;
});
}
// creating a playlist
openCreatePlaylistDialog() {
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: {
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.getAllPlaylists();
this.postsService.openSnackBar('Successfully created playlist!', '');
} else if (result === false) {
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
}
});
}
goToPlaylist(info_obj) {
const playlist = info_obj.file;
const playlistID = playlist.id;
const type = playlist.type;
if (playlist) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
}
} else {
// playlist not found
console.error(`Playlist with ID ${playlistID} not found!`);
}
}
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
if (playlistID) { this.downloading_content[type][playlistID] = false };
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
});
}
deletePlaylist(args) {
const playlist = args.file;
const index = args.index;
const playlistID = playlist.id;
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
if (res['success']) {
this.playlists.splice(index, 1);
this.postsService.openSnackBar('Playlist successfully removed.', '');
}
this.getAllPlaylists();
});
}
editPlaylistDialog(args) {
const playlist = args.playlist;
const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: playlist,
width: '65vw'
}
});
dialogRef.afterClosed().subscribe(res => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlists[index] = dialogRef.componentInstance.original_playlist;
}
});
}
}

View File

@@ -15,7 +15,7 @@
</div>
</div>
<div>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn">Clear all downloads</button>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
</div>
</mat-card>
</ng-container>

View File

@@ -1,11 +1,24 @@
<div style="height: 100%">
<div *ngIf="logs_loading" style="position: absolute; top: 40%; left: 50%">
<div style="height: 275px;">
<div *ngIf="logs_loading" style="z-index: 999; 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>
<!-- Virtual mode (fast, select text buggy) -->
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</cdk-virtual-scroll-viewport>-->
<!-- Non-virtual mode (slow, bug-free) -->
<div style="height: 274px; overflow-y: auto">
<div *ngFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</div>
<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">
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
<div style="display: inline-block;">
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>&nbsp;
<mat-form-field style="width: 75px;">
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
@@ -17,5 +30,8 @@
</mat-select>
</mat-form-field>
</div>
<span class="spacer"></span>
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
</div>
</div>

View File

@@ -0,0 +1 @@
.spacer {flex: 1 1 auto;}

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { PostsService } from '../../posts.services';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-logs-viewer',
@@ -8,10 +10,11 @@ import { PostsService } from '../../posts.services';
})
export class LogsViewerComponent implements OnInit {
logs: string = null;
logs: any = null;
logs_text: string = null;
requested_lines = 50;
logs_loading = false;
constructor(private postsService: PostsService) { }
constructor(private postsService: PostsService, private dialog: MatDialog) { }
ngOnInit(): void {
this.getLogs();
@@ -21,8 +24,24 @@ export class LogsViewerComponent implements OnInit {
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'];
if (res['logs'] !== null || res['logs'] !== undefined) {
this.logs_text = res['logs'];
this.logs = [];
const logs_arr = res['logs'].split('\n');
logs_arr.forEach(log_line => {
let color = 'inherit'
if (log_line.includes('ERROR')) {
color = 'red';
} else if (log_line.includes('WARN')) {
color = 'yellow';
} else if (log_line.includes('VERBOSE')) {
color = 'gray';
}
this.logs.push({
text: log_line,
color: color
})
});
} else {
this.postsService.openSnackBar('Failed to retrieve logs!');
}
@@ -37,4 +56,30 @@ export class LogsViewerComponent implements OnInit {
this.postsService.openSnackBar('Logs copied to clipboard!');
}
clearLogs() {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: 'Clear logs',
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
submitText: 'Clear'
}
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.postsService.clearAllLogs().subscribe(res => {
if (res['success']) {
this.logs = [];
this.logs_text = '';
this.getLogs();
this.postsService.openSnackBar('Logs successfully cleared!');
} else {
this.postsService.openSnackBar('Failed to clear logs!');
}
}, err => {
this.postsService.openSnackBar('Failed to clear logs!');
});
}
});
}
}

View File

@@ -16,7 +16,7 @@
<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="default"><ng-container i18n="Use role default">Use role 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>
@@ -27,5 +27,5 @@
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -4,7 +4,7 @@
<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">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search" i18n-placeholder="search field description">
</mat-form-field>
</div>
@@ -55,22 +55,22 @@
<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">
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Save" i18n-matTooltip="save user edit action button tooltip">
<mat-icon>done</mat-icon>
</button>
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel editing user">
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel" i18n-matTooltip="cancel user edit action button tooltip">
<mat-icon>cancel</mat-icon>
</button>
</span>
<ng-template #notediting>
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user">
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
<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">
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user" i18n-matTooltip="manage user action button tooltip">
<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">
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user" i18n-matTooltip="delete user action button tooltip">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>

View File

@@ -0,0 +1,45 @@
<div class="container-fluid" style="max-width: 941px;">
<div class="row">
<div class="col-12 order-2 col-sm-4 order-sm-1 d-flex justify-content-center">
<div>
<div style="display: inline-block;">
<mat-form-field style="width: 132px;">
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="sort-dir-div">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>
</div>
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
<h4 class="my-videos-title" i18n="My videos title">My videos</h4>
</div>
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
</div>
<div>
<div class="container">
<div class="row justify-content-center">
<ng-container *ngIf="normal_files_received">
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div>
</ng-container>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -0,0 +1,60 @@
.large-col {
max-width: 320px;
}
.medium-col {
max-width: 240px;
}
.small-col {
max-width: 240px;
}
.search-bar {
transition: all .5s ease;
position: relative;
float: right;
}
.search-bar-unfocused {
width: 132px;
}
.search-input {
transition: all .5s ease;
}
.search-bar-focused {
width: 200px;
}
.flex-grid {
width: 100%;
display: block;
position: relative;
padding-left: 12px;
padding-right: 12px;
}
.column {
width: 33%;
display: inline-block;
}
.sort-dir-div {
display: inline-block;
position: absolute;
top: 10px;
}
.my-videos-title {
text-align: center;
position: relative;
top: 12px;
}
@media (max-width: 576px) {
.my-videos-title {
top: 0px;
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RecentVideosComponent } from './recent-videos.component';
describe('RecentVideosComponent', () => {
let component: RecentVideosComponent;
let fixture: ComponentFixture<RecentVideosComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RecentVideosComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RecentVideosComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,288 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
@Component({
selector: 'app-recent-videos',
templateUrl: './recent-videos.component.html',
styleUrls: ['./recent-videos.component.scss']
})
export class RecentVideosComponent implements OnInit {
cached_file_count = 0;
loading_files = null;
normal_files_received = false;
subscription_files_received = false;
files: any[] = null;
filtered_files: any[] = null;
downloading_content = {'video': {}, 'audio': {}};
search_mode = false;
search_text = '';
searchIsFocused = false;
descendingMode = true;
filterProperties = {
'registered': {
'key': 'registered',
'label': 'Download Date',
'property': 'registered'
},
'upload_date': {
'key': 'upload_date',
'label': 'Upload Date',
'property': 'upload_date'
},
'name': {
'key': 'name',
'label': 'Name',
'property': 'title'
},
'file_size': {
'key': 'file_size',
'label': 'File Size',
'property': 'size'
},
'duration': {
'key': 'duration',
'label': 'Duration',
'property': 'duration'
}
};
filterProperty = this.filterProperties['upload_date'];
constructor(public postsService: PostsService, private router: Router) {
// get cached file count
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count');
this.loading_files = Array(this.cached_file_count).fill(0);
}
}
ngOnInit(): void {
if (this.postsService.initialized) {
this.getAllFiles();
}
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllFiles();
}
});
// set filter property to cached
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
}
// search
onSearchInputChanged(newvalue) {
if (newvalue.length > 0) {
this.search_mode = true;
this.filterFiles(newvalue);
} else {
this.search_mode = false;
this.filtered_files = this.files;
}
}
private filterFiles(value: string) {
const filterValue = value.toLowerCase();
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
}
filterByProperty(prop) {
if (this.descendingMode) {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
} else {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
}
}
filterOptionChanged(value) {
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
}
toggleModeChange() {
this.descendingMode = !this.descendingMode;
this.filterByProperty(this.filterProperty['property']);
}
// get files
getAllFiles() {
this.normal_files_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.files = res['files'];
this.files.forEach(file => {
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
});
this.files.sort(this.sortFiles);
if (this.search_mode) {
this.filterFiles(this.search_text);
} else {
this.filtered_files = this.files;
}
this.filterByProperty(this.filterProperty['property']);
// set cached file count for future use, note that we convert the amount of files to a string
localStorage.setItem('cached_file_count', '' + this.files.length);
this.normal_files_received = true;
});
}
// navigation
goToFile(info_obj) {
const file = info_obj['file'];
const event = info_obj['event'];
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file);
} else {
this.navigateToFile(file, event.ctrlKey);
}
}
navigateToFile(file, new_tab) {
localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) {
// streaming only mode subscriptions
!new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else {
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
}
} else {
// normal files
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
: window.open(`/#/player;type=${file.isAudio ? 'audio' : 'video'};uid=${file.uid}`);
}
}
goToSubscription(file) {
this.router.navigate(['/subscription', {id: file.sub_id}]);
}
// downloading
downloadFile(file) {
if (file.sub_id) {
this.downloadSubscriptionFile(file);
} else {
this.downloadNormalFile(file);
}
}
downloadSubscriptionFile(file) {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
const blob: Blob = res;
saveAs(blob, file.id + ext);
}, err => {
console.log(err);
});
}
downloadNormalFile(file) {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const name = file.id;
this.downloading_content[type][name] = true;
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
this.downloading_content[type][name] = false;
const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + ext);
if (!this.postsService.config.Extra.file_manager_enabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, false).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
}
});
}
// deleting
deleteFile(args) {
const file = args.file;
const index = args.index;
const blacklistMode = args.blacklistMode;
if (file.sub_id) {
this.deleteSubscriptionFile(file, index, blacklistMode);
} else {
this.deleteNormalFile(file, index, blacklistMode);
}
}
deleteNormalFile(file, index, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.files.splice(index, 1);
} else {
this.postsService.openSnackBar('Delete failed!', 'OK.');
}
}, err => {
this.postsService.openSnackBar('Delete failed!', 'OK.');
});
}
deleteSubscriptionFile(file, index, blacklistMode = false) {
if (blacklistMode) {
this.deleteForever(file, index);
} else {
this.deleteAndRedownload(file, index);
}
}
deleteAndRedownload(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.files.splice(index, 1);
});
}
deleteForever(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.files.splice(index, 1);
});
}
// sorting and filtering
sortFiles(a, b) {
// uses the 'registered' flag as the timestamp
const result = b.registered - a.registered;
return result;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

View File

@@ -0,0 +1,48 @@
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
<div *ngIf="!loading" class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>&nbsp;&nbsp;{{file_obj.registered | date:'shortDate'}}</div>
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<ng-container *ngIf="!is_playlist && !loading">
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
<mat-divider></mat-divider>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
</button>
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container>
</button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</ng-container>
<ng-container *ngIf="is_playlist && !loading">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<mat-divider></mat-divider>
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</ng-container>
<ng-container *ngIf="loading">
<button mat-menu-item>Placeholder</button>
</ng-container>
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time">
{{file_length}}
</div>
</div>
</div>
<div *ngIf="loading" class="img-div">
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="100" height="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
</div>
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
</div>
</mat-card>
</div>

View File

@@ -0,0 +1,137 @@
.large-mat-card {
width: 300px;
height: 250px;
padding: 0px;
cursor: pointer;
}
.file-mat-card {
width: 200px;
height: 200px;
padding: 0px;
cursor: pointer;
}
.small-mat-card {
width: 150px;
height: 150px;
padding: 0px;
cursor: pointer;
}
.menuButton {
right: 0px;
top: -1px;
position: absolute;
z-index: 999;
}
/* Coerce the <span> icon container away from display:inline */
.mat-icon-button .mat-button-wrapper {
display: flex;
justify-content: center;
}
.image-large {
width: 300px;
height: 167.5px;
object-fit: cover;
}
.image {
width: 200px;
height: 112.5px;
object-fit: cover;
}
.image-small {
width: 150px;
height: 84.5px;
object-fit: cover;
}
.example-full-width-height {
width: 100%;
height: 100%
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.img-div {
max-height: 80px;
padding: 0px;
margin: 32px 0px 0px -5px;
width: calc(100% + 5px + 5px);
}
.max-two-lines {
display: -webkit-box;
display: -moz-box;
max-height: 2.4em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.max-one-line {
display: -webkit-box;
display: -moz-box;
max-height: 1.2em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.duration-time {
position: absolute;
bottom: 5px;
right: 5px;
z-index: 99999;
background: rgba(255,255,255,0.6);
padding-left: 5px;
padding-right: 5px;
color: black;
}
.download-time {
position: absolute;
top: 1px;
left: 5px;
z-index: 99999;
}
.audio-video-icon {
position: relative;
top: 6px;
}
.title-loading {
width: 93%;
position: absolute;
bottom: 1px;
}
@media (max-width: 576px){
// .example-card {
// width: 175px !important;
// }
// .image {
// width: 175px;
// }
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UnifiedFileCardComponent } from './unified-file-card.component';
describe('UnifiedFileCardComponent', () => {
let component: UnifiedFileCardComponent;
let fixture: ComponentFixture<UnifiedFileCardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UnifiedFileCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UnifiedFileCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,124 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-unified-file-card',
templateUrl: './unified-file-card.component.html',
styleUrls: ['./unified-file-card.component.scss']
})
export class UnifiedFileCardComponent implements OnInit {
// required info
file_title = '';
file_length = '';
file_thumbnail = '';
type = null;
elevated = false;
// optional vars
thumbnailBlobURL = null;
// input/output
@Input() loading = true;
@Input() theme = null;
@Input() file_obj = null;
@Input() card_size = 'medium';
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Output() goToFile = new EventEmitter<any>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() editPlaylist = new EventEmitter<any>();
/*
Planned sizes:
small: 150x175
medium: 200x200
big: 250x200
*/
constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
ngOnInit(): void {
if (!this.loading) {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
if (this.file_obj && this.file_obj.thumbnailBlob) {
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
const bloburl = URL.createObjectURL(blob);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
}
}
emitDeleteFile(blacklistMode = false) {
this.deleteFile.emit({
file: this.file_obj,
index: this.index,
blacklistMode: blacklistMode
});
}
navigateToFile(event) {
this.goToFile.emit({file: this.file_obj, event: event});
}
navigateToSubscription() {
this.goToSubscription.emit(this.file_obj);
}
openFileInfoDialog() {
this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.file_obj,
},
minWidth: '50vw'
})
}
emitEditPlaylist() {
this.editPlaylist.emit({
playlist: this.file_obj,
index: this.index
});
}
}
function fancyTimeFormat(time) {
if (typeof time === 'string') {
return time;
}
// Hours, minutes and seconds
const hrs = ~~(time / 3600);
const mins = ~~((time % 3600) / 60);
const secs = ~~time % 60;
// Output like "1:01" or "4:03:59" or "123:03:59"
let ret = '';
if (hrs > 0) {
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
}
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
ret += '' + secs;
return ret;
}
function getMimeByFilename(name) {
switch (name.substring(name.length-4, name.length)) {
case '.jpg':
return 'image/jpeg';
case '.png':
return 'image/png';
case 'webp':
return 'image/webp';
default:
return null;
}
}

View File

@@ -1 +1 @@
export const CURRENT_VERSION = 'v4.0';
export const CURRENT_VERSION = 'v4.1';

View File

@@ -1,18 +1,34 @@
<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" 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 *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>
</mat-form-field>
<div *ngIf="filesToSelectFrom || (audiosToSelectFrom && videosToSelectFrom)">
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div *ngIf="!filesToSelectFrom">
<mat-form-field color="accent">
<mat-select placeholder="Type" i18n-placeholder="Type select" [(ngModel)]="type" [ngModelOptions]="{standalone: true}">
<mat-option value="audio"><ng-container i18n="Audio">Audio</ng-container></mat-option>
<mat-option value="video"><ng-container i18n="Video">Video</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length > 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length > 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length > 0))" color="accent">
<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>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
</mat-select>
</mat-form-field>
<!-- No videos available -->
<div style="margin-bottom: 15px;" *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length === 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length === 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length === 0))">
No files available.
</div>
</div>
</div>
</form>

View File

@@ -14,6 +14,8 @@ export class CreatePlaylistComponent implements OnInit {
filesToSelectFrom = null;
type = null;
filesSelect = new FormControl();
audiosToSelectFrom = null;
videosToSelectFrom = null;
name = '';
create_in_progress = false;
@@ -28,12 +30,30 @@ export class CreatePlaylistComponent implements OnInit {
this.filesToSelectFrom = this.data.filesToSelectFrom;
this.type = this.data.type;
}
if (!this.filesToSelectFrom) {
this.getMp3s();
this.getMp4s();
}
}
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
this.audiosToSelectFrom = result['mp3s'];
});
}
getMp4s() {
this.postsService.getMp4s().subscribe(result => {
this.videosToSelectFrom = result['mp4s'];
});
}
createPlaylist() {
const thumbnailURL = this.getThumbnailURL();
const duration = this.calculateDuration();
this.create_in_progress = true;
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => {
this.create_in_progress = false;
if (res['success']) {
this.dialogRef.close(true);
@@ -44,8 +64,12 @@ export class CreatePlaylistComponent implements OnInit {
}
getThumbnailURL() {
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
const file = this.filesToSelectFrom[i];
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === this.filesSelect.value[0]) {
// different services store the thumbnail in different places
if (file.thumbnailURL) { return file.thumbnailURL };
@@ -55,4 +79,35 @@ export class CreatePlaylistComponent implements OnInit {
return null;
}
getDuration(file_id) {
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === file_id) {
return file.duration;
}
}
return null;
}
calculateDuration() {
let sum = 0;
for (let i = 0; i < this.filesSelect.value.length; i++) {
const duration_val = this.getDuration(this.filesSelect.value[i]);
sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val;
}
return sum;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

View File

@@ -18,15 +18,43 @@
<h5 style="margin-top: 10px;">Installation details:</h5>
<p>
<ng-container i18n="Version label">Installed version:</ng-container>&nbsp;{{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner>&nbsp;<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>&nbsp;&nbsp;<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>
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [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></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>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
</p>
<mat-divider></mat-divider>
<div style="margin-top: 10px;">
<h5>Personal settings:</h5>
<mat-form-field placeholder="Sidepanel mode">
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
<mat-option value="over">
Over
</mat-option>
<mat-option value="side">
Side
</mat-option>
</mat-select>
</mat-form-field>
<br/>
<mat-form-field placeholder="Card size">
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
<mat-option value="large">
Large
</mat-option>
<mat-option value="medium">
Medium
</mat-option>
<mat-option value="small">
Small
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close>Close</button>
</mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -16,11 +16,13 @@ export class AboutDialogComponent implements OnInit {
checking_for_updates = true;
current_version_tag = CURRENT_VERSION;
sidepanel_mode = this.postsService.sidepanel_mode;
card_size = this.postsService.card_size;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getLatestGithubRelease()
this.getLatestGithubRelease();
}
getLatestGithubRelease() {
@@ -30,4 +32,14 @@ export class AboutDialogComponent implements OnInit {
});
}
sidePanelModeChanged(new_mode) {
localStorage.setItem('sidepanel_mode', new_mode);
this.postsService.sidepanel_mode = new_mode;
}
cardSizeOptionChanged(new_size) {
localStorage.setItem('card_size', new_size);
this.postsService.card_size = new_size;
}
}

View File

@@ -1,18 +0,0 @@
<h4 *ngIf="pinSetChecked" mat-dialog-title>{{dialog_title}}</h4>
<mat-dialog-content>
<div style="position: relative">
<div *ngIf="pinSetChecked">
<mat-form-field color="accent">
<input type="password" (keyup.enter)="doAction()" matInput [(ngModel)]="input" [placeholder]="input_placeholder">
</mat-form-field>
</div>
<div class="spinner-div" *ngIf="!pinSetChecked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button [disabled]="input.length === 0" color="accent" style="margin-bottom: 12px;" (click)="doAction()" mat-raised-button>{{button_label}}</button>
</mat-dialog-actions>

View File

@@ -1,6 +0,0 @@
.spinner-div {
position: absolute;
margin: 0 auto;
top: 30%;
left: 42%;
}

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CheckOrSetPinDialogComponent } from './check-or-set-pin-dialog.component';
describe('CheckOrSetPinDialogComponent', () => {
let component: CheckOrSetPinDialogComponent;
let fixture: ComponentFixture<CheckOrSetPinDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CheckOrSetPinDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CheckOrSetPinDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,96 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-check-or-set-pin-dialog',
templateUrl: './check-or-set-pin-dialog.component.html',
styleUrls: ['./check-or-set-pin-dialog.component.scss']
})
export class CheckOrSetPinDialogComponent implements OnInit {
pinSetChecked = false;
pinSet = true;
resetMode = false;
dialog_title = '';
input_placeholder = null;
input = '';
button_label = '';
constructor(private postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: any,
public dialogRef: MatDialogRef<CheckOrSetPinDialogComponent>, private snackBar: MatSnackBar) { }
ngOnInit() {
if (this.data) {
this.resetMode = this.data.resetMode;
}
if (this.resetMode) {
this.pinSetChecked = true;
this.notSetLogic();
} else {
this.isPinSet();
}
}
isPinSet() {
this.postsService.isPinSet().subscribe(res => {
this.pinSetChecked = true;
if (res['is_set']) {
this.isSetLogic();
} else {
this.notSetLogic();
}
});
}
isSetLogic() {
this.pinSet = true;
this.dialog_title = 'Pin Required';
this.input_placeholder = 'Pin';
this.button_label = 'Submit'
}
notSetLogic() {
this.pinSet = false;
this.dialog_title = 'Set your pin';
this.input_placeholder = 'New pin';
this.button_label = 'Set Pin'
}
doAction() {
// pin set must have been checked, and input must not be empty
if (!this.pinSetChecked || this.input.length === 0) {
return;
}
if (this.pinSet) {
this.postsService.checkPin(this.input).subscribe(res => {
if (res['success']) {
this.dialogRef.close(true);
} else {
this.dialogRef.close(false);
this.openSnackBar('Pin is incorrect!');
}
});
} else {
this.postsService.setPin(this.input).subscribe(res => {
if (res['success']) {
this.dialogRef.close(true);
this.openSnackBar('Pin successfully set!');
} else {
this.dialogRef.close(false);
this.openSnackBar('Failed to set pin!');
}
});
}
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -0,0 +1,15 @@
<h4 mat-dialog-title>{{dialogTitle}}</h4>
<mat-dialog-content>
<div style="margin-bottom: 10px;">
{{dialogText}}
</div>
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<div class="mat-spinner" *ngIf="submitClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
<span class="spacer"></span>
<button style="float: right;" mat-stroked-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,5 @@
.spacer {flex: 1 1 auto;}
.mat-spinner {
margin-left: 8px;
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfirmDialogComponent } from './confirm-dialog.component';
describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ConfirmDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,44 @@
import { Component, OnInit, Inject, EventEmitter } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
export class ConfirmDialogComponent implements OnInit {
dialogTitle = 'Confirm';
dialogText = 'Would you like to confirm?';
submitText = 'Yes'
submitClicked = false;
doneEmitter: EventEmitter<any> = null;
onlyEmitOnDone = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
if (this.data.submitText) { this.submitText = this.data.submitText };
// checks if emitter exists, if so don't autoclose as it should be handled by caller
if (this.data.doneEmitter) {
this.doneEmitter = this.data.doneEmitter;
this.onlyEmitOnDone = true;
}
}
confirmClicked() {
if (this.onlyEmitOnDone) {
this.doneEmitter.emit(true);
this.submitClicked = true;
} else {
this.dialogRef.close(true);
}
}
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,62 @@
<h4 mat-dialog-title i18n="Edit subscription dialog title">Editing {{sub.name}}</h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
<div class="col-12" *ngIf="!download_all && editor_initialized">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
<mat-form-field color="accent" style="width: 50px; text-align: center; margin-left: 10px;">
<input type="number" matInput [(ngModel)]="timerange_amount" (ngModelChange)="timerangeChanged($event, false)">
</mat-form-field>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit" (ngModelChange)="timerangeChanged($event, true)">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mb-3">
<mat-form-field color="accent">
<input [(ngModel)]="new_sub.custom_args" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder">
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-hint>
<ng-container i18n="Custom args hint">These are added after the standard args.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [(ngModel)]="new_sub.custom_output" matInput placeholder="Custom file output" i18n-placeholder="Subscription custom file output placeholder">
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="updating || !subChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -0,0 +1,9 @@
.args-edit-button {
position: absolute;
margin-left: 10px;
}
.unit-select {
width: 75px;
margin-left: 20px;
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EditSubscriptionDialogComponent } from './edit-subscription-dialog.component';
describe('EditSubscriptionDialogComponent', () => {
let component: EditSubscriptionDialogComponent;
let fixture: ComponentFixture<EditSubscriptionDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EditSubscriptionDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditSubscriptionDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,124 @@
import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component';
@Component({
selector: 'app-edit-subscription-dialog',
templateUrl: './edit-subscription-dialog.component.html',
styleUrls: ['./edit-subscription-dialog.component.scss']
})
export class EditSubscriptionDialogComponent implements OnInit {
updating = false;
sub = null;
new_sub = null;
editor_initialized = false;
timerange_amount: number;
timerange_unit = 'days';
audioOnlyMode = null;
download_all = null;
time_units = [
'day',
'week',
'month',
'year'
];
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialog: MatDialog, private postsService: PostsService) {
this.sub = this.data.sub;
this.new_sub = JSON.parse(JSON.stringify(this.sub));
this.audioOnlyMode = this.sub.type === 'audio';
this.download_all = !this.sub.timerange;
if (this.sub.timerange) {
const timerange_str = this.sub.timerange.split('-')[1];
console.log(timerange_str);
const number = timerange_str.replace(/\D/g,'');
let units = timerange_str.replace(/[0-9]/g, '');
console.log(units);
// // remove plural on units
// if (units[units.length-1] === 's') {
// units = units.substring(0, units.length-1);
// }
this.timerange_amount = parseInt(number);
this.timerange_unit = units;
this.editor_initialized = true;
} else {
this.editor_initialized = true
}
}
ngOnInit(): void {
}
downloadAllToggled() {
if (this.download_all) {
this.new_sub.timerange = null;
} else {
console.log('checking');
this.timerangeChanged(null, null);
}
}
saveSubscription() {
this.postsService.updateSubscription(this.sub).subscribe(res => {
this.sub = this.new_sub;
this.new_sub = JSON.parse(JSON.stringify(this.sub));
})
}
getSubscription() {
this.postsService.getSubscription(this.sub.id).subscribe(res => {
this.sub = res['subscription'];
this.new_sub = JSON.parse(JSON.stringify(this.sub));
});
}
timerangeChanged(value, select_changed) {
console.log(this.timerange_amount);
console.log(this.timerange_unit);
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
console.log(this.new_sub.timerange);
} else {
this.new_sub.timerange = null;
}
}
saveClicked() {
this.saveSubscription();
}
// modify custom args
openArgsModifierDialog() {
if (!this.new_sub.custom_args) {
this.new_sub.custom_args = '';
}
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
data: {
initial_args: this.new_sub.custom_args
}
});
dialogRef.afterClosed().subscribe(new_args => {
if (new_args !== null && new_args !== undefined) {
this.new_sub.custom_args = new_args;
}
});
}
subChanged() {
return JSON.stringify(this.new_sub) !== JSON.stringify(this.sub);
}
}

View File

@@ -24,14 +24,16 @@
</div>
<div class="col-12" *ngIf="!download_all">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
<mat-form-field color="accent" style="width: 50px; text-align: center">
<mat-form-field color="accent" style="width: 50px; text-align: center; margin-left: 10px;">
<input type="number" matInput [(ngModel)]="timerange_amount">
</mat-form-field>
<mat-select color="accent" class="unit-select" [(ngModel)]="timerange_unit">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>

View File

@@ -5,8 +5,8 @@
</mat-grid-tile>
<mat-grid-tile [colspan]="13">
<mat-progress-bar [value]="(download.complete || download.error) ? 100 : download.percent_complete" [mode]="(!download.complete && download.percent_complete === 0 && !download.error) ? 'indeterminate' : 'determinate'"></mat-progress-bar>
<mat-icon *ngIf="download.complete" style="margin-left: 25px; cursor: default" matTooltip="The download is complete" matTooltip-i18n>done</mat-icon>
<mat-icon *ngIf="download.error" style="margin-left: 25px; cursor: default" matTooltip="An error has occurred" matTooltip-i18n>error</mat-icon>
<mat-icon *ngIf="download.complete" style="margin-left: 25px; cursor: default" matTooltip="The download was successful" i18n-matTooltip="download successful tooltip">done</mat-icon>
<mat-icon *ngIf="download.error" style="margin-left: 25px; cursor: default" matTooltip="An error has occurred" i18n-matTooltip="download error tooltip">error</mat-icon>
</mat-grid-tile>
<mat-grid-tile [colspan]="4">
<button style="margin-bottom: 2px;" (click)="cancelTheDownload()" mat-icon-button color="warn"><mat-icon fontSet="material-icons-outlined">cancel</mat-icon></button>

View File

@@ -1,11 +1,8 @@
<br/>
<div class="big demo-basic">
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : null">
<mat-card-title>
<ng-container i18n="Youtube downloader home page label">Youtube Downloader</ng-container>
</mat-card-title>
<mat-card-content>
<div style="position: relative;">
<mat-card-content style="padding: 0px 8px 0px 8px;">
<div style="position: relative; margin-right: 15px;">
<form class="example-form">
<div class="container-fluid">
<div class="row">
@@ -167,7 +164,7 @@
<br/>
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
<div class="margined">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 15;else indeterminateprogress">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
</div>
@@ -183,7 +180,15 @@
<ng-template #nofile>
</ng-template>
<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
</ng-container>
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<mat-accordion>
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
<mat-expansion-panel-header>
@@ -260,7 +265,7 @@
</mat-grid-tile>
</mat-grid-list>
<!-- Add video playlist button -->
<!-- Add video playlist button --<
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
<div *ngIf="playlists.video.length === 0">
<ng-container i18n="No video playlists available text">
@@ -270,12 +275,4 @@
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template #nomp3s>
</ng-template>
<ng-template #nomp4s>
</ng-template>
</div>-->

View File

@@ -82,8 +82,9 @@ export class MainComponent implements OnInit {
useDefaultDownloadingAgent = true;
customDownloadingAgent = null;
// formats cache
// cache
cachedAvailableFormats = {};
cachedFileManagerEnabled = localStorage.getItem('cached_filemanager_enabled') === 'true';
// youtube api
youtubeSearchEnabled = false;
@@ -232,7 +233,8 @@ export class MainComponent implements OnInit {
async loadConfig() {
// loading config
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled'];
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('filemanager'));
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
@@ -261,6 +263,10 @@ export class MainComponent implements OnInit {
}
// set final cache items
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
this.cachedFileManagerEnabled = this.fileManagerEnabled;
if (this.allowAdvancedDownload) {
if (localStorage.getItem('customArgsEnabled') !== null) {
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
@@ -1033,8 +1039,8 @@ export class MainComponent implements OnInit {
}
} else if (format_obj.type === 'video') {
// check if video format is mp4
const key = format.height.toString();
if (format.ext === 'mp4') {
const key = format.format_note.replace('p', '');
if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
format_obj['height'] = format.height;
format_obj['acodec'] = format.acodec;
format_obj['format_id'] = format.format_id;

View File

@@ -15,6 +15,7 @@
.video-styles {
width: 100%;
height: 75vh;
}
::ng-deep .mat-button-toggle-label-content {
@@ -65,6 +66,13 @@
padding-right: 0px;
padding-left: 0.01px;
height: 100%;
width: 100%;
}
.audio-col {
margin: 0 auto;
margin-top: 10px;
width: 95%;
}
.save-icon {

View File

@@ -1,13 +1,13 @@
<div *ngIf="playlist.length > 0 && show_player">
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
<div style="max-width: 100%; margin-left: 0px; height: 70vh" class="row">
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
</video>
</vg-player>
</div>
<div class="col-12 my-2">
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
import { VgAPI } from 'ngx-videogular';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface IMedia {
templateUrl: './player.component.html',
styleUrls: ['./player.component.css']
})
export class PlayerComponent implements OnInit {
export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
playlist: Array<IMedia> = [];
original_playlist: string = null;
@@ -62,6 +62,7 @@ export class PlayerComponent implements OnInit {
downloading = false;
save_volume_timer = null;
original_volume = null;
@HostListener('window:resize', ['$event'])
@@ -94,6 +95,15 @@ export class PlayerComponent implements OnInit {
}
}
ngAfterViewInit() {
this.postsService.sidenav.close();
}
ngOnDestroy() {
// prevents volume save feature from running in the background
clearInterval(this.save_volume_timer);
}
constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
public snackBar: MatSnackBar) {
@@ -241,7 +251,8 @@ export class PlayerComponent implements OnInit {
if (localStorage.getItem('player_volume')) {
this.api.volume = parseFloat(localStorage.getItem('player_volume'));
}
setInterval(() => this.saveVolume(this.api), 2000)
this.save_volume_timer = setInterval(() => this.saveVolume(this.api), 2000)
this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this));
this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this));

View File

@@ -15,14 +15,14 @@ import * as Fingerprint2 from 'fingerprintjs2';
@Injectable()
export class PostsService implements CanActivate {
path = '';
audioFolder = '';
videoFolder = '';
startPath = null; // 'http://localhost:17442/';
startPathSSL = null; // 'https://localhost:17442/'
handShakeComplete = false;
// local settings
THEMES_CONFIG = THEMES_CONFIG;
theme;
settings_changed = new BehaviorSubject<boolean>(false);
card_size = 'medium';
sidepanel_mode = 'over';
// auth
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null;
httpOptions = null;
@@ -39,19 +39,24 @@ export class PostsService implements CanActivate {
available_permissions = null;
// behavior subjects
reload_config = new BehaviorSubject<boolean>(false);
config_reloaded = new BehaviorSubject<boolean>(false);
service_initialized = new BehaviorSubject<boolean>(false);
initialized = false;
settings_changed = new BehaviorSubject<boolean>(false);
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
// app status
initialized = false;
// global vars
config = null;
subscriptions = null;
sidenav = null;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar) {
console.log('PostsService Initialized...');
// this.startPath = window.location.href + '/api/';
// this.startPathSSL = window.location.href + '/api/';
this.path = this.document.location.origin + '/api/';
if (isDevMode()) {
@@ -81,6 +86,7 @@ export class PostsService implements CanActivate {
if (result) {
this.config = result['YoutubeDLMaterial'];
if (this.config['Advanced']['multi_user_mode']) {
this.checkAdminCreationStatus();
// login stuff
if (localStorage.getItem('jwt_token') && localStorage.getItem('jwt_token') !== 'null') {
this.token = localStorage.getItem('jwt_token');
@@ -100,6 +106,14 @@ export class PostsService implements CanActivate {
this.reload_config.subscribe(yes_reload => {
if (yes_reload) { this.reloadConfig(); }
});
if (localStorage.getItem('sidepanel_mode')) {
this.sidepanel_mode = localStorage.getItem('sidepanel_mode');
}
if (localStorage.getItem('card_size')) {
this.card_size = localStorage.getItem('card_size');
}
}
canActivate(route, state): Promise<boolean> {
return new Promise(resolve => {
@@ -113,6 +127,15 @@ export class PostsService implements CanActivate {
this.theme = this.THEMES_CONFIG[theme];
}
getSubscriptionByID(sub_id) {
for (let i = 0; i < this.subscriptions.length; i++) {
if (this.subscriptions[i]['id'] === sub_id) {
return this.subscriptions[i];
}
}
return null;
}
startHandshake(url: string) {
return this.http.get(url + 'geturl');
}
@@ -131,14 +154,6 @@ export class PostsService implements CanActivate {
});
}
getVideoFolder() {
return this.http.get(this.startPath + 'videofolder');
}
getAudioFolder() {
return this.http.get(this.startPath + 'audiofolder');
}
// tslint:disable-next-line: max-line-length
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
return this.http.post(this.path + 'tomp3', {url: url,
@@ -163,12 +178,8 @@ export class PostsService implements CanActivate {
ui_uid: ui_uid}, this.httpOptions);
}
getFileStatusMp3(name: string) {
return this.http.post(this.path + 'fileStatusMp3', {name: name}, this.httpOptions);
}
getFileStatusMp4(name: string) {
return this.http.post(this.path + 'fileStatusMp4', {name: name}, this.httpOptions);
killAllDownloads() {
return this.http.post(this.path + 'killAllDownloads', {}, this.httpOptions);
}
loadNavItems() {
@@ -207,6 +218,10 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'getFile', {uid: uid, type: type, uuid: uuid}, this.httpOptions);
}
getAllFiles() {
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
uid = null, uuid = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
@@ -238,16 +253,8 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'logs', {lines: lines}, this.httpOptions);
}
isPinSet() {
return this.http.post(this.path + 'isPinSet', {}, this.httpOptions);
}
setPin(unhashed_pin) {
return this.http.post(this.path + 'setPin', {pin: unhashed_pin}, this.httpOptions);
}
checkPin(unhashed_pin) {
return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin}, this.httpOptions);
clearAllLogs() {
return this.http.post(this.path + 'clearAllLogs', {}, this.httpOptions);
}
generateNewAPIKey() {
@@ -262,11 +269,12 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
}
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) {
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
fileNames: fileNames,
type: type,
thumbnailURL: thumbnailURL}, this.httpOptions);
thumbnailURL: thumbnailURL,
duration: duration}, this.httpOptions);
}
getPlaylist(playlistID, type, uuid = null) {
@@ -293,6 +301,10 @@ export class PostsService implements CanActivate {
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
}
updateSubscription(subscription) {
return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
}
unsubscribe(sub, deleteMode = false) {
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
}
@@ -366,7 +378,7 @@ export class PostsService implements CanActivate {
// user methods
login(username, password) {
const call = this.http.post(this.path + 'auth/login', {userid: username, password: password}, this.httpOptions);
const call = this.http.post(this.path + 'auth/login', {username: username, password: password}, this.httpOptions);
return call;
}
@@ -380,6 +392,8 @@ export class PostsService implements CanActivate {
}, err => {
if (err.status === 401) {
this.sendToLogin();
this.token = null;
this.resetHttpParams();
}
console.log(err);
});
@@ -396,14 +410,7 @@ export class PostsService implements CanActivate {
this.router.navigate(['/login']);
}
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
this.resetHttpParams();
}
// user methods
@@ -415,7 +422,6 @@ export class PostsService implements CanActivate {
}
sendToLogin() {
this.checkAdminCreationStatus();
if (!this.initialized) {
this.setInitialized();
}
@@ -429,12 +435,29 @@ export class PostsService implements CanActivate {
this.openSnackBar('You must log in to access this page!');
}
resetHttpParams() {
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
}
setInitialized() {
this.service_initialized.next(true);
this.initialized = true;
this.config_reloaded.next(true);
}
reloadSubscriptions() {
this.getAllSubscriptions().subscribe(res => {
this.subscriptions = res['subscriptions'];
});
}
adminExists() {
return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions);
}
@@ -445,8 +468,8 @@ export class PostsService implements CanActivate {
password: password}, this.httpOptions);
}
checkAdminCreationStatus(skip_check = false) {
if (!skip_check && !this.config['Advanced']['multi_user_mode']) {
checkAdminCreationStatus(force_show = false) {
if (!force_show && !this.config['Advanced']['multi_user_mode']) {
return;
}
this.adminExists().subscribe(res => {

View File

@@ -19,7 +19,7 @@
<mat-hint><ng-container i18n="URL setting input hint">URL this app will be accessed from, without the port.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-4">
<div class="col-12 mb-4 mt-3">
<mat-form-field class="text-field" color="accent">
<input [(ngModel)]="new_config['Host']['port']" matInput placeholder="Port" i18n-placeholder="Port input placeholder" required>
<mat-hint><ng-container i18n="Port setting input hint">The desired port. Default is 17442.</ng-container></mat-hint>
@@ -42,26 +42,6 @@
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Encryption']['use-encryption']"><ng-container i18n="Use encryption setting">Use encryption</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['cert-file-path']" matInput placeholder="Cert file path" i18n-placeholder="Cert file path input placeholder">
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['key-file-path']" matInput placeholder="Key file path" i18n-placeholder="Key file path input placeholder">
</mat-form-field>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
@@ -73,17 +53,12 @@
<mat-hint><ng-container i18n="Subscriptions base path setting input hint">Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-5">
<div class="col-12 mt-5 mb-3">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_check_interval']" matInput placeholder="Check interval" i18n-placeholder="Check interval input setting placeholder">
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']"><ng-container i18n="Use youtube-dl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist"><ng-container i18n="youtube-dl archive explanation prefix link">With youtube-dl's archive</ng-container></a>&nbsp;<ng-container i18n="youtube-dl archive explanation middle">feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory.</ng-container></p>
<p><ng-container i18n="youtube-dl archive explanation suffix">This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</ng-container></p>
</div>
</div>
</div>
<mat-divider></mat-divider>
@@ -140,7 +115,7 @@
</mat-form-field>
</div>
<div class="col-12 mt-5">
<div class="col-12 mt-4">
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
@@ -148,13 +123,20 @@
</mat-form-field>
</div>
<div class="col-12 mt-4">
<div class="col-12 mt-5">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p>Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head down to the Subscriptions section.</p>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['safe_download_override']"><ng-container i18n="Safe download override setting">Safe download override</ng-container></mat-checkbox>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<button (click)="killAllDownloads()" mat-stroked-button color="warn"><ng-container i18n="Kill all downloads button">Kill all downloads</ng-container></button>
</div>
</div>
</div>
@@ -186,10 +168,6 @@
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_multi_download_mode']"><ng-container i18n="Allow multi-download mode setting">Allow multi-download mode</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox [disabled]="new_config['Advanced']['multi_user_mode']" color="accent" [(ngModel)]="new_config['Extra']['settings_pin_required']"><ng-container i18n="Require pin for settings setting">Require pin for settings</ng-container></mat-checkbox>
<button style="margin-left: 15px; margin-bottom: 10px;" mat-stroked-button (click)="setNewPin()" [disabled]="!new_config['Extra']['settings_pin_required']"><ng-container i18n="Set new pin button">Set New Pin</ng-container></button>
</div>
</div>
</div>
<mat-divider></mat-divider>
@@ -206,7 +184,7 @@
</mat-form-field>
</div>
<div class="api-key-div">
<button matTooltip-i18n matTooltip="This will delete your old API key!" mat-stroked-button (click)="generateAPIKey()"><ng-container i18n="Generate key button">Generate</ng-container></button>
<button matTooltip="This will delete your old API key!" i18n-matTooltip="delete api key tooltip" mat-stroked-button (click)="generateAPIKey()"><ng-container i18n="Generate key button">Generate</ng-container></button>
</div>
</div>
</div>
@@ -285,6 +263,18 @@
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mt-2 mb-1">
<mat-form-field>
<mat-label><ng-container i18n="Login expiration select label">Login expiration</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['jwt_expiration']">
<mat-option [value]="3600">1 Hour</mat-option>
<mat-option [value]="86400">1 Day</mat-option>
<mat-option [value]="604800">1 Week</mat-option>
<mat-option [value]="2592000">1 Month</mat-option>
<mat-option [value]="31536000">1 Year</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['allow_advanced_download']"><ng-container i18n="Allow advanced downloading setting">Allow advanced download</ng-container></mat-checkbox>
</div>
@@ -306,8 +296,50 @@
</ng-template>
</mat-tab>
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">
<div style="margin-left: 48px; margin-top: 24px;">
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
<div style="margin-left: 48px; margin-top: 24px; margin-bottom: -25px;">
<div>
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
</div>
<mat-divider></mat-divider>
<mat-form-field style="margin-top: 15px;">
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
<mat-option value="internal">
<ng-container i18n="Internal auth method">Internal</ng-container>
</mat-option>
<mat-option value="ldap">
<ng-container i18n="LDAP auth method">LDAP</ng-container>
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
<div>
<mat-form-field>
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
</mat-form-field>
</div>
</div>
<mat-divider></mat-divider>
</div>
<app-modify-users></app-modify-users>
</mat-tab>

View File

@@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, EventEmitter } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { CheckOrSetPinDialogComponent } from 'app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
import { isoLangs } from './locales_list';
import { MatSnackBar } from '@angular/material/snack-bar';
import {DomSanitizer} from '@angular/platform-browser';
@@ -9,6 +8,7 @@ import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-
import { CURRENT_VERSION } from 'app/consts';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-settings',
@@ -77,14 +77,6 @@ export class SettingsComponent implements OnInit {
})
}
setNewPin() {
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
data: {
resetMode: true
}
});
}
generateAPIKey() {
this.postsService.generateNewAPIKey().subscribe(res => {
if (res['new_api_key']) {
@@ -163,6 +155,34 @@ export class SettingsComponent implements OnInit {
});
}
killAllDownloads() {
const done = new EventEmitter<any>();
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: 'Kill downloads',
dialogText: 'Are you sure you want to kill all downloads? Any subscription and non-subscription downloads will end immediately, though this operation may take a minute or so to complete.',
submitText: 'Kill all downloads',
doneEmitter: done
}
});
done.subscribe(confirmed => {
if (confirmed) {
this.postsService.killAllDownloads().subscribe(res => {
if (res['success']) {
dialogRef.close();
this.postsService.openSnackBar('Successfully killed all downloads!');
} else {
dialogRef.close();
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.');
}
}, err => {
dialogRef.close();
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.');
});
}
});
}
// snackbar helper
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {

View File

@@ -108,4 +108,4 @@ function fancyTimeFormat(time) {
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
ret += '' + secs;
return ret;
}
}

View File

@@ -42,5 +42,6 @@
</div>
</div>
</div>
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
</div>

View File

@@ -54,7 +54,13 @@
.save-button {
right: 25px;
position: absolute;
position: fixed;
bottom: 25px;
}
.edit-button {
left: 25px;
position: fixed;
bottom: 25px;
}

View File

@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
@Component({
selector: 'app-subscription',
@@ -43,9 +45,21 @@ export class SubscriptionComponent implements OnInit {
filterProperty = this.filterProperties['upload_date'];
downloading = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { }
initialized = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
ngOnInit() {
this.route.paramMap.subscribe((params: ParamMap) => {
this.id = params.get('id');
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.initialized = true;
this.getConfig();
this.getSubscription();
}
});
});
if (this.route.snapshot.paramMap.get('id')) {
this.id = this.route.snapshot.paramMap.get('id');
@@ -82,7 +96,7 @@ export class SubscriptionComponent implements OnInit {
}
getConfig() {
this.use_youtubedl_archive = this.postsService.config['Subscriptions']['subscriptions_use_youtubedl_archive'];
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
}
goToFile(emit_obj) {
@@ -121,7 +135,6 @@ export class SubscriptionComponent implements OnInit {
}
filterOptionChanged(value) {
// this.filterProperty = value;
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
}
@@ -148,4 +161,12 @@ export class SubscriptionComponent implements OnInit {
});
}
editSubscription() {
this.dialog.open(EditSubscriptionDialogComponent, {
data: {
sub: this.subscription
}
});
}
}

View File

@@ -80,6 +80,7 @@ export class SubscriptionsComponent implements OnInit {
} else {
this.channel_subscriptions.push(result);
}
this.postsService.reloadSubscriptions();
}
});
}
@@ -96,6 +97,7 @@ export class SubscriptionsComponent implements OnInit {
if (success) {
this.openSnackBar(`${sub.name} successfully deleted!`)
this.getSubscriptions();
this.postsService.reloadSubscriptions();
}
})
}

View File

@@ -4,15 +4,10 @@
"url": "http://localhost",
"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,
"use_youtubedl_archive": true,
"custom_args": "",
"safe_download_override": false
},
@@ -50,6 +45,7 @@
"custom_downloading_agent": "",
"multi_user_mode": true,
"allow_advanced_download": true,
"jwt_expiration": 86400,
"logger_level": "debug",
"use_cookies": true
}

View File

@@ -183,7 +183,7 @@
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",

View File

@@ -1772,7 +1772,7 @@
</trans-unit>
<trans-unit id="29c97c8e76763bb15b6d515648fa5bd1eb0f7510" datatype="html">
<source>User UID:</source>
<target xml:lang="de-DE">Benutzer-UID</target>
<target xml:lang="de-DE">Benutzer-UID:</target>
<note from="description" priority="1">User UID</note><context-group purpose="location">
<context context-type="sourcefile">app/components/manage-user/manage-user.component.html</context>
<context context-type="linenumber">4</context>

View File

@@ -16,6 +16,10 @@
<context context-type="sourcefile">app/create-playlist/create-playlist.component.html</context>
<context context-type="linenumber">5</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/modify-playlist/modify-playlist.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
<note priority="1" from="description">Playlist name placeholder</note>
</trans-unit>
<trans-unit id="f47e2d56dd8a145b2e9599da9730c049d52962a2" datatype="html">
@@ -102,7 +106,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">42</context>
<context context-type="linenumber">70</context>
</context-group>
<note priority="1" from="description">Arg modifier cancel button</note>
</trans-unit>
@@ -122,21 +126,13 @@
</context-group>
<note priority="1" from="description">Youtube downloader home page label</note>
</trans-unit>
<trans-unit id="6d2ec8898344c8955542b0542c942038ef28bb80" datatype="html">
<source>Please enter a valid URL!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
<note priority="1" from="description">Enter valid URL error</note>
</trans-unit>
<trans-unit id="a38ae1082fec79ba1f379978337385a539a28e73" datatype="html">
<source>
Quality
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">24</context>
<context context-type="linenumber">21</context>
</context-group>
<note priority="1" from="description">Quality select label</note>
</trans-unit>
@@ -144,7 +140,7 @@
<source>Use URL</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">52</context>
<context context-type="linenumber">49</context>
</context-group>
<note priority="1" from="description">YT search Use URL button for searched video</note>
</trans-unit>
@@ -154,7 +150,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">55</context>
<context context-type="linenumber">52</context>
</context-group>
<note priority="1" from="description">YT search View button for searched video</note>
</trans-unit>
@@ -164,7 +160,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">65</context>
<context context-type="linenumber">62</context>
</context-group>
<note priority="1" from="description">Only Audio checkbox</note>
</trans-unit>
@@ -174,7 +170,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">67</context>
</context-group>
<note priority="1" from="description">Multi-download Mode checkbox</note>
</trans-unit>
@@ -184,7 +180,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">79</context>
<context context-type="linenumber">76</context>
</context-group>
<note priority="1" from="description">Main download button</note>
</trans-unit>
@@ -194,7 +190,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">81</context>
</context-group>
<note priority="1" from="description">Cancel download button</note>
</trans-unit>
@@ -204,7 +200,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">96</context>
<context context-type="linenumber">93</context>
</context-group>
<note priority="1" from="description">Advanced download mode panel</note>
</trans-unit>
@@ -214,7 +210,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">102</context>
<context context-type="linenumber">99</context>
</context-group>
<note priority="1" from="description">Simulated command label</note>
</trans-unit>
@@ -224,7 +220,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">110</context>
<context context-type="linenumber">107</context>
</context-group>
<note priority="1" from="description">Use custom args checkbox</note>
</trans-unit>
@@ -232,12 +228,16 @@
<source>Custom args</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">113</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">145</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">48</context>
</context-group>
<note priority="1" from="description">Custom args placeholder</note>
</trans-unit>
<trans-unit id="a6911c2157f1b775284bbe9654ce5eb30cf45d7f" datatype="html">
@@ -246,7 +246,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">118</context>
<context context-type="linenumber">115</context>
</context-group>
<note priority="1" from="description">Custom Args input hint</note>
</trans-unit>
@@ -256,7 +256,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">123</context>
</context-group>
<note priority="1" from="description">Use custom output checkbox</note>
</trans-unit>
@@ -264,7 +264,7 @@
<source>Custom output</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">128</context>
</context-group>
<note priority="1" from="description">Custom output placeholder</note>
</trans-unit>
@@ -272,7 +272,11 @@
<source>Documentation</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">133</context>
<context context-type="linenumber">130</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">60</context>
</context-group>
<note priority="1" from="description">Youtube-dl output template documentation link</note>
</trans-unit>
@@ -280,7 +284,11 @@
<source>Path is relative to the config download path. Don&apos;t include extension.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">61</context>
</context-group>
<note priority="1" from="description">Custom Output input hint</note>
</trans-unit>
@@ -290,7 +298,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">140</context>
<context context-type="linenumber">137</context>
</context-group>
<note priority="1" from="description">Use authentication checkbox</note>
</trans-unit>
@@ -298,7 +306,7 @@
<source>Username</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">145</context>
<context context-type="linenumber">142</context>
</context-group>
<note priority="1" from="description">YT Username placeholder</note>
</trans-unit>
@@ -306,7 +314,7 @@
<source>Password</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">150</context>
<context context-type="linenumber">147</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html</context>
@@ -324,7 +332,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">194</context>
<context context-type="linenumber">191</context>
</context-group>
<note priority="1" from="description">Audio files title</note>
</trans-unit>
@@ -334,7 +342,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">199</context>
<context context-type="linenumber">196</context>
</context-group>
<note priority="1" from="description">Audio files description</note>
</trans-unit>
@@ -342,11 +350,11 @@
<source>Playlists</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">211</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">253</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/subscriptions/subscriptions.component.html</context>
@@ -360,7 +368,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">225</context>
<context context-type="linenumber">222</context>
</context-group>
<note priority="1" from="description">No video playlists available text</note>
</trans-unit>
@@ -370,7 +378,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">232</context>
</context-group>
<note priority="1" from="description">Video files title</note>
</trans-unit>
@@ -380,7 +388,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">237</context>
</context-group>
<note priority="1" from="description">Video files description</note>
</trans-unit>
@@ -390,7 +398,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">269</context>
<context context-type="linenumber">266</context>
</context-group>
<note priority="1" from="description">No video playlists available text</note>
</trans-unit>
@@ -456,6 +464,10 @@
<context context-type="sourcefile">app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">31</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">27</context>
@@ -486,6 +498,14 @@
</context-group>
<note priority="1" from="description">Close subscription info button</note>
</trans-unit>
<trans-unit id="4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1" datatype="html">
<source>Modify playlist</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/modify-playlist/modify-playlist.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Modify playlist dialog title</note>
</trans-unit>
<trans-unit id="ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1" datatype="html">
<source>ID:</source>
<context-group purpose="location">
@@ -510,11 +530,31 @@
</context-group>
<note priority="1" from="description">Playlist video count</note>
</trans-unit>
<trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
<source>Edit</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Playlist edit button</note>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">Delete playlist</note>
</trans-unit>
<trans-unit id="321e4419a943044e674beb55b8039f42a9761ca5" datatype="html">
<source>Info</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">20</context>
<context context-type="linenumber">24</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/subscription/subscription-file-card/subscription-file-card.component.html</context>
@@ -522,22 +562,38 @@
</context-group>
<note priority="1" from="description">Video info button</note>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">21</context>
</context-group>
<note priority="1" from="description">Delete video button</note>
</trans-unit>
<trans-unit id="34504b488c24c27e68089be549f0eeae6ebaf30b" datatype="html">
<source>Delete and blacklist</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Delete and blacklist video button</note>
</trans-unit>
<trans-unit id="ebadf946ae90f13ecd0c70f09edbc0f983af8a0f" datatype="html">
<source>Upload new cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Cookies uploader dialog title</note>
</trans-unit>
<trans-unit id="98a8a42e5efffe17ab786636ed0139b4c7032d0e" datatype="html">
<source>Drag and Drop</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Drag and Drop</note>
</trans-unit>
<trans-unit id="85e0725c870b28458fd3bbba905397d890f00a69" datatype="html">
<source>NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Cookies upload warning</note>
</trans-unit>
<trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
<source>Settings</source>
<context-group purpose="location">
@@ -802,6 +858,14 @@
</context-group>
<note priority="1" from="description">Custom args setting input hint</note>
</trans-unit>
<trans-unit id="d01715b75228878a773ae6d059acc639d4898a03" datatype="html">
<source>Safe download override</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">157</context>
</context-group>
<note priority="1" from="description">Safe download override setting</note>
</trans-unit>
<trans-unit id="0ba25ad86a240576c4f20a2fada4722ebba77b1e" datatype="html">
<source>Downloader</source>
<context-group purpose="location">
@@ -814,7 +878,7 @@
<source>Top title</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">166</context>
<context context-type="linenumber">170</context>
</context-group>
<note priority="1" from="description">Top title input placeholder</note>
</trans-unit>
@@ -822,7 +886,7 @@
<source>File manager enabled</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">171</context>
<context context-type="linenumber">175</context>
</context-group>
<note priority="1" from="description">File manager enabled setting</note>
</trans-unit>
@@ -830,7 +894,7 @@
<source>Downloads manager enabled</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">174</context>
<context context-type="linenumber">178</context>
</context-group>
<note priority="1" from="description">Downloads manager enabled setting</note>
</trans-unit>
@@ -838,7 +902,7 @@
<source>Allow quality select</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">177</context>
<context context-type="linenumber">181</context>
</context-group>
<note priority="1" from="description">Allow quality seelct setting</note>
</trans-unit>
@@ -846,7 +910,7 @@
<source>Download only mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">180</context>
<context context-type="linenumber">184</context>
</context-group>
<note priority="1" from="description">Download only mode setting</note>
</trans-unit>
@@ -854,7 +918,7 @@
<source>Allow multi-download mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">187</context>
</context-group>
<note priority="1" from="description">Allow multi-download mode setting</note>
</trans-unit>
@@ -862,7 +926,7 @@
<source>Require pin for settings</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">186</context>
<context context-type="linenumber">190</context>
</context-group>
<note priority="1" from="description">Require pin for settings setting</note>
</trans-unit>
@@ -870,7 +934,7 @@
<source>Set New Pin</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">187</context>
<context context-type="linenumber">191</context>
</context-group>
<note priority="1" from="description">Set new pin button</note>
</trans-unit>
@@ -878,7 +942,7 @@
<source>Enable Public API</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">195</context>
<context context-type="linenumber">199</context>
</context-group>
<note priority="1" from="description">Enable Public API key setting</note>
</trans-unit>
@@ -886,7 +950,7 @@
<source>Public API Key</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">204</context>
</context-group>
<note priority="1" from="description">Public API Key setting placeholder</note>
</trans-unit>
@@ -894,7 +958,7 @@
<source>View documentation</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">205</context>
</context-group>
<note priority="1" from="description">View API docs setting hint</note>
</trans-unit>
@@ -902,7 +966,7 @@
<source>Generate</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">205</context>
<context context-type="linenumber">209</context>
</context-group>
<note priority="1" from="description">Generate key button</note>
</trans-unit>
@@ -910,7 +974,7 @@
<source>Use YouTube API</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">218</context>
</context-group>
<note priority="1" from="description">Use YouTube API setting</note>
</trans-unit>
@@ -918,7 +982,7 @@
<source>Youtube API Key</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">218</context>
<context context-type="linenumber">222</context>
</context-group>
<note priority="1" from="description">Youtube API Key setting placeholder</note>
</trans-unit>
@@ -926,7 +990,7 @@
<source>Generating a key is easy!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">219</context>
<context context-type="linenumber">223</context>
</context-group>
<note priority="1" from="description">Youtube API Key setting hint</note>
</trans-unit>
@@ -934,11 +998,11 @@
<source>Click here</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">233</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">239</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
@@ -950,7 +1014,7 @@
<source>to download the official YoutubeDL-Material Chrome extension manually.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">233</context>
</context-group>
<note priority="1" from="description">Chrome click here suffix</note>
</trans-unit>
@@ -958,7 +1022,7 @@
<source>You must manually load the extension and modify the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">230</context>
<context context-type="linenumber">234</context>
</context-group>
<note priority="1" from="description">Chrome setup suffix</note>
</trans-unit>
@@ -966,7 +1030,7 @@
<source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">239</context>
</context-group>
<note priority="1" from="description">Firefox click here suffix</note>
</trans-unit>
@@ -974,7 +1038,7 @@
<source>Detailed setup instructions.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">236</context>
<context context-type="linenumber">240</context>
</context-group>
<note priority="1" from="description">Firefox setup prefix link</note>
</trans-unit>
@@ -982,7 +1046,7 @@
<source>Not much is required other than changing the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">236</context>
<context context-type="linenumber">240</context>
</context-group>
<note priority="1" from="description">Firefox setup suffix</note>
</trans-unit>
@@ -990,7 +1054,7 @@
<source>Drag the link below to your bookmarks, and you&apos;re good to go! Just navigate to the YouTube video you&apos;d like to download, and click the bookmark.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">241</context>
<context context-type="linenumber">245</context>
</context-group>
<note priority="1" from="description">Bookmarklet instructions</note>
</trans-unit>
@@ -998,7 +1062,7 @@
<source>Generate &apos;audio only&apos; bookmarklet</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">242</context>
<context context-type="linenumber">246</context>
</context-group>
<note priority="1" from="description">Generate audio only bookmarklet checkbox</note>
</trans-unit>
@@ -1006,7 +1070,7 @@
<source>Extra</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">164</context>
</context-group>
<note priority="1" from="description">Extra settings label</note>
</trans-unit>
@@ -1014,7 +1078,7 @@
<source>Use default downloading agent</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">260</context>
</context-group>
<note priority="1" from="description">Use default downloading agent setting</note>
</trans-unit>
@@ -1022,27 +1086,47 @@
<source>Select a downloader</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">260</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">274</context>
<context context-type="linenumber">264</context>
</context-group>
<note priority="1" from="description">Custom downloader select label</note>
</trans-unit>
<trans-unit id="00e274c496b094a019f0679c3fab3945793f3335" datatype="html">
<source>Select a logger level</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">278</context>
</context-group>
<note priority="1" from="description">Logger level select label</note>
</trans-unit>
<trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html">
<source>Allow advanced download</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">285</context>
<context context-type="linenumber">289</context>
</context-group>
<note priority="1" from="description">Allow advanced downloading setting</note>
</trans-unit>
<trans-unit id="431e5f3a0dde88768d1074baedd65266412b3f02" datatype="html">
<source>Use Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">297</context>
</context-group>
<note priority="1" from="description">Use cookies setting</note>
</trans-unit>
<trans-unit id="80651a7ad1229ea6613557d3559f702cfa5aecf5" datatype="html">
<source>Set Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">298</context>
</context-group>
<note priority="1" from="description">Set cookies button</note>
</trans-unit>
<trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
<source>Advanced</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">251</context>
<context context-type="linenumber">255</context>
</context-group>
<note priority="1" from="description">Host settings label</note>
</trans-unit>
@@ -1050,7 +1134,7 @@
<source>Allow user registration</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">297</context>
<context context-type="linenumber">310</context>
</context-group>
<note priority="1" from="description">Allow registration setting</note>
</trans-unit>
@@ -1058,15 +1142,23 @@
<source>Users</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">295</context>
<context context-type="linenumber">308</context>
</context-group>
<note priority="1" from="description">Users settings label</note>
</trans-unit>
<trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
<source>Logs</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">314</context>
</context-group>
<note priority="1" from="description">Logs settings label</note>
</trans-unit>
<trans-unit id="52c9a103b812f258bcddc3d90a6e3f46871d25fe" datatype="html">
<source>Save</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">307</context>
<context context-type="linenumber">327</context>
</context-group>
<note priority="1" from="description">Settings save button</note>
</trans-unit>
@@ -1074,7 +1166,7 @@
<source>{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">310</context>
<context context-type="linenumber">330</context>
</context-group>
<note priority="1" from="description">Settings cancel and close button</note>
</trans-unit>
@@ -1188,6 +1280,10 @@
<context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">44</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/components/login/login.component.html</context>
<context context-type="linenumber">15</context>
@@ -1254,7 +1350,7 @@
<source>Subscriptions</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Navigation menu Subscriptions Page title</note>
</trans-unit>
@@ -1262,7 +1358,7 @@
<source>Downloads</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">45</context>
<context context-type="linenumber">46</context>
</context-group>
<note priority="1" from="description">Navigation menu Downloads Page title</note>
</trans-unit>
@@ -1320,6 +1416,10 @@
<context context-type="sourcefile">app/dialogs/share-media-dialog/share-media-dialog.component.html</context>
<context context-type="linenumber">24</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/components/logs-viewer/logs-viewer.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
<note priority="1" from="description">Copy to clipboard button</note>
</trans-unit>
<trans-unit id="5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f" datatype="html">
@@ -1390,23 +1490,15 @@
<source>Custom name</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">14</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Subscription custom name placeholder</note>
</trans-unit>
<trans-unit id="f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7" datatype="html">
<source>This is optional</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">15</context>
</context-group>
<note priority="1" from="description">Custom name input hint</note>
</trans-unit>
<trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html">
<source>Download all uploads</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">19</context>
<context context-type="linenumber">23</context>
</context-group>
<note priority="1" from="description">Download all uploads subscription setting</note>
</trans-unit>
@@ -1414,23 +1506,47 @@
<source>Download videos uploaded in the last</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Download time range prefix</note>
</trans-unit>
<trans-unit id="c76a955642714b8949ff3e4b4990864a2e2cac95" datatype="html">
<source>Audio-only mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">38</context>
</context-group>
<note priority="1" from="description">Streaming-only mode</note>
</trans-unit>
<trans-unit id="408ca4911457e84a348cecf214f02c69289aa8f1" datatype="html">
<source>Streaming-only mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">43</context>
</context-group>
<note priority="1" from="description">Streaming-only mode</note>
</trans-unit>
<trans-unit id="f432e1a8d6adb12e612127978ce2e0ced933959c" datatype="html">
<source>These are added after the standard args.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Custom args hint</note>
</trans-unit>
<trans-unit id="98b6ec9ec138186d663e64770267b67334353d63" datatype="html">
<source>Custom file output</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
<note priority="1" from="description">Subscription custom file output placeholder</note>
</trans-unit>
<trans-unit id="d0336848b0c375a1c25ba369b3481ee383217a4f" datatype="html">
<source>Subscribe</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">72</context>
</context-group>
<note priority="1" from="description">Subscribe button</note>
</trans-unit>
@@ -1594,7 +1710,7 @@
<source>No downloads available!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">No downloads label</note>
</trans-unit>
@@ -1726,6 +1842,22 @@
</context-group>
<note priority="1" from="description">Edit role</note>
</trans-unit>
<trans-unit id="fd59fb984749fcdb5e386ae85faec82f8e5ac098" datatype="html">
<source>Logs will appear here</source>
<context-group purpose="location">
<context context-type="sourcefile">app/components/logs-viewer/logs-viewer.component.html</context>
<context context-type="linenumber">5</context>
</context-group>
<note priority="1" from="description">Logs placeholder</note>
</trans-unit>
<trans-unit id="5009630cdf32ab4f1c78737b9617b8773512c05a" datatype="html">
<source>Lines:</source>
<context-group purpose="location">
<context context-type="sourcefile">app/components/logs-viewer/logs-viewer.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<note priority="1" from="description">Label for lines select in logger view</note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -196,5 +196,20 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: Cargar nuevas cookies anulará sus cookies anteriores. También tenga en cuenta que las cookies son de toda la instancia, no por usuario.",
"d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura",
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
}

View File

@@ -3,6 +3,8 @@ const THEMES_CONFIG = {
'key': 'default',
'background_color': 'ghostwhite',
'alternate_color': 'gray',
'ghost_primary': '#f9f9f9',
'ghost_secondary': '#ecebeb',
'css_label': 'default-theme',
'social_theme': 'material-light'
},
@@ -10,12 +12,16 @@ const THEMES_CONFIG = {
'key': 'dark',
'background_color': '#141414',
'alternate_color': '#695959',
'ghost_primary': '#444444',
'ghost_secondary': '#141414',
'css_label': 'dark-theme',
'social_theme': 'material-dark'
},
'light': {
'key': 'light',
'background_color': 'white',
'ghost_primary': '#f9f9f9',
'ghost_secondary': '#ecebeb',
'css_label': 'light-theme',
'social_theme': 'material-light'
}