Compare commits

...

119 Commits

Author SHA1 Message Date
Isaac Abadi
dc0ae1dbcc Moved hooks to proper location 2020-08-08 16:18:24 -04:00
Isaac Abadi
d5955f6a4c Updated arm dockerfile 2020-08-08 16:10:21 -04:00
Isaac Abadi
8d8ccc66dd Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into dockertest 2020-08-08 16:08:54 -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
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
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
f5215baa55 Updated arm dockerfile 2020-07-18 22:35:06 -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
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
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
Isaac Grynsztein
a9f197e46d Updated logs viewer component
- now by default last 50 lines are showed
- added copy to clipboard button
- added loading spinner to indicate to users when the logs are loading

app.get('/api/logs') is now app.post to allow for additional parameters (such as lines to retrieve)
2020-07-03 03:46:58 -04:00
Isaac Grynsztein
3732d13562 Implemented greater transparency for login/registration errors on frontend 2020-07-02 17:42:05 -04:00
Isaac Grynsztein
cf14880d21 Empty URL setting will result in the default being applied 2020-07-01 23:20:11 -04:00
Isaac Grynsztein
e81d0cab42 Fixed bug where changing a user's password would change the admin's password 2020-07-01 17:26:32 -04:00
Isaac Grynsztein
7e24180f03 Fixed bug in globalArgsRequiresSafeDownload function 2020-06-30 23:03:07 -04:00
Isaac Grynsztein
053c8db9dd Fixed bug where config api would call itself 2020-06-30 22:55:08 -04:00
Isaac Grynsztein
5537852134 Deleting a file will now delete its downloaded thumbnail as well
Thumbnails will now have their permissions auto updated to align themselves with the other downloaded files
2020-06-30 22:38:01 -04:00
Isaac Grynsztein
efdc471ccf Fixed bug where if multi-user mode was enabled, old subscriptions would keep downloading and vice versa 2020-06-29 23:23:31 -04:00
Tzahi12345
6f1b37d5eb Merge pull request #149 from Tzahi12345/player-improvements
Playlist and player improvements
2020-06-29 20:30:31 -04:00
Isaac Grynsztein
06557673a2 Updated frontend binaries 2020-06-29 20:20:07 -04:00
Isaac Grynsztein
c20d09e902 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into player-improvements 2020-06-29 20:11:20 -04:00
Isaac Grynsztein
a68ecfa730 Modifying playlist in dialog will now update the file manager automatically 2020-06-29 20:06:37 -04:00
Isaac Grynsztein
86c609c1b2 Player component now remembers previously set volume
Updated name of updatePlaylist->updatePlaylistFiles for clarity and added updatePlaylist route

Added smarter safe download override, will auto activate if subtitle args are included.
2020-06-29 19:39:47 -04:00
Isaac Grynsztein
d100e80ccf Added ability to clear all downloads in a session 2020-06-29 19:35:59 -04:00
Isaac Grynsztein
5511c94071 Added modify playlist component 2020-06-29 19:35:34 -04:00
Isaac Grynsztein
b21886d8f8 Rebuilt frontend binaries and included Deutsch translation in public directory 2020-06-29 18:45:40 -04:00
Tzahi12345
e535603103 Merge pull request #145 from UnlimitedCookies/master
Add German Translation
2020-06-29 18:21:06 -04:00
UnlimitedCookies
9415901f17 Revert to 1:1 translation 2020-06-29 18:58:46 +02:00
UnlimitedCookies
92e5716f93 More clarification 2020-06-29 17:03:07 +02:00
UnlimitedCookies
5b5c93f783 Minor changes to increase coherence 2020-06-29 16:42:05 +02:00
UnlimitedCookies
4db6a49df5 Make custom Arg description more clear
Co-authored-by: Sandro <sandro.jaeckel@gmail.com>
2020-06-29 16:12:51 +02:00
Isaac Grynsztein
3abb29eee4 Removed exe binaries from repo 2020-06-29 02:28:00 -04:00
Isaac Grynsztein
e5461de2f7 Ignore youtube-dl cookies and executable files from the repo 2020-06-29 02:26:55 -04:00
UnlimitedCookies
4a69a0d362 modified: src/app/settings/settings.component.ts
new file:   src/assets/i18n/messages.de.json
	new file:   src/assets/i18n/messages.de.xlf
2020-06-29 03:48:29 +02:00
Tzahi12345
97f7f0b462 Merge pull request #141 from web-connect/feature/remove_whitespace
Removing extra white spaces
2020-06-27 12:43:44 -04:00
Tzahi12345
d3cbfa265e Merge pull request #140 from web-connect/feature/readme_update
Updating README regarding output dir
2020-06-27 12:42:30 -04:00
Justin Turner
42bd219ed6 Removing extra white spaces 2020-06-27 01:09:41 -05:00
Justin Turner
f8123cf03b Updating output dir 2020-06-27 00:55:55 -05:00
Isaac Grynsztein
94df98e5d0 Fixed bug that prevented subscription archives from being downloaded if their path was express as a full path 2020-06-23 00:01:23 -04:00
Isaac Grynsztein
2998562655 Added the ability to view logs from the settings menu 2020-06-22 23:15:21 -04:00
Tzahi12345
09d8ce04d7 Merge pull request #128 from web-connect/bug/i127_nan_not_found
Fixes #127 by adding nan to dependencies
2020-06-22 18:33:35 -04:00
Tzahi12345
504c818c2f Merge pull request #136 from Tzahi12345/subscriptions-custom-path
Subscriptions V3
2020-06-22 00:11:12 -04:00
Isaac Grynsztein
ca0e6b993d Re-compiled frontend 2020-06-22 00:03:42 -04:00
Isaac Grynsztein
0346833c3b Merged changes from master 2020-06-21 23:54:18 -04:00
Isaac Grynsztein
32da9dd9dd format in custom args for subscriptions now overrides default format (allows for users to specify custom formats for subs) 2020-06-21 23:49:00 -04:00
Isaac Grynsztein
20f162d794 Added args modifier dialog to custom args input in the subscribe dialog 2020-06-21 23:40:39 -04:00
Isaac Grynsztein
319bb0160b Finished adding support for audio subscriptions, custom args for subscriptions, and custom output for subscription downloads 2020-06-21 23:27:14 -04:00
Tzahi12345
5983a8bd52 Merge pull request #129 from web-connect/feature/i100_UI-typo-for-logger-level
#100 Typo fix for logger
2020-06-13 16:56:02 -04:00
Justin Turner
49b8cd416e Typo fix for logger 2020-06-13 13:39:42 -05:00
Justin Turner
58f71469b5 Fixes #127 by adding nan to dependencies 2020-06-13 01:21:03 -05:00
Tzahi12345
db81120645 Added audioOnlyMode, customArgs, and customFileOutput fields to the subscribe dialog 2020-06-12 17:57:34 -04:00
Tzahi12345
163a88bcfd DB implementation of subs now can properly delete subs 2020-06-10 21:41:05 -04:00
Tzahi12345
2441270d88 Removed redundant redirect when in the login screen
Fixed bug that prevented user registration with a faulty token
2020-06-09 21:19:42 -04:00
Tzahi12345
a518ac680f Fixed bug that prevented new users from accessing the login screen 2020-06-09 18:12:55 -04:00
Tzahi12345
78d3145e0b Deleting a video with an extension in the filename will now work UI-side 2020-06-09 18:02:46 -04:00
Tzahi12345
b8a4e0773f Added new utils.js module to assist backend with shared helper functions
Subscription files are now stored in the database, and will be primarily managed through it
2020-06-09 18:02:25 -04:00
Tzahi12345
f04139634a Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into subscriptions-custom-path 2020-06-08 21:24:46 -04:00
Tzahi12345
a074166903 Added catch statement if youtube-dl tags could not be retrieved 2020-06-08 13:04:16 -04:00
Tzahi12345
6893dbd506 Merge pull request #118 from Tzahi12345/Windows-build-fix-for-new-dockerfile
Updated Dockerfile to support Windows builds
2020-06-06 13:08:45 -04:00
Tzahi12345
e8ee4ffb64 Made additional cleanups as per recs by SuperSandro 2020-06-06 13:07:50 -04:00
Tzahi12345
378025bd9d Updated dockerfile to support Windows builds 2020-06-03 20:56:48 -04:00
Tzahi12345
d8e85df6d6 Scaffolding for registering subscription downloads 2020-06-03 19:18:10 -04:00
Tzahi12345
0c864c3d8d Merge pull request #117 from SuperSandro2000/docker-fix
Docker: Fix startup error in entrypoint, ownership of node_modules
2020-06-03 09:46:58 -04:00
Sandro Jäckel
dd8ab9be29 Fix default uid/gid of node_modules 2020-06-03 07:12:14 +02:00
Sandro Jäckel
bab354ce81 Fix variable expansion 2020-06-03 07:11:30 +02:00
Tzahi12345
d3d0f92ea5 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into subscriptions-custom-path 2020-06-02 23:24:54 -04:00
Tzahi12345
b37d912e04 Merge pull request #116 from SuperSandro2000/docker-non-root
Run docker as non root
2020-06-02 21:47:22 -04:00
Tzahi12345
1c93a4f9f2 Updated frontend files to support video sharing with non-users in multi user mode 2020-06-02 20:09:40 -04:00
Tzahi12345
abfe0dad03 Prevents login redirect for shared videos in multi user mode 2020-06-02 20:03:01 -04:00
Sandro Jäckel
5bfecfcefe Run docker as non root, copy package-json.lock 2020-06-03 01:53:06 +02:00
Tzahi12345
ffe3133635 Merge pull request #115 from SuperSandro2000/alpine-12
Docker: Update Alpine to 3.12
2020-06-02 17:43:57 -04:00
Sandro Jäckel
68c67ca7d5 Update alpine to 3.12 2020-06-02 23:34:05 +02:00
Sandro Jäckel
c4d50c9018 Format 2020-06-02 23:32:16 +02:00
Isaac Grynsztein
42b749a101 Updated frontend binaries 2020-05-30 16:39:11 -04:00
Isaac Grynsztein
9c729abfaa Added new safe download override setting to config manager (forgot to do this before) 2020-05-30 16:30:28 -04:00
Isaac Grynsztein
dcc7fbd81c Added new setting to force a safe download (removes features like progress bar) 2020-05-30 16:28:00 -04:00
Isaac Grynsztein
b3c8f9e57a Fixed bug that caused downloads to fail when archiving was enabled
Removed error message on URL input on the home page

Fixed bug that prevented file deletion in multi user mode with archiving enabled
2020-05-30 16:20:03 -04:00
Tzahi12345
9e5ad66a9d Added scaffolding for custom paths in subscriptions 2020-05-10 04:53:49 -04:00
88 changed files with 4432 additions and 1134 deletions

2
.gitignore vendored
View File

@@ -53,6 +53,7 @@ backend/public/assets/default.json
backend/subscriptions/channels/* backend/subscriptions/channels/*
backend/subscriptions/playlists/* backend/subscriptions/playlists/*
backend/subscriptions/archives/* backend/subscriptions/archives/*
backend/*.exe
src/assets/default.json src/assets/default.json
backend/appdata/db.json backend/appdata/db.json
backend/appdata/archives/archive_audio.txt backend/appdata/archives/archive_audio.txt
@@ -63,3 +64,4 @@ backend/appdata/logs/combined.log
backend/appdata/logs/error.log backend/appdata/logs/error.log
backend/appdata/users.json backend/appdata/users.json
backend/users/* backend/users/*
backend/appdata/cookies.txt

View File

@@ -53,41 +53,13 @@ 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. 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 ## 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. If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend. To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` folder. Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`. The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
@@ -97,14 +69,24 @@ Alternatively, you can port forward the port specified in the config (defaults t
## Docker ## 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. 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. 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. Unzip the `youtubedl-material-docker.zip` and navigate into the root folder. 2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
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. 3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
4. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image. 4. Make sure you can connect to the specified URL + port, and if so, you are done!
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! ### 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
@@ -116,12 +98,20 @@ Once you have enabled the API and have the key, you can start sending requests b
## Contributing ## 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 ## Authors
* **Isaac Grynsztein** (me!) - *Initial work* * **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. See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
## License ## License

View File

@@ -1,18 +1,27 @@
FROM alpine:3.11 FROM alpine:3.12
RUN \ ENV UID=1000 \
apk add --no-cache npm python ffmpeg && \ GID=1000 \
apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ 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 atomicparsley
WORKDIR /app WORKDIR /app
COPY --chown=$UID:$GID [ "package.json", "package-lock.json", "/app/" ]
COPY package.json /app/ RUN npm install && chown -R $UID:$GID ./
RUN npm install COPY --chown=$UID:$GID [ "./", "/app/" ]
COPY ./ /app/
EXPOSE 17442 EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ] CMD [ "node", "app.js" ]

29
backend/Dockerfile-armhf Normal file
View File

@@ -0,0 +1,29 @@
FROM arm32v7/alpine:3.12
COPY 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 [ "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

@@ -13,6 +13,8 @@ var express = require("express");
var bodyParser = require("body-parser"); var bodyParser = require("body-parser");
var archiver = require('archiver'); var archiver = require('archiver');
var unzipper = require('unzipper'); var unzipper = require('unzipper');
var db_api = require('./db')
var utils = require('./utils')
var mergeFiles = require('merge-files'); var mergeFiles = require('merge-files');
const low = require('lowdb') const low = require('lowdb')
var ProgressBar = require('progress'); var ProgressBar = require('progress');
@@ -27,6 +29,8 @@ var config_api = require('./config.js');
var subscriptions_api = require('./subscriptions') var subscriptions_api = require('./subscriptions')
const CONSTS = require('./consts') const CONSTS = require('./consts')
const { spawn } = require('child_process') const { spawn } = require('child_process')
const read_last_lines = require('read-last-lines');
var ps = require('ps-node');
const is_windows = process.platform === 'win32'; const is_windows = process.platform === 'win32';
@@ -73,8 +77,9 @@ const logger = winston.createLogger({
}); });
config_api.initialize(logger); config_api.initialize(logger);
subscriptions_api.initialize(db, users_db, logger);
auth_api.initialize(users_db, logger); auth_api.initialize(users_db, logger);
db_api.initialize(db, users_db, logger);
subscriptions_api.initialize(db, users_db, logger, db_api);
// var GithubContent = require('github-content'); // var GithubContent = require('github-content');
@@ -92,7 +97,6 @@ db.defaults(
configWriteFlag: false, configWriteFlag: false,
downloads: {}, downloads: {},
subscriptions: [], subscriptions: [],
pin_md5: '',
files_to_db_migration_complete: false files_to_db_migration_complete: false
}).write(); }).write();
@@ -191,27 +195,12 @@ app.use(bodyParser.json());
// use passport // use passport
app.use(auth_api.passport.initialize()); app.use(auth_api.passport.initialize());
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
this.id = id;
this.title = title;
this.thumbnailURL = thumbnailURL;
this.isAudio = isAudio;
this.duration = duration;
this.url = url;
this.uploader = uploader;
this.size = size;
this.path = path;
this.upload_date = upload_date;
}
// actual functions // actual functions
async function checkMigrations() { async function checkMigrations() {
return new Promise(async resolve => { return new Promise(async resolve => {
// 3.5->3.6 migration // 3.5->3.6 migration
const files_to_db_migration_complete = db.get('files_to_db_migration_complete').value(); const files_to_db_migration_complete = true; // migration phased out! previous code: db.get('files_to_db_migration_complete').value();
if (!files_to_db_migration_complete) { if (!files_to_db_migration_complete) {
logger.info('Beginning migration: 3.5->3.6+') logger.info('Beginning migration: 3.5->3.6+')
@@ -236,7 +225,7 @@ async function runFilesToDBMigration() {
const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value(); const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value();
if (!file_already_in_db) { if (!file_already_in_db) {
logger.verbose(`Migrating file ${file_obj.id}`); logger.verbose(`Migrating file ${file_obj.id}`);
registerFileDB(file_obj.id + '.mp3', 'audio'); db_api.registerFileDB(file_obj.id + '.mp3', 'audio');
} }
} }
@@ -245,7 +234,7 @@ async function runFilesToDBMigration() {
const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value(); const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value();
if (!file_already_in_db) { if (!file_already_in_db) {
logger.verbose(`Migrating file ${file_obj.id}`); logger.verbose(`Migrating file ${file_obj.id}`);
registerFileDB(file_obj.id + '.mp4', 'video'); db_api.registerFileDB(file_obj.id + '.mp4', 'video');
} }
} }
@@ -515,6 +504,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() { async function setPortItemFromENV() {
return new Promise(resolve => { return new Promise(resolve => {
config_api.setConfigItem('ytdl_port', backendPort.toString()); config_api.setConfigItem('ytdl_port', backendPort.toString());
@@ -605,6 +631,8 @@ function loadConfigValues() {
}; };
} }
// empty url defaults to default URL
if (!url || url === '') url = 'http://example.com'
url_domain = new URL(url); url_domain = new URL(url);
let logger_level = config_api.getConfigItem('ytdl_logger_level'); let logger_level = config_api.getConfigItem('ytdl_logger_level');
@@ -654,8 +682,18 @@ async function watchSubscriptions() {
continue; continue;
} }
if (!sub.name) {
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
continue;
}
logger.verbose('Watching ' + sub.name + ' with delay interval of ' + delay_interval); logger.verbose('Watching ' + sub.name + ' with delay interval of ' + delay_interval);
setTimeout(async () => { setTimeout(async () => {
const multiUserModeChanged = config_api.getConfigItem('ytdl_multi_user_mode') !== multiUserMode;
if (multiUserModeChanged) {
logger.verbose(`Skipping subscription ${sub.name} due to multi-user mode change.`);
return;
}
await subscriptions_api.getVideosForSub(sub, sub.user_uid); await subscriptions_api.getVideosForSub(sub, sub.user_uid);
subscription_timeouts[sub.id] = false; subscription_timeouts[sub.id] = false;
}, current_delay); }, current_delay);
@@ -700,7 +738,7 @@ function getMp3s() {
var stats = fs.statSync(file); var stats = fs.statSync(file);
var id = file_path.substring(0, file_path.length-4); var id = file_path.substring(0, file_path.length-4);
var jsonobj = getJSONMp3(id); var jsonobj = utils.getJSONMp3(id, audioFolderPath);
if (!jsonobj) continue; if (!jsonobj) continue;
var title = jsonobj.title; var title = jsonobj.title;
var url = jsonobj.webpage_url; var url = jsonobj.webpage_url;
@@ -713,7 +751,7 @@ function getMp3s() {
var thumbnail = jsonobj.thumbnail; var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration; var duration = jsonobj.duration;
var isaudio = true; var isaudio = true;
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
mp3s.push(file_obj); mp3s.push(file_obj);
} }
return mp3s; return mp3s;
@@ -729,7 +767,7 @@ function getMp4s(relative_path = true) {
var stats = fs.statSync(file); var stats = fs.statSync(file);
var id = file_path.substring(0, file_path.length-4); var id = file_path.substring(0, file_path.length-4);
var jsonobj = getJSONMp4(id); var jsonobj = utils.getJSONMp4(id, videoFolderPath);
if (!jsonobj) continue; if (!jsonobj) continue;
var title = jsonobj.title; var title = jsonobj.title;
var url = jsonobj.webpage_url; var url = jsonobj.webpage_url;
@@ -742,7 +780,7 @@ function getMp4s(relative_path = true) {
var size = stats.size; var size = stats.size;
var isaudio = false; var isaudio = false;
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date); var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
mp4s.push(file_obj); mp4s.push(file_obj);
} }
return mp4s; return mp4s;
@@ -750,14 +788,14 @@ function getMp4s(relative_path = true) {
function getThumbnailMp3(name) function getThumbnailMp3(name)
{ {
var obj = getJSONMp3(name); var obj = utils.getJSONMp3(name, audioFolderPath);
var thumbnailLink = obj.thumbnail; var thumbnailLink = obj.thumbnail;
return thumbnailLink; return thumbnailLink;
} }
function getThumbnailMp4(name) function getThumbnailMp4(name)
{ {
var obj = getJSONMp4(name); var obj = utils.getJSONMp4(name, videoFolderPath);
var thumbnailLink = obj.thumbnail; var thumbnailLink = obj.thumbnail;
return thumbnailLink; return thumbnailLink;
} }
@@ -794,54 +832,6 @@ function getFileSizeMp4(name)
return filesize; return filesize;
} }
function getJSONMp3(name, customPath = null, openReadPerms = false)
{
var jsonPath = audioFolderPath+name+".info.json";
var alternateJsonPath = audioFolderPath+name+".mp3.info.json";
if (!customPath) {
jsonPath = audioFolderPath + name + ".info.json";
} else {
jsonPath = customPath + name + ".info.json";
alternateJsonPath = customPath + name + ".mp3.info.json";
}
var obj = null;
if (fs.existsSync(jsonPath)) {
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
if (!is_windows && openReadPerms) fs.chmodSync(jsonPath, 0o755);
}
else if (fs.existsSync(alternateJsonPath)) {
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
if (!is_windows && openReadPerms) fs.chmodSync(alternateJsonPath, 0o755);
}
else
obj = 0;
return obj;
}
function getJSONMp4(name, customPath = null, openReadPerms = false)
{
var obj = null; // output
let jsonPath = null;
var alternateJsonPath = videoFolderPath + name + ".mp4.info.json";
if (!customPath) {
jsonPath = videoFolderPath + name + ".info.json";
} else {
jsonPath = customPath + name + ".info.json";
alternateJsonPath = customPath + name + ".mp4.info.json";
}
if (fs.existsSync(jsonPath))
{
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
if (openReadPerms) fs.chmodSync(jsonPath, 0o644);
} else if (fs.existsSync(alternateJsonPath)) {
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
if (openReadPerms) fs.chmodSync(alternateJsonPath, 0o644);
}
else obj = 0;
return obj;
}
function getAmountDownloadedMp3(name) function getAmountDownloadedMp3(name)
{ {
var partPath = audioFolderPath+name+".mp3.part"; var partPath = audioFolderPath+name+".mp3.part";
@@ -928,17 +918,22 @@ 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 => { return new Promise(resolve => {
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams let filePath = customPath ? customPath : audioFolderPath;
var jsonPath = path.join(audioFolderPath,name+'.mp3.info.json');
var altJSONPath = path.join(audioFolderPath,name+'.info.json'); var jsonPath = path.join(filePath,name+'.mp3.info.json');
var audioFilePath = path.join(audioFolderPath,name+'.mp3'); 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); jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath); altJSONPath = path.join(__dirname, altJSONPath);
audioFilePath = path.join(__dirname, audioFilePath); audioFilePath = path.join(__dirname, audioFilePath);
let jsonExists = fs.existsSync(jsonPath); let jsonExists = fs.existsSync(jsonPath);
let thumbnailExists = fs.existsSync(thumbnailPath);
if (!jsonExists) { if (!jsonExists) {
if (fs.existsSync(altJSONPath)) { if (fs.existsSync(altJSONPath)) {
@@ -947,6 +942,13 @@ async function deleteAudioFile(name, blacklistMode = false) {
} }
} }
if (!thumbnailExists) {
if (fs.existsSync(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
let audioFileExists = fs.existsSync(audioFilePath); let audioFileExists = fs.existsSync(audioFilePath);
if (config_api.descriptors[name]) { if (config_api.descriptors[name]) {
@@ -965,7 +967,7 @@ async function deleteAudioFile(name, blacklistMode = false) {
// get ID from JSON // get ID from JSON
var jsonobj = getJSONMp3(name); var jsonobj = utils.getJSONMp3(name, filePath);
let id = null; let id = null;
if (jsonobj) id = jsonobj.id; if (jsonobj) id = jsonobj.id;
@@ -980,6 +982,7 @@ async function deleteAudioFile(name, blacklistMode = false) {
} }
if (jsonExists) fs.unlinkSync(jsonPath); if (jsonExists) fs.unlinkSync(jsonPath);
if (thumbnailExists) fs.unlinkSync(thumbnailPath);
if (audioFileExists) { if (audioFileExists) {
fs.unlink(audioFilePath, function(err) { fs.unlink(audioFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) { if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) {
@@ -1000,12 +1003,32 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
return new Promise(resolve => { return new Promise(resolve => {
let filePath = customPath ? customPath : videoFolderPath; let filePath = customPath ? customPath : videoFolderPath;
var jsonPath = path.join(filePath,name+'.info.json'); 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 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); jsonPath = path.join(__dirname, jsonPath);
videoFilePath = path.join(__dirname, videoFilePath); videoFilePath = path.join(__dirname, videoFilePath);
jsonExists = fs.existsSync(jsonPath); let jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath); let videoFileExists = fs.existsSync(videoFilePath);
let thumbnailExists = fs.existsSync(thumbnailPath);
if (!jsonExists) {
if (fs.existsSync(altJSONPath)) {
jsonExists = true;
jsonPath = altJSONPath;
}
}
if (!thumbnailExists) {
if (fs.existsSync(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
if (config_api.descriptors[name]) { if (config_api.descriptors[name]) {
try { try {
@@ -1023,7 +1046,7 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
// get ID from JSON // get ID from JSON
var jsonobj = getJSONMp4(name); var jsonobj = utils.getJSONMp4(name, filePath);
let id = null; let id = null;
if (jsonobj) id = jsonobj.id; if (jsonobj) id = jsonobj.id;
@@ -1038,6 +1061,7 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
} }
if (jsonExists) fs.unlinkSync(jsonPath); if (jsonExists) fs.unlinkSync(jsonPath);
if (thumbnailExists) fs.unlinkSync(thumbnailPath);
if (videoFileExists) { if (videoFileExists) {
fs.unlink(videoFilePath, function(err) { fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) { if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
@@ -1077,7 +1101,7 @@ function recFindByExt(base,ext,files,result)
) )
return result return result
} }
/*
function registerFileDB(file_path, type, multiUserMode = null) { function registerFileDB(file_path, type, multiUserMode = null) {
const file_id = file_path.substring(0, file_path.length-4); const file_id = file_path.substring(0, file_path.length-4);
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path); const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path);
@@ -1094,7 +1118,7 @@ function registerFileDB(file_path, type, multiUserMode = null) {
if (multiUserMode) { if (multiUserMode) {
auth_api.registerUserFile(multiUserMode.user, file_object, type); auth_api.registerUserFile(multiUserMode.user, file_object, type);
} else { } else if (type === 'audio' || type === 'video') {
// remove existing video if overwriting // remove existing video if overwriting
db.get(`files.${type}`) db.get(`files.${type}`)
.remove({ .remove({
@@ -1104,13 +1128,15 @@ function registerFileDB(file_path, type, multiUserMode = null) {
db.get(`files.${type}`) db.get(`files.${type}`)
.push(file_object) .push(file_object)
.write(); .write();
} else if (type == 'subscription') {
} }
return file_object['uid']; return file_object['uid'];
} }
function generateFileObject(id, type, customPath = null) { function generateFileObject(id, type, customPath = null) {
var jsonobj = (type === 'audio') ? getJSONMp3(id, customPath, true) : getJSONMp4(id, customPath, true); var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
if (!jsonobj) { if (!jsonobj) {
return null; return null;
} }
@@ -1130,10 +1156,10 @@ function generateFileObject(id, type, customPath = null) {
var thumbnail = jsonobj.thumbnail; var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration; var duration = jsonobj.duration;
var isaudio = type === 'audio'; var isaudio = type === 'audio';
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date); var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
return file_obj; return file_obj;
} }
*/
// replaces .webm with appropriate extension // replaces .webm with appropriate extension
function getTrueFileName(unfixed_path, type) { function getTrueFileName(unfixed_path, type) {
let fixed_path = unfixed_path; let fixed_path = unfixed_path;
@@ -1209,6 +1235,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
options.customFileFolderPath = fileFolderPath; options.customFileFolderPath = fileFolderPath;
} }
options.downloading_method = 'exec';
const downloadConfig = await generateArgs(url, type, options); const downloadConfig = await generateArgs(url, type, options);
// adds download to download helper // adds download to download helper
@@ -1246,6 +1273,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
} else if (output) { } else if (output) {
if (output.length === 0 || output[0].length === 0) { if (output.length === 0 || output[0].length === 0) {
download['error'] = 'No output. Check if video already exists in your archive.'; download['error'] = 'No output. Check if video already exists in your archive.';
logger.warn(`No output received for video download, check if it exists in your archive.`)
updateDownloads(); updateDownloads();
resolve(false); resolve(false);
@@ -1289,17 +1317,17 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
} }
// registers file in DB // registers file in DB
file_uid = registerFileDB(full_file_path.substring(fileFolderPath.length, full_file_path.length), type, multiUserMode); file_uid = db_api.registerFileDB(full_file_path.substring(fileFolderPath.length, full_file_path.length), type, multiUserMode);
if (file_name) file_names.push(file_name); if (file_name) file_names.push(file_name);
} }
let is_playlist = file_names.length > 1; let is_playlist = file_names.length > 1;
if (options.merged_string) { if (options.merged_string !== null && options.merged_string !== undefined) {
let current_merged_archive = fs.readFileSync(fileFolderPath + 'merged.txt', 'utf8'); let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8');
let diff = current_merged_archive.replace(options.merged_string, ''); let diff = current_merged_archive.replace(options.merged_string, '');
const archive_path = path.join(archivePath, `archive_${type}.txt`); const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
fs.appendFileSync(archive_path, diff); fs.appendFileSync(archive_path, diff);
} }
@@ -1343,6 +1371,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
options.customFileFolderPath = fileFolderPath; options.customFileFolderPath = fileFolderPath;
} }
options.downloading_method = 'normal';
const downloadConfig = await generateArgs(url, type, options); const downloadConfig = await generateArgs(url, type, options);
// adds download to download helper // adds download to download helper
@@ -1428,12 +1457,12 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
// registers file in DB // registers file in DB
const base_file_name = video_info._filename.substring(fileFolderPath.length, video_info._filename.length); const base_file_name = video_info._filename.substring(fileFolderPath.length, video_info._filename.length);
file_uid = registerFileDB(base_file_name, type, multiUserMode); file_uid = db_api.registerFileDB(base_file_name, type, multiUserMode);
if (options.merged_string) { if (options.merged_string !== null && options.merged_string !== undefined) {
let current_merged_archive = fs.readFileSync(fileFolderPath + 'merged.txt', 'utf8'); let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8');
let diff = current_merged_archive.replace(options.merged_string, ''); let diff = current_merged_archive.replace(options.merged_string, '');
const archive_path = req.isAuthenticated() ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`); const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
fs.appendFileSync(archive_path, diff); fs.appendFileSync(archive_path, diff);
} }
@@ -1483,24 +1512,24 @@ async function generateArgs(url, type, options) {
var youtubePassword = options.youtubePassword; var youtubePassword = options.youtubePassword;
let downloadConfig = null; 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', 'best[ext=mp4]'];
const is_youtube = url.includes('youtu'); const is_youtube = url.includes('youtu');
if (!is_audio && !is_youtube) { if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format // tiktok videos fail when using the default format
qualityPath = null; qualityPath = null;
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) { } else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
qualityPath = '-f bestvideo+bestaudio' qualityPath = ['-f', 'bestvideo+bestaudio']
} }
if (customArgs) { if (customArgs) {
downloadConfig = customArgs.split(',,'); downloadConfig = customArgs.split(',,');
} else { } else {
if (customQualityConfiguration) { if (customQualityConfiguration) {
qualityPath = `-f ${customQualityConfiguration}`; qualityPath = ['-f', customQualityConfiguration];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) { } else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = `-f '(mp4)[height=${selectedHeight}]'`; qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (maxBitrate && is_audio) { } else if (maxBitrate && is_audio) {
qualityPath = `--audio-quality ${maxBitrate}` qualityPath = ['--audio-quality', maxBitrate]
} }
if (customOutput) { if (customOutput) {
@@ -1509,7 +1538,7 @@ async function generateArgs(url, type, options) {
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json']; 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) { if (is_audio && !options.skip_audio_args) {
downloadConfig.push('-x'); downloadConfig.push('-x');
@@ -1534,7 +1563,11 @@ async function generateArgs(url, type, options) {
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) { if (useYoutubeDLArchive) {
const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`); const archive_folder = options.user ? path.join(fileFolderPath, 'archives') : archivePath;
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
fs.ensureDirSync(archive_folder);
// create archive file if it doesn't exist // create archive file if it doesn't exist
if (!fs.existsSync(archive_path)) { if (!fs.existsSync(archive_path)) {
fs.closeSync(fs.openSync(archive_path, 'w')); fs.closeSync(fs.openSync(archive_path, 'w'));
@@ -1546,7 +1579,7 @@ async function generateArgs(url, type, options) {
fs.closeSync(fs.openSync(blacklist_path, 'w')); fs.closeSync(fs.openSync(blacklist_path, 'w'));
} }
let merged_path = fileFolderPath + 'merged.txt'; let merged_path = path.join(fileFolderPath, `merged_${type}.txt`);
fs.ensureFileSync(merged_path); fs.ensureFileSync(merged_path);
// merges blacklist and regular archive // merges blacklist and regular archive
let inputPathList = [archive_path, blacklist_path]; let inputPathList = [archive_path, blacklist_path];
@@ -1568,6 +1601,7 @@ async function generateArgs(url, type, options) {
} }
} }
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
// downloadConfig.map((arg) => `"${arg}"`); // downloadConfig.map((arg) => `"${arg}"`);
resolve(downloadConfig); resolve(downloadConfig);
}); });
@@ -1737,7 +1771,10 @@ async function autoUpdateYoutubeDL() {
resolve(true); resolve(true);
}); });
} }
})
.catch(err => {
logger.error('Failed to check youtube-dl version for an update.')
logger.error(err)
}); });
}); });
} }
@@ -1855,7 +1892,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; let new_config_file = req.body.new_config_file;
if (new_config_file && new_config_file['YoutubeDLMaterial']) { if (new_config_file && new_config_file['YoutubeDLMaterial']) {
let success = config_api.setConfigFile(new_config_file); let success = config_api.setConfigFile(new_config_file);
@@ -1887,8 +1924,12 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
user: req.isAuthenticated() ? req.user.uid : null user: req.isAuthenticated() ? req.user.uid : null
} }
const safeDownloadOverride = config_api.getConfigItem('ytdl_safe_download_override') || config_api.globalArgsRequiresSafeDownload();
if (safeDownloadOverride) logger.verbose('Download is running with the safe download override.');
const is_playlist = url.includes('playlist'); const is_playlist = url.includes('playlist');
if (is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID); result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID);
else else
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID); result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
@@ -1897,8 +1938,6 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
} else { } else {
res.sendStatus(500); res.sendStatus(500);
} }
res.end("yes");
}); });
app.post('/api/tomp4', optionalJwt, async function(req, res) { app.post('/api/tomp4', optionalJwt, async function(req, res) {
@@ -1914,9 +1953,12 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
user: req.isAuthenticated() ? req.user.uid : null user: req.isAuthenticated() ? req.user.uid : null
} }
const safeDownloadOverride = config_api.getConfigItem('ytdl_safe_download_override') || config_api.globalArgsRequiresSafeDownload();
if (safeDownloadOverride) logger.verbose('Download is running with the safe download override.');
const is_playlist = url.includes('playlist'); const is_playlist = url.includes('playlist');
let result_obj = null; let result_obj = null;
if (is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu')) if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID); result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID);
else else
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID); result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
@@ -1925,50 +1967,11 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
} else { } else {
res.sendStatus(500); res.sendStatus(500);
} }
res.end("yes");
}); });
// gets the status of the mp3 file that's being downloaded app.post('/api/killAllDownloads', optionalJwt, async function(req, res) {
app.post('/api/fileStatusMp3', function(req, res) { const result_obj = await killAllDownloads();
var name = decodeURIComponent(req.body.name + ""); res.send(result_obj);
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");
}); });
// gets all download mp3s // gets all download mp3s
@@ -2138,14 +2141,17 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
let url = req.body.url; let url = req.body.url;
let timerange = req.body.timerange; let timerange = req.body.timerange;
let streamingOnly = req.body.streamingOnly; let streamingOnly = req.body.streamingOnly;
let audioOnly = req.body.audioOnly;
let customArgs = req.body.customArgs;
let customOutput = req.body.customFileOutput;
let user_uid = req.isAuthenticated() ? req.user.uid : null; let user_uid = req.isAuthenticated() ? req.user.uid : null;
const new_sub = { const new_sub = {
name: name, name: name,
url: url, url: url,
id: uuid(), id: uuid(),
streamingOnly: streamingOnly, streamingOnly: streamingOnly,
user_uid: user_uid user_uid: user_uid,
type: audioOnly ? 'audio' : 'video'
}; };
// adds timerange if it exists, otherwise all videos will be downloaded // adds timerange if it exists, otherwise all videos will be downloaded
@@ -2153,6 +2159,14 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
new_sub.timerange = timerange; new_sub.timerange = timerange;
} }
if (customArgs && customArgs !== '') {
new_sub.custom_args = customArgs;
}
if (customOutput && customOutput !== '') {
new_sub.custom_output = customOutput;
}
const result_obj = await subscriptions_api.subscribe(new_sub, user_uid); const result_obj = await subscriptions_api.subscribe(new_sub, user_uid);
if (result_obj.success) { if (result_obj.success) {
@@ -2188,10 +2202,11 @@ app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => { app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
let deleteForever = req.body.deleteForever; let deleteForever = req.body.deleteForever;
let file = req.body.file; let file = req.body.file;
let file_uid = req.body.file_uid;
let sub = req.body.sub; let sub = req.body.sub;
let user_uid = req.isAuthenticated() ? req.user.uid : null; let user_uid = req.isAuthenticated() ? req.user.uid : null;
let success = await subscriptions_api.deleteSubscriptionFile(sub, file, deleteForever, user_uid); let success = await subscriptions_api.deleteSubscriptionFile(sub, file, deleteForever, file_uid, user_uid);
if (success) { if (success) {
res.send({ res.send({
@@ -2218,45 +2233,49 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
// get sub videos // get sub videos
if (subscription.name && !subscription.streamingOnly) { if (subscription.name && !subscription.streamingOnly) {
let base_path = null; var parsed_files = subscription.videos;
if (user_uid) if (!parsed_files) {
base_path = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); parsed_files = [];
else let base_path = null;
base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); if (user_uid)
base_path = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
base_path = config_api.getConfigItem('ytdl_subscriptions_base_path');
let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/'); let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/');
let files; let files;
try { try {
files = recFindByExt(appended_base_path, 'mp4'); files = recFindByExt(appended_base_path, 'mp4');
} catch(e) { } catch(e) {
files = null; files = null;
logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path); logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
res.sendStatus(500); res.sendStatus(500);
return; return;
}
for (let i = 0; i < files.length; i++) {
let file = files[i];
var file_path = file.substring(appended_base_path.length, file.length);
var stats = fs.statSync(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = utils.getJSONMp4(id, appended_base_path);
if (!jsonobj) continue;
var title = jsonobj.title;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}`;
var size = stats.size;
var isaudio = false;
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
parsed_files.push(file_obj);
}
} }
var parsed_files = [];
for (let i = 0; i < files.length; i++) {
let file = files[i];
var file_path = file.substring(appended_base_path.length, file.length);
var stats = fs.statSync(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = getJSONMp4(id, appended_base_path);
if (!jsonobj) continue;
var title = jsonobj.title;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}`;
var size = stats.size;
var isaudio = false;
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
parsed_files.push(file_obj);
}
res.send({ res.send({
subscription: subscription, subscription: subscription,
@@ -2268,7 +2287,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
if (subscription.videos) { if (subscription.videos) {
for (let i = 0; i < subscription.videos.length; i++) { for (let i = 0; i < subscription.videos.length; i++) {
const video = subscription.videos[i]; const video = subscription.videos[i];
parsed_files.push(new File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date)); parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date));
} }
} }
res.send({ res.send({
@@ -2291,6 +2310,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) => { app.post('/api/getAllSubscriptions', optionalJwt, async (req, res) => {
let user_uid = req.isAuthenticated() ? req.user.uid : null; let user_uid = req.isAuthenticated() ? req.user.uid : null;
@@ -2362,7 +2391,7 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
}); });
}); });
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => { app.post('/api/updatePlaylistFiles', optionalJwt, async (req, res) => {
let playlistID = req.body.playlistID; let playlistID = req.body.playlistID;
let fileNames = req.body.fileNames; let fileNames = req.body.fileNames;
let type = req.body.type; let type = req.body.type;
@@ -2370,7 +2399,7 @@ app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
let success = false; let success = false;
try { try {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
auth_api.updatePlaylist(req.user.uid, playlistID, fileNames, type); auth_api.updatePlaylistFiles(req.user.uid, playlistID, fileNames, type);
} else { } else {
db.get(`playlists.${type}`) db.get(`playlists.${type}`)
.find({id: playlistID}) .find({id: playlistID})
@@ -2388,6 +2417,14 @@ app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
}) })
}); });
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
let playlist = req.body.playlist;
let success = db_api.updatePlaylist(playlist, req.user && req.user.uid);
res.send({
success: success
});
});
app.post('/api/deletePlaylist', optionalJwt, async (req, res) => { app.post('/api/deletePlaylist', optionalJwt, async (req, res) => {
let playlistID = req.body.playlistID; let playlistID = req.body.playlistID;
let type = req.body.type; let type = req.body.type;
@@ -2431,11 +2468,10 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
var wasDeleted = false; var wasDeleted = false;
if (fs.existsSync(fullpath)) if (fs.existsSync(fullpath))
{ {
deleteAudioFile(name, blacklistMode); deleteAudioFile(name, null, blacklistMode);
db.get('files.audio').remove({uid: uid}).write(); db.get('files.audio').remove({uid: uid}).write();
wasDeleted = true; wasDeleted = true;
res.send(wasDeleted); res.send(wasDeleted);
res.end("yes");
} else if (audio_obj) { } else if (audio_obj) {
db.get('files.audio').remove({uid: uid}).write(); db.get('files.audio').remove({uid: uid}).write();
wasDeleted = true; wasDeleted = true;
@@ -2467,7 +2503,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
db.get('files.video').remove({uid: uid}).write(); db.get('files.video').remove({uid: uid}).write();
// wasDeleted = true; // wasDeleted = true;
res.send(wasDeleted); res.send(wasDeleted);
res.end("yes");
} else if (video_obj) { } else if (video_obj) {
db.get('files.video').remove({uid: uid}).write(); db.get('files.video').remove({uid: uid}).write();
wasDeleted = true; wasDeleted = true;
@@ -2475,7 +2510,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
} else { } else {
wasDeleted = false; wasDeleted = false;
res.send(wasDeleted); res.send(wasDeleted);
res.end("yes");
} }
}); });
@@ -2508,7 +2542,8 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
basePath = path.join(usersFileFolder, req.user.uid, 'subscriptions'); basePath = path.join(usersFileFolder, req.user.uid, 'subscriptions');
else else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
file = path.join(__dirname, basePath, (subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + '.mp4')
file = path.join(__dirname, basePath, (subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
} }
} else { } else {
for (let i = 0; i < fileNames.length; i++) { for (let i = 0; i < fileNames.length; i++) {
@@ -2544,7 +2579,7 @@ app.post('/api/downloadArchive', async (req, res) => {
let sub = req.body.sub; let sub = req.body.sub;
let archive_dir = sub.archive; let archive_dir = sub.archive;
let full_archive_path = path.join(__dirname, archive_dir, 'archive.txt'); let full_archive_path = path.join(archive_dir, 'archive.txt');
if (fs.existsSync(full_archive_path)) { if (fs.existsSync(full_archive_path)) {
res.sendFile(full_archive_path); res.sendFile(full_archive_path);
@@ -2597,49 +2632,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 // API Key API calls
app.post('/api/generateNewAPIKey', function (req, res) { app.post('/api/generateNewAPIKey', function (req, res) {
@@ -2709,9 +2701,21 @@ app.get('/api/audio/:id', optionalJwt, function(req , res){
var head; var head;
let id = decodeURIComponent(req.params.id); let id = decodeURIComponent(req.params.id);
let file_path = "audio/" + id + '.mp3'; let file_path = "audio/" + id + '.mp3';
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
let optionalParams = url_api.parse(req.url,true).query;
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); if (optionalParams['subName']) {
file_path = path.join(usersFileFolder, req.user.uid, 'audio', id + '.mp3'); const isPlaylist = optionalParams['subPlaylist'];
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + '.mp3')
} else {
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
file_path = path.join(usersFileFolder, req.user.uid, 'audio', id + '.mp3');
}
} else if (optionalParams['subName']) {
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const isPlaylist = optionalParams['subPlaylist'];
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
file_path = basePath + optionalParams['subName'] + '/' + id + '.mp3';
} }
file_path = file_path.replace(/\"/g, '\''); file_path = file_path.replace(/\"/g, '\'');
const stat = fs.statSync(file_path) const stat = fs.statSync(file_path)
@@ -2813,6 +2817,42 @@ app.get('/api/audio/:id', optionalJwt, function(req , res){
res.send({success: success, downloads: downloads}); 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) => { app.post('/api/getVideoInfos', async (req, res) => {
let fileNames = req.body.fileNames; let fileNames = req.body.fileNames;
let urlMode = !!req.body.urlMode; let urlMode = !!req.body.urlMode;
@@ -2851,7 +2891,7 @@ app.post('/api/auth/jwtAuth'
, auth_api.returnAuthResponse , auth_api.returnAuthResponse
); );
app.post('/api/auth/changePassword', optionalJwt, async (req, res) => { app.post('/api/auth/changePassword', optionalJwt, async (req, res) => {
let user_uid = req.user.uid; let user_uid = req.body.user_uid;
let password = req.body.new_password; let password = req.body.new_password;
let success = await auth_api.changeUserPassword(user_uid, password); let success = await auth_api.changeUserPassword(user_uid, password);
res.send({success: success}); res.send({success: success});

View File

@@ -13,7 +13,8 @@
"path-audio": "audio/", "path-audio": "audio/",
"path-video": "video/", "path-video": "video/",
"use_youtubedl_archive": false, "use_youtubedl_archive": false,
"custom_args": "" "custom_args": "",
"safe_download_override": false
}, },
"Extra": { "Extra": {
"title_top": "YoutubeDL-Material", "title_top": "YoutubeDL-Material",
@@ -21,7 +22,6 @@
"allow_quality_select": true, "allow_quality_select": true,
"download_only_mode": false, "download_only_mode": false,
"allow_multi_download_mode": true, "allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true "enable_downloads_manager": true
}, },
"API": { "API": {

View File

@@ -13,7 +13,8 @@
"path-audio": "audio/", "path-audio": "audio/",
"path-video": "video/", "path-video": "video/",
"use_youtubedl_archive": false, "use_youtubedl_archive": false,
"custom_args": "" "custom_args": "",
"safe_download_override": false
}, },
"Extra": { "Extra": {
"title_top": "YoutubeDL-Material", "title_top": "YoutubeDL-Material",
@@ -21,7 +22,6 @@
"allow_quality_select": true, "allow_quality_select": true,
"download_only_mode": false, "download_only_mode": false,
"allow_multi_download_mode": true, "allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true "enable_downloads_manager": true
}, },
"API": { "API": {

Binary file not shown.

View File

@@ -5,7 +5,7 @@ var subscriptions_api = require('../subscriptions')
const fs = require('fs-extra'); const fs = require('fs-extra');
var jwt = require('jsonwebtoken'); var jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
var bcrypt = require('bcrypt'); var bcrypt = require('bcryptjs');
var LocalStrategy = require('passport-local').Strategy; var LocalStrategy = require('passport-local').Strategy;
@@ -331,7 +331,7 @@ exports.addPlaylist = function(user_uid, new_playlist, type) {
return true; return true;
} }
exports.updatePlaylist = function(user_uid, playlistID, new_filenames, type) { exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames, type) {
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames}); users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
return true; return true;
} }
@@ -430,8 +430,8 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
fs.appendFileSync(blacklistPath, line); fs.appendFileSync(blacklistPath, line);
} }
} else { } else {
logger.info('Could not find archive file for audio files. Creating...'); logger.info(`Could not find archive file for ${type} files. Creating...`);
fs.closeSync(fs.openSync(archive_path, 'w')); fs.ensureFileSync(archive_path);
} }
} }
} }

View File

@@ -155,6 +155,13 @@ function setConfigItems(items) {
return success; return success;
} }
function globalArgsRequiresSafeDownload() {
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
return failedArgs && failedArgs.length > 0;
}
module.exports = { module.exports = {
getConfigItem: getConfigItem, getConfigItem: getConfigItem,
setConfigItem: setConfigItem, setConfigItem: setConfigItem,
@@ -164,7 +171,8 @@ module.exports = {
configExistsCheck: configExistsCheck, configExistsCheck: configExistsCheck,
CONFIG_ITEMS: CONFIG_ITEMS, CONFIG_ITEMS: CONFIG_ITEMS,
initialize: initialize, initialize: initialize,
descriptors: {} descriptors: {},
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
} }
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
@@ -182,7 +190,8 @@ DEFAULT_CONFIG = {
"path-audio": "audio/", "path-audio": "audio/",
"path-video": "video/", "path-video": "video/",
"use_youtubedl_archive": false, "use_youtubedl_archive": false,
"custom_args": "" "custom_args": "",
"safe_download_override": false
}, },
"Extra": { "Extra": {
"title_top": "YoutubeDL-Material", "title_top": "YoutubeDL-Material",
@@ -190,7 +199,6 @@ DEFAULT_CONFIG = {
"allow_quality_select": true, "allow_quality_select": true,
"download_only_mode": false, "download_only_mode": false,
"allow_multi_download_mode": true, "allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true "enable_downloads_manager": true
}, },
"API": { "API": {

View File

@@ -40,6 +40,10 @@ let CONFIG_ITEMS = {
'key': 'ytdl_custom_args', 'key': 'ytdl_custom_args',
'path': 'YoutubeDLMaterial.Downloader.custom_args' 'path': 'YoutubeDLMaterial.Downloader.custom_args'
}, },
'ytdl_safe_download_override': {
'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
},
// Extra // Extra
'ytdl_title_top': { 'ytdl_title_top': {
@@ -62,10 +66,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_multi_download_mode', 'key': 'ytdl_allow_multi_download_mode',
'path': 'YoutubeDLMaterial.Extra.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': { 'ytdl_enable_downloads_manager': {
'key': 'ytdl_enable_downloads_manager', 'key': 'ytdl_enable_downloads_manager',
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager' 'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
@@ -170,5 +170,5 @@ AVAILABLE_PERMISSIONS = [
module.exports = { module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS, CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS, AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.0' CURRENT_VERSION: 'v4.1'
} }

119
backend/db.js Normal file
View File

@@ -0,0 +1,119 @@
var fs = require('fs-extra')
var path = require('path')
var utils = require('./utils')
const { uuid } = require('uuidv4');
const config_api = require('./config');
var logger = null;
var db = null;
var users_db = null;
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db }
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger) {
setDB(input_db, input_users_db);
setLogger(input_logger);
}
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
const file_id = file_path.substring(0, file_path.length-4);
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
return false;
}
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
if (!sub) {
if (multiUserMode) {
const user_uid = multiUserMode.user;
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.remove({
path: file_object['path']
}).write();
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.push(file_object)
.write();
} else {
// remove existing video if overwriting
db.get(`files.${type}`)
.remove({
path: file_object['path']
}).write();
db.get(`files.${type}`)
.push(file_object)
.write();
}
} else {
sub_db = null;
if (multiUserMode) {
const user_uid = multiUserMode.user;
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
sub_db = db.get('subscriptions').find({id: sub.id});
}
sub_db.get('videos').push(file_object).write();
}
return file_object['uid'];
}
function generateFileObject(id, type, customPath = null, sub = null) {
if (!customPath && sub) {
customPath = getAppendedBasePathSub(sub, config_api.getConfigItem('ytdl_subscriptions_base_path'));
}
var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
if (!jsonobj) {
return null;
}
const ext = (type === 'audio') ? '.mp3' : '.mp4'
const file_path = utils.getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
// console.
var stats = fs.statSync(path.join(__dirname, file_path));
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
var size = stats.size;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = type === 'audio';
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
return file_obj;
}
function updatePlaylist(playlist, user_uid) {
let playlistID = playlist.id;
let type = playlist.type;
let db_loc = null;
if (user_uid) {
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID});
} else {
db_loc = db.get(`playlists.${type}`).find({id: playlistID});
}
db_loc.assign(playlist).write();
return true;
}
function getAppendedBasePathSub(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
module.exports = {
initialize: initialize,
registerFileDB: registerFileDB,
updatePlaylist: updatePlaylist
}

17
backend/entrypoint.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
CMD="node app.js"
# if the first arg starts with "-" pass it to program
if [ "${1#-}" != "$1" ]; then
set -- "$CMD" "$@"
fi
# chown current working directory to current user
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || 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
exec "$@"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
#!/bin/bash
# downloads a local copy of qemu on docker-hub build machines
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 .

4
backend/hooks/pre_build Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
# Register qemu-*-static for all supported processors except the
# current one, but also remove all registered binfmt_misc before
docker run --rm --privileged multiarch/qemu-user-static:register --reset

View File

@@ -50,6 +50,11 @@
"color-convert": "^1.9.0" "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": { "anymatch": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@@ -64,11 +69,6 @@
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
}, },
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"archiver": { "archiver": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz",
@@ -126,31 +126,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": { "array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -204,15 +179,6 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" "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": { "bcrypt-pbkdf": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -221,6 +187,11 @@
"tweetnacl": "^0.14.3" "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": { "big-integer": {
"version": "1.6.48", "version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
@@ -434,11 +405,6 @@
"readdirp": "~3.3.0" "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": { "ci-info": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
@@ -449,11 +415,6 @@
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" "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": { "color": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
@@ -614,10 +575,10 @@
"xdg-basedir": "^3.0.0" "xdg-basedir": "^3.0.0"
} }
}, },
"console-control-strings": { "connected-domain": {
"version": "1.1.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/connected-domain/-/connected-domain-1.0.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" "integrity": "sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM="
}, },
"content-disposition": { "content-disposition": {
"version": "0.5.3", "version": "0.5.3",
@@ -718,11 +679,6 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
}, },
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"depd": { "depd": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -733,11 +689,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" "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": { "diagnostics": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
@@ -1051,14 +1002,6 @@
"universalify": "^1.0.0" "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": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1081,54 +1024,6 @@
"rimraf": "2" "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": { "get-stream": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
@@ -1228,11 +1123,6 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" "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": { "hashish": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz", "resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz",
@@ -1301,14 +1191,6 @@
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" "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": { "import-lazy": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
@@ -1596,9 +1478,9 @@
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
}, },
"lodash.defaults": { "lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
@@ -1782,30 +1664,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "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": { "mkdirp": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
@@ -1859,46 +1717,26 @@
} }
} }
}, },
"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"
}
},
"nanoid": { "nanoid": {
"version": "2.1.11", "version": "2.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" "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=="
}
}
},
"negotiator": { "negotiator": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "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": { "node-fetch": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
@@ -1912,34 +1750,6 @@
"iconv-lite": "^0.4.15" "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": { "nodemon": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz",
@@ -1985,29 +1795,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" "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": { "npm-run-path": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -2016,22 +1803,6 @@
"path-key": "^3.0.0" "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": { "oauth-sign": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -2076,25 +1847,6 @@
"mimic-fn": "^2.1.0" "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": { "p-finally": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
@@ -2243,6 +1995,14 @@
"ipaddr.js": "1.9.1" "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": { "pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -2311,6 +2071,14 @@
"strip-json-comments": "~2.0.1" "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": { "readable-stream": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -2403,11 +2171,6 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "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": { "semver": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -2459,11 +2222,6 @@
"send": "0.17.1" "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": { "setimmediate": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -2603,25 +2361,12 @@
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
} }
}, },
"tar": { "table-parser": {
"version": "4.4.13", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", "resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "integrity": "sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=",
"requires": { "requires": {
"chownr": "^1.1.1", "connected-domain": "^1.0.0"
"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=="
}
} }
}, },
"tar-stream": { "tar-stream": {
@@ -2724,6 +2469,22 @@
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" "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": { "timed-out": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
@@ -2939,14 +2700,6 @@
"isexe": "^2.0.0" "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": { "widest-line": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",

View File

@@ -30,7 +30,7 @@
"dependencies": { "dependencies": {
"archiver": "^3.1.1", "archiver": "^3.1.1",
"async": "^3.1.0", "async": "^3.1.0",
"bcrypt": "^4.0.1", "bcryptjs": "^2.4.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.2.3", "config": "^3.2.3",
"exe": "^1.0.2", "exe": "^1.0.2",
@@ -50,6 +50,8 @@
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"progress": "^2.0.3", "progress": "^2.0.3",
"ps-node": "^0.1.6",
"read-last-lines": "^1.7.2",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"unzipper": "^0.10.10", "unzipper": "^0.10.10",
"uuidv4": "^6.0.6", "uuidv4": "^6.0.6",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,198 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Youtube-dl Argumente ändern",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulierte neue Argumente",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Argument hinzufügen",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Nach Kategorie filtern",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Argument-Wert verwenden",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Argument-Wert",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Argument hinzufügen",
"d7b35c384aecd25a516200d6921836374613dfe7": "Abbrechen",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Ändern",
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "YouTube Downloader",
"6d2ec8898344c8955542b0542c942038ef28bb80": "Bitte geben Sie eine gültige URL ein.",
"a38ae1082fec79ba1f379978337385a539a28e73": "Qualität",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL verwenden",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Ansehen",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Nur Audio",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-Download Modus",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Download",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Abbrechen",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Erweitert",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulierter Befehl:",
"4e4c721129466be9c3862294dc40241b64045998": "Benutzerdefinierte Argumente verwenden",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Benutzerdefinierte Argumente",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Die URL muss nicht angegeben werden, sondern nur der Teil danach. Argumente werden mit zwei Kommata getrennt: ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Benutzerdefinierte Ausgabe verwenden",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Benutzerdefinierte Ausgabe",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentation",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Der Pfad ist relativ zum Konfigurations-Download-Pfad. Dateiendung auslassen.",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Authentifizierung verwenden",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Benutzername",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
"0f59c46ca29e9725898093c9ea6b586730d0624e": "Keine Playlisten verfügbar. Erstellen Sie eine aus heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Name:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Kanal:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Dateigröße:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Pfad:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Hochgeladen am:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Schließen",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Anzahl:",
"321e4419a943044e674beb55b8039f42a9761ca5": "Info",
"826b25211922a1b46436589233cb6f1a163d89b7": "Löschen",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Löschen und zur Blacklist hinzufügen",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Einstellungen",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL, über die auf diese Applikation zugegriffen wird, ohne Port.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Der gewünschte Port. Standard ist 17442.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Multi-User Modus",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Benutzer Basispfad",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Basispfad für Benutzer und deren heruntergeladene Videos.",
"cbe16a57be414e84b6a68309d08fad894df797d6": "Verschlüsselung verwenden",
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Dateipfad zum Zertifikat",
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Dateipfad zum Zertifikatsschlüssel",
"4e3120311801c4acd18de7146add2ee4a4417773": "Abonnements erlauben",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnements Basispfad",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Basispfad für Videos von abonnierten Kanälen und Wiedergabelisten. Dieser ist relativ zum Stammordner von YTDL-Material.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Prüfintervall",
"0f56a7449b77630c114615395bbda4cab398efd8": "Einheit ist Sekunden, nur Zahlen sind erlaubt.",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Youtube-DL Archiv verwenden",
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Mit der Archivfunktion",
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "werden Informationen über Videos, welche durch ein Abonnement heruntergeladen wurden, in einem Textdokument festgehalten. Diese befinden sich in dem Archiv Unterverzeichnis vom Abonnementsordner.",
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Dadurch können Videos permanent gelöscht werden, ohne das Abonnement beenden zu müssen. Außerdem kann dadurch aufgezeichnet werden, welche Videos heruntergeladen wurden. Z. B. im Falle eines Datenverlusts.",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Design",
"ff7cee38a2259526c519f878e71b964f41db4348": "Standard",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Dunkel",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Designänderung erlauben",
"fe46ccaae902ce974e2441abe752399288298619": "Sprache",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Allgemein",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Audio Basispfad",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Dateipfad für Audio-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Qualitätsauswahl erlauben",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Nur Download Modus",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Multi-Download Modus erlauben",
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Einstellungen durch PIN schützen",
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Neuen PIN festlegen",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Öffentliche API aktivieren",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Öffentlicher API-Schlüssel",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Dokumentation ansehen",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generieren",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "YouTube API verwenden",
"ce10d31febb3d9d60c160750570310f303a22c22": "Youtube API-Schlüssel",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Schlüsselgeneration ist einfach!",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Hier klicken",
"7f09776373995003161235c0c8d02b7f91dbc4df": "um die offizielle YoutubeDL-Material Chrome-Erweiterung manuell herunterzuladen.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Die Erweiterung muss manuell installiert werden und in den Einstellungen der Erweiterung muss die Frontend-URL eingetragen werden.",
"9a2ec6da48771128384887525bdcac992632c863": "um die offizielle YoutubeDL-Material Firefox-Erweiterung direkt aus dem Firefox-Addon-Store zu installieren.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Detaillierte Anleitung.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Die Frontend-URL muss in den Einstellungen der Erweiterung eingetragen werden.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Der untenstehende Link muss nur in die Lesezeichenleiste gezogen werden. Auf einer unterstützten Webseite können Sie danach einfach auf das Lesezeichen klicken, um das Video herunterzuladen.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "'Nur Audio' Lesezeichen generieren",
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Standard Download-Agent verwenden",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Downloader auswählen",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Erweiterte Download-Optionen aktivieren",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Erweitert",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Benutzerregistrierung zulassen",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Benutzer",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Sie sind nicht angemeldet.",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Anmelden",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Ausloggen",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Admin-Konto erstellen",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Es wurde kein Standard-Administratorkonto erkannt. Ein Administratorkonto mit dem Benutzernamen \"admin\" wird erstellt und ein Passwort wird festgelegt.",
"70a67e04629f6d412db0a12d51820b480788d795": "Erstellen",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Zeitstempel verwenden",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Sekunden",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "In die Zwischenablage kopieren",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Änderungen speichern",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Details",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Ein Fehler ist aufgetreten:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Videos herunterladen aus den letzten",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Nur Streaming Modus",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonnieren",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Typ:",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archiv:",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Archiv exportieren",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Deabonnieren",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Ihre Abonnements",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nein",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Rolle verwalten",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Benutzername",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten"
}

View File

@@ -196,5 +196,20 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ", "52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ", "59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios", "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

@@ -14,5 +14,5 @@
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head> <link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body> <body>
<app-root></app-root> <app-root></app-root>
<script src="runtime-es2015.06b6262a0d981fd4885e.js" type="module"></script><script src="runtime-es5.06b6262a0d981fd4885e.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> <script src="runtime-es2015.42092efdfb84b81949da.js" type="module"></script><script src="runtime-es5.42092efdfb84b81949da.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> </html>

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:"cc1ef452b2945b55327a"}[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

@@ -0,0 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=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,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(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,a),t.l=!0,t.exports}a.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,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es2015."+{1:"c401a556fe28cac6abab"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.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:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[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:"cc1ef452b2945b55327a"}[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

@@ -0,0 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=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,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(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,a),t.l=!0,t.exports}a.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,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es5."+{1:"c401a556fe28cac6abab"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.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:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

View File

@@ -6,17 +6,20 @@ var path = require('path');
var youtubedl = require('youtube-dl'); var youtubedl = require('youtube-dl');
const config_api = require('./config'); const config_api = require('./config');
var utils = require('./utils')
const debugMode = process.env.YTDL_MODE === 'debug'; const debugMode = process.env.YTDL_MODE === 'debug';
var logger = null; var logger = null;
var db = null; var db = null;
var users_db = null; var users_db = null;
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db } var db_api = null;
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
function setLogger(input_logger) { logger = input_logger; } function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger) { function initialize(input_db, input_users_db, input_logger, input_db_api) {
setDB(input_db, input_users_db); setDB(input_db, input_users_db, input_db_api);
setLogger(input_logger); setLogger(input_logger);
} }
@@ -28,6 +31,7 @@ async function subscribe(sub, user_uid = null) {
return new Promise(async resolve => { return new Promise(async resolve => {
// sub should just have url and name. here we will get isPlaylist and path // sub should just have url and name. here we will get isPlaylist and path
sub.isPlaylist = sub.url.includes('playlist'); sub.isPlaylist = sub.url.includes('playlist');
sub.videos = [];
let url_exists = false; let url_exists = false;
@@ -44,15 +48,25 @@ async function subscribe(sub, user_uid = null) {
} }
// add sub to db // add sub to db
if (user_uid) let sub_db = null;
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write(); users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
else sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
db.get('subscriptions').push(sub).write(); db.get('subscriptions').push(sub).write();
sub_db = db.get('subscriptions').find({id: sub.id});
}
let success = await getSubscriptionInfo(sub, user_uid); let success = await getSubscriptionInfo(sub, user_uid);
if (success) {
sub = sub_db.value();
getVideosForSub(sub, user_uid);
} else {
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
};
result_obj.success = success; result_obj.success = success;
result_obj.sub = sub; result_obj.sub = sub;
getVideosForSub(sub, user_uid);
resolve(result_obj); resolve(result_obj);
}); });
@@ -67,7 +81,15 @@ async function getSubscriptionInfo(sub, user_uid = null) {
return new Promise(resolve => { return new Promise(resolve => {
// get videos // 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) { youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) { if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id); logger.info('Subscribe: got info for subscription ' + sub.id);
@@ -144,6 +166,11 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
else else
db.get('subscriptions').remove({id: id}).write(); 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); const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && fs.existsSync(appendedBasePath)) { if (deleteMode && fs.existsSync(appendedBasePath)) {
if (sub.archive && fs.existsSync(sub.archive)) { if (sub.archive && fs.existsSync(sub.archive)) {
@@ -160,25 +187,33 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
} }
async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null) { async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
let basePath = null; let basePath = null;
if (user_uid) let sub_db = null;
if (user_uid) {
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path'); 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_subscriptions_use_youtubedl_archive');
const appendedBasePath = getAppendedBasePath(sub, basePath); const appendedBasePath = getAppendedBasePath(sub, basePath);
const name = file; const name = file;
let retrievedID = null; let retrievedID = null;
sub_db.get('videos').remove({uid: file_uid}).write();
return new Promise(resolve => { return new Promise(resolve => {
let filePath = appendedBasePath; let filePath = appendedBasePath;
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
var jsonPath = path.join(__dirname,filePath,name+'.info.json'); var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+'.mp4'); var videoFilePath = path.join(__dirname,filePath,name+ext);
var imageFilePath = path.join(__dirname,filePath,name+'.jpg'); var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
jsonExists = fs.existsSync(jsonPath); jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath); videoFileExists = fs.existsSync(videoFilePath);
imageFileExists = fs.existsSync(imageFilePath); imageFileExists = fs.existsSync(imageFilePath);
altImageFileExists = fs.existsSync(altImageFilePath);
if (jsonExists) { if (jsonExists) {
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id']; retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
@@ -189,6 +224,10 @@ async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null)
fs.unlinkSync(imageFilePath); fs.unlinkSync(imageFilePath);
} }
if (altImageFileExists) {
fs.unlinkSync(altImageFilePath);
}
if (videoFileExists) { if (videoFileExists) {
fs.unlink(videoFilePath, function(err) { fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) { if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
@@ -237,13 +276,45 @@ async function getVideosForSub(sub, user_uid = null) {
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive'); const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
let appendedBasePath = null let appendedBasePath = null
if (sub.name) { appendedBasePath = getAppendedBasePath(sub, basePath);
appendedBasePath = getAppendedBasePath(sub, basePath);
} else { let multiUserMode = null;
appendedBasePath = path.join(basePath, (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s')); if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
} }
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-info-json', '--print-json']; const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
let fullOutput = appendedBasePath + '/%(title)s' + ext;
if (sub.custom_output) {
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
}
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
} else {
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
}
downloadConfig.push(...qualityPath)
if (sub.custom_args) {
customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {
// if custom args has a custom quality, replce the original quality with that of custom args
const original_output_index = downloadConfig.indexOf('-f');
downloadConfig.splice(original_output_index, 2);
}
downloadConfig.push(...customArgsArray);
}
let archive_dir = null; let archive_dir = null;
let archive_path = null; let archive_path = null;
@@ -286,7 +357,7 @@ async function getVideosForSub(sub, user_uid = null) {
const outputs = err.stdout.split(/\r\n|\r|\n/); const outputs = err.stdout.split(/\r\n|\r|\n/);
for (let i = 0; i < outputs.length; i++) { for (let i = 0; i < outputs.length; i++) {
const output = JSON.parse(outputs[i]); const output = JSON.parse(outputs[i]);
handleOutputJSON(sub, sub_db, output, i === 0) handleOutputJSON(sub, sub_db, output, i === 0, multiUserMode)
if (err.stderr.includes(output['id']) && archive_path) { if (err.stderr.includes(output['id']) && archive_path) {
// we found a video that errored! add it to the archive to prevent future errors // we found a video that errored! add it to the archive to prevent future errors
fs.appendFileSync(archive_path, output['id']); fs.appendFileSync(archive_path, output['id']);
@@ -315,7 +386,7 @@ async function getVideosForSub(sub, user_uid = null) {
} }
const reset_videos = i === 0; const reset_videos = i === 0;
handleOutputJSON(sub, sub_db, output_json, reset_videos); handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
// TODO: Potentially store downloaded files in db? // TODO: Potentially store downloaded files in db?
@@ -323,10 +394,12 @@ async function getVideosForSub(sub, user_uid = null) {
resolve(true); resolve(true);
} }
}); });
}, err => {
logger.error(err);
}); });
} }
function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) { function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
if (sub.streamingOnly) { if (sub.streamingOnly) {
if (reset_videos) { if (reset_videos) {
sub_db.assign({videos: []}).write(); sub_db.assign({videos: []}).write();
@@ -337,6 +410,9 @@ function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) {
// add to db // add to db
sub_db.get('videos').push(output_json).write(); 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);
} }
} }
@@ -354,6 +430,15 @@ function getSubscription(subID, user_uid = null) {
return db.get('subscriptions').find({id: subID}).value(); 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) { function subExists(subID, user_uid = null) {
if (user_uid) if (user_uid)
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value(); return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
@@ -413,6 +498,7 @@ function removeIDFromArchive(archive_path, id) {
module.exports = { module.exports = {
getSubscription : getSubscription, getSubscription : getSubscription,
getAllSubscriptions : getAllSubscriptions, getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe, subscribe : subscribe,
unsubscribe : unsubscribe, unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile, deleteSubscriptionFile : deleteSubscriptionFile,

97
backend/utils.js Normal file
View File

@@ -0,0 +1,97 @@
var fs = require('fs-extra')
var path = require('path')
const config_api = require('./config');
const is_windows = process.platform === 'win32';
function getTrueFileName(unfixed_path, type) {
let fixed_path = unfixed_path;
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
let unfixed_parts = unfixed_path.split('.');
const old_ext = unfixed_parts[unfixed_parts.length-1];
if (old_ext !== new_ext) {
unfixed_parts[unfixed_parts.length-1] = new_ext;
fixed_path = unfixed_parts.join('.');
}
return fixed_path;
}
function getJSONMp4(name, customPath, openReadPerms = false) {
var obj = null; // output
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
var jsonPath = path.join(customPath, name + ".info.json");
var alternateJsonPath = path.join(customPath, name + ".mp4.info.json");
if (fs.existsSync(jsonPath))
{
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
} else if (fs.existsSync(alternateJsonPath)) {
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
}
else obj = 0;
return obj;
}
function getJSONMp3(name, customPath, openReadPerms = false) {
var obj = null;
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
var jsonPath = path.join(customPath, name + ".info.json");
var alternateJsonPath = path.join(customPath, name + ".mp3.info.json");
if (fs.existsSync(jsonPath)) {
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
}
else if (fs.existsSync(alternateJsonPath)) {
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
}
else
obj = 0;
return obj;
}
function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
const files_to_fix = [
// JSONs
path.join(customPath, name + '.info.json'),
path.join(customPath, name + ext + '.info.json'),
// Thumbnails
path.join(customPath, name + '.webp'),
path.join(customPath, name + '.jpg')
];
for (const file of files_to_fix) {
if (!fs.existsSync(file)) continue;
fs.chmodSync(file, 0o644);
}
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
this.id = id;
this.title = title;
this.thumbnailURL = thumbnailURL;
this.isAudio = isAudio;
this.duration = duration;
this.url = url;
this.uploader = uploader;
this.size = size;
this.path = path;
this.upload_date = upload_date;
}
module.exports = {
getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4,
getTrueFileName: getTrueFileName,
fixVideoMetadataPerms: fixVideoMetadataPerms,
File: File
}

Binary file not shown.

View File

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

View File

30
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "youtube-dl-material", "name": "youtube-dl-material",
"version": "4.0.0", "version": "4.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -2557,6 +2557,16 @@
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
"dev": true "dev": true
}, },
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"blob": { "blob": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
@@ -5230,6 +5240,13 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
"integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
}, },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filename-regex": { "filename-regex": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
@@ -7155,6 +7172,8 @@
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1",
"node-pre-gyp": "*" "node-pre-gyp": "*"
}, },
"dependencies": { "dependencies": {
@@ -9077,6 +9096,11 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true "dev": true
}, },
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -14122,6 +14146,8 @@
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1",
"node-pre-gyp": "*" "node-pre-gyp": "*"
}, },
"dependencies": { "dependencies": {
@@ -15200,6 +15226,8 @@
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1",
"node-pre-gyp": "*" "node-pre-gyp": "*"
}, },
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "youtube-dl-material", "name": "youtube-dl-material",
"version": "4.0.0", "version": "4.1.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
@@ -34,6 +34,7 @@
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"fingerprintjs2": "^2.1.0", "fingerprintjs2": "^2.1.0",
"nan": "^2.14.1",
"ng-lazyload-image": "^7.0.1", "ng-lazyload-image": "^7.0.1",
"ngx-file-drop": "^9.0.1", "ngx-file-drop": "^9.0.1",
"ngx-videogular": "^9.0.1", "ngx-videogular": "^9.0.1",

View File

@@ -23,7 +23,7 @@
<span i18n="Dark mode toggle label">Dark</span> <span i18n="Dark mode toggle label">Dark</span>
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle> <mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
</button> </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> <mat-icon>settings</mat-icon>
<span i18n="Settings menu label">Settings</span> <span i18n="Settings menu label">Settings</span>
</button> </button>

View File

@@ -21,7 +21,6 @@ import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { OverlayContainer } from '@angular/cdk/overlay'; import { OverlayContainer } from '@angular/cdk/overlay';
import { THEMES_CONFIG } from '../themes'; import { THEMES_CONFIG } from '../themes';
import { SettingsComponent } from './settings/settings.component'; 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 { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-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'; import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
@@ -42,8 +41,6 @@ export class AppComponent implements OnInit {
allowThemeChange = null; allowThemeChange = null;
allowSubscriptions = false; allowSubscriptions = false;
enableDownloadsManager = false; enableDownloadsManager = false;
// defaults to true to prevent attack
settingsPinRequired = true;
@ViewChild('sidenav') sidenav: MatSidenav; @ViewChild('sidenav') sidenav: MatSidenav;
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef; @ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
@@ -79,7 +76,6 @@ export class AppComponent implements OnInit {
loadConfig() { loadConfig() {
// loading config // loading config
this.topBarTitle = this.postsService.config['Extra']['title_top']; this.topBarTitle = this.postsService.config['Extra']['title_top'];
this.settingsPinRequired = this.postsService.config['Extra']['settings_pin_required'];
const themingExists = this.postsService.config['Themes']; const themingExists = this.postsService.config['Themes'];
this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default'; this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default';
this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true; this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true;
@@ -175,31 +171,11 @@ onSetTheme(theme, old_theme) {
} }
openSettingsDialog() { openSettingsDialog() {
if (this.settingsPinRequired) {
this.openPinDialog();
} else {
this.actuallyOpenSettingsDialog();
}
}
actuallyOpenSettingsDialog() {
const dialogRef = this.dialog.open(SettingsComponent, { const dialogRef = this.dialog.open(SettingsComponent, {
width: '80vw' width: '80vw'
}); });
} }
openPinDialog() {
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.actuallyOpenSettingsDialog();
}
});
}
openAboutDialog() { openAboutDialog() {
const dialogRef = this.dialog.open(AboutDialogComponent, { const dialogRef = this.dialog.open(AboutDialogComponent, {
width: '80vw' width: '80vw'

View File

@@ -51,7 +51,6 @@ import { SubscriptionComponent } from './subscription//subscription/subscription
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component'; import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component'; import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
import { SettingsComponent } from './settings/settings.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 { MatChipsModule } from '@angular/material/chips';
import { NgxFileDropModule } from 'ngx-file-drop'; import { NgxFileDropModule } from 'ngx-file-drop';
@@ -71,6 +70,10 @@ import { AddUserDialogComponent } from './dialogs/add-user-dialog/add-user-dialo
import { ManageUserComponent } from './components/manage-user/manage-user.component'; import { ManageUserComponent } from './components/manage-user/manage-user.component';
import { ManageRoleComponent } from './components/manage-role/manage-role.component'; import { ManageRoleComponent } from './components/manage-role/manage-role.component';
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component'; import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
registerLocaleData(es, 'es'); registerLocaleData(es, 'es');
@@ -93,7 +96,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
SubscriptionFileCardComponent, SubscriptionFileCardComponent,
SubscriptionInfoDialogComponent, SubscriptionInfoDialogComponent,
SettingsComponent, SettingsComponent,
CheckOrSetPinDialogComponent,
AboutDialogComponent, AboutDialogComponent,
VideoInfoDialogComponent, VideoInfoDialogComponent,
ArgModifierDialogComponent, ArgModifierDialogComponent,
@@ -109,7 +111,11 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
AddUserDialogComponent, AddUserDialogComponent,
ManageUserComponent, ManageUserComponent,
ManageRoleComponent, ManageRoleComponent,
CookiesUploaderDialogComponent CookiesUploaderDialogComponent,
LogsViewerComponent,
ModifyPlaylistComponent,
ConfirmDialogComponent,
EditSubscriptionDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -163,8 +169,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
CreatePlaylistComponent, CreatePlaylistComponent,
SubscribeDialogComponent, SubscribeDialogComponent,
SubscriptionInfoDialogComponent, SubscriptionInfoDialogComponent,
SettingsComponent, SettingsComponent
CheckOrSetPinDialogComponent
], ],
providers: [ providers: [
PostsService PostsService

View File

@@ -14,6 +14,9 @@
</div> </div>
</div> </div>
</div> </div>
<div>
<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> </mat-card>
</ng-container> </ng-container>
</div> </div>

View File

@@ -43,7 +43,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
valid_sessions_length = 0; valid_sessions_length = 0;
sort_downloads = (a, b) => { sort_downloads = (a, b) => {
const result = a.value.timestamp_start < b.value.timestamp_start; const result = b.value.timestamp_start - a.value.timestamp_start;
return result; return result;
} }
@@ -81,7 +81,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
clearDownload(session_id, download_uid) { clearDownload(session_id, download_uid) {
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => { this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
if (res['success']) { if (res['success']) {
this.downloads = res['downloads']; // this.downloads = res['downloads'];
} else { } else {
} }
}); });
@@ -107,11 +107,32 @@ export class DownloadsComponent implements OnInit, OnDestroy {
assignNewValues(new_downloads_by_session) { assignNewValues(new_downloads_by_session) {
const session_keys = Object.keys(new_downloads_by_session); const session_keys = Object.keys(new_downloads_by_session);
// remove missing session IDs
const current_session_ids = Object.keys(this.downloads);
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
for (const missing_session_id of missing_session_ids) {
delete this.downloads[missing_session_id];
}
// loop through sessions
for (let i = 0; i < session_keys.length; i++) { for (let i = 0; i < session_keys.length; i++) {
const session_id = session_keys[i]; const session_id = session_keys[i];
const session_downloads_by_id = new_downloads_by_session[session_id]; const session_downloads_by_id = new_downloads_by_session[session_id];
const session_download_ids = Object.keys(session_downloads_by_id); const session_download_ids = Object.keys(session_downloads_by_id);
if (this.downloads[session_id]) {
// remove missing download IDs
const current_download_ids = Object.keys(this.downloads[session_id]);
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
for (const missing_download_id of missing_download_ids) {
console.log('removing missing download id');
delete this.downloads[session_id][missing_download_id];
}
}
if (!this.downloads[session_id]) { if (!this.downloads[session_id]) {
this.downloads[session_id] = session_downloads_by_id; this.downloads[session_id] = session_downloads_by_id;
} else { } else {

View File

@@ -49,9 +49,19 @@ export class LoginComponent implements OnInit {
this.loggingIn = false; this.loggingIn = false;
if (res['token']) { if (res['token']) {
this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']); this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
} else {
this.openSnackBar('Login failed, unknown error.');
} }
}, err => { }, err => {
this.loggingIn = false; this.loggingIn = false;
const error_code = err.status;
if (error_code === 401) {
this.openSnackBar('User name or password is incorrect!');
} else if (error_code === 404) {
this.openSnackBar('Login failed, cannot connect to the server.');
} else {
this.openSnackBar('Login failed, unknown error.');
}
}); });
} }
@@ -84,7 +94,7 @@ export class LoginComponent implements OnInit {
this.loginUsernameInput = res['user']['name']; this.loginUsernameInput = res['user']['name'];
this.selectedTabIndex = 0; this.selectedTabIndex = 0;
} else { } else {
this.openSnackBar('Failed to register user, unknown error.');
} }
}, err => { }, err => {
this.registering = false; this.registering = false;

View File

@@ -0,0 +1,37 @@
<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>
<!-- 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="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">
<mat-option [value]="10">10</mat-option>
<mat-option [value]="25">25</mat-option>
<mat-option [value]="50">50</mat-option>
<mat-option [value]="100">100</mat-option>
<mat-option [value]="0">All</mat-option>
</mat-select>
</mat-form-field>
</div>
<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

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

View File

@@ -0,0 +1,85 @@
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',
templateUrl: './logs-viewer.component.html',
styleUrls: ['./logs-viewer.component.scss']
})
export class LogsViewerComponent implements OnInit {
logs: any = null;
logs_text: string = null;
requested_lines = 50;
logs_loading = false;
constructor(private postsService: PostsService, private dialog: MatDialog) { }
ngOnInit(): void {
this.getLogs();
}
getLogs() {
if (!this.logs) { this.logs_loading = true; } // only show loading spinner at the first load
this.postsService.getLogs(this.requested_lines !== 0 ? this.requested_lines : null).subscribe(res => {
this.logs_loading = false;
if (res['logs'] !== 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!');
}
}, err => {
this.logs_loading = false;
console.error(err);
this.postsService.openSnackBar('Failed to retrieve logs!');
});
}
copiedLogsToClipboard() {
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> <h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
<span matLine> <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-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="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-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group> </mat-radio-group>
@@ -27,5 +27,5 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <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> </mat-dialog-actions>

View File

@@ -4,7 +4,7 @@
<div class="table table-responsive px-5 pb-4 pt-2"> <div class="table table-responsive px-5 pb-4 pt-2">
<div class="example-header"> <div class="example-header">
<mat-form-field> <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> </mat-form-field>
</div> </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-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
<span *ngIf="editObject && editObject.uid === row.uid; else notediting"> <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> <mat-icon>done</mat-icon>
</button> </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> <mat-icon>cancel</mat-icon>
</button> </button>
</span> </span>
<ng-template #notediting> <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> <mat-icon>edit</mat-icon>
</button> </button>
</ng-template> </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> <mat-icon>settings</mat-icon>
</button> </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> <mat-icon>delete</mat-icon>
</button> </button>
</mat-cell> </mat-cell>

View File

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

View File

@@ -18,7 +18,7 @@
<h5 style="margin-top: 10px;">Installation details:</h5> <h5 style="margin-top: 10px;">Installation details:</h5>
<p> <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> <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> <span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p> </p>
<p> <p>
@@ -28,5 +28,5 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close>Close</button> <button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions> </mat-dialog-actions>

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

@@ -0,0 +1,28 @@
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<mat-dialog-content>
<!-- Playlist info -->
<div>
<mat-form-field color="accent">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
</mat-form-field>
</div>
<!-- Playlist order -->
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of playlist.fileNames; let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<div class="add-content-button">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu">Add more content</button>
</div>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
</mat-menu>
</mat-dialog-content>
<mat-dialog-actions>
<!-- Save -->
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent">Save</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,50 @@
.media-list {
}
.media-box {
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.media-box:last-child {
border: none;
}
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.add-content-button {
margin-top: 15px;
margin-bottom: 10px;
}
.remove-item-button {
right: 10px;
position: absolute;
top: 4px;
}
.playlist-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
margin: 0 auto;
}

View File

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

View File

@@ -0,0 +1,83 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-modify-playlist',
templateUrl: './modify-playlist.component.html',
styleUrls: ['./modify-playlist.component.scss']
})
export class ModifyPlaylistComponent implements OnInit {
original_playlist = null;
playlist = null;
available_files = [];
all_files = [];
playlist_updated = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
private postsService: PostsService,
public dialogRef: MatDialogRef<ModifyPlaylistComponent>) { }
ngOnInit(): void {
if (this.data) {
this.playlist = JSON.parse(JSON.stringify(this.data.playlist));
this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist));
this.getFiles();
}
}
getFiles() {
if (this.playlist.type === 'audio') {
this.postsService.getMp3s().subscribe(res => {
this.processFiles(res['mp3s']);
});
} else {
this.postsService.getMp4s().subscribe(res => {
this.processFiles(res['mp4s']);
});
}
}
processFiles(new_files = null) {
if (new_files) { this.all_files = new_files.map(file => file.id); }
this.available_files = this.all_files.filter(e => !this.playlist.fileNames.includes(e))
}
updatePlaylist() {
this.postsService.updatePlaylist(this.playlist).subscribe(res => {
this.playlist_updated = true;
this.postsService.openSnackBar('Playlist updated successfully.');
this.getPlaylist();
});
}
playlistChanged() {
return JSON.stringify(this.playlist) === JSON.stringify(this.original_playlist);
}
getPlaylist() {
this.postsService.getPlaylist(this.playlist.id, this.playlist.type, null).subscribe(res => {
if (res['playlist']) {
this.playlist = res['playlist'];
this.original_playlist = JSON.parse(JSON.stringify(this.playlist));
}
});
}
addContent(file) {
this.playlist.fileNames.push(file);
this.processFiles();
}
removeContent(index) {
this.playlist.fileNames.splice(index, 1);
this.processFiles();
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex);
}
}

View File

@@ -3,16 +3,20 @@
<mat-dialog-content> <mat-dialog-content>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12 mb-4">
<mat-form-field color="accent"> <mat-form-field color="accent">
<input [(ngModel)]="url" matInput placeholder="URL" i18n-placeholder="Subscription URL input placeholder" required aria-required="true"> <input [(ngModel)]="url" matInput placeholder="URL" i18n-placeholder="Subscription URL input placeholder" required aria-required="true">
<mat-hint><ng-container i18n="Subscription URL input hint">The playlist or channel URL</ng-container></mat-hint> <mat-hint><ng-container i18n="Subscription URL input hint">The playlist or channel URL</ng-container></mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
</div>
</div>
<mat-divider></mat-divider>
<div class="container-fluid">
<div class="row">
<div class="col-12"> <div class="col-12">
<mat-form-field color="accent"> <mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Custom name" i18n-placeholder="Subscription custom name placeholder"> <input [(ngModel)]="name" matInput placeholder="Custom name" i18n-placeholder="Subscription custom name placeholder">
<mat-hint><ng-container i18n="Custom name input hint">This is optional</ng-container></mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">
@@ -20,20 +24,46 @@
</div> </div>
<div class="col-12" *ngIf="!download_all"> <div class="col-12" *ngIf="!download_all">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container> <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"> <input type="number" matInput [(ngModel)]="timerange_amount">
</mat-form-field> </mat-form-field>
<mat-select color="accent" class="unit-select" [(ngModel)]="timerange_unit"> <mat-form-field class="unit-select">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')"> <mat-select color="accent" [(ngModel)]="timerange_unit">
{{time_unit + (timerange_amount === 1 ? '' : 's')}} <mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
</mat-option> {{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-select> </mat-option>
</mat-select>
</mat-form-field>
</div> </div>
<div class="col-12"> <div class="col-12">
<div> <div>
<mat-checkbox [(ngModel)]="streamingOnlyMode"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox> <mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div> </div>
</div> </div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="audioOnlyMode" [(ngModel)]="streamingOnlyMode"><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)]="customArgs" 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)]="customFileOutput" 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>
</div> </div>
</mat-dialog-content> </mat-dialog-content>

View File

@@ -6,3 +6,8 @@
.mat-spinner { .mat-spinner {
margin-left: 5%; margin-left: 5%;
} }
.args-edit-button {
position: absolute;
margin-left: 10px;
}

View File

@@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef, MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component';
@Component({ @Component({
selector: 'app-subscribe-dialog', selector: 'app-subscribe-dialog',
@@ -22,6 +23,12 @@ export class SubscribeDialogComponent implements OnInit {
// no videos actually downloaded, just streamed // no videos actually downloaded, just streamed
streamingOnlyMode = false; streamingOnlyMode = false;
// audio only mode
audioOnlyMode = false;
customFileOutput = '';
customArgs = '';
time_units = [ time_units = [
'day', 'day',
'week', 'week',
@@ -31,6 +38,7 @@ export class SubscribeDialogComponent implements OnInit {
constructor(private postsService: PostsService, constructor(private postsService: PostsService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private dialog: MatDialog,
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { } public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
ngOnInit() { ngOnInit() {
@@ -49,7 +57,8 @@ export class SubscribeDialogComponent implements OnInit {
if (!this.download_all) { if (!this.download_all) {
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit; timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
} }
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode).subscribe(res => { this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
this.subscribing = false; this.subscribing = false;
if (res['new_sub']) { if (res['new_sub']) {
this.dialogRef.close(res['new_sub']); this.dialogRef.close(res['new_sub']);
@@ -63,6 +72,20 @@ export class SubscribeDialogComponent implements OnInit {
} }
} }
// modify custom args
openArgsModifierDialog() {
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
data: {
initial_args: this.customArgs
}
});
dialogRef.afterClosed().subscribe(new_args => {
if (new_args !== null && new_args !== undefined) {
this.customArgs = new_args;
}
});
}
public openSnackBar(message: string, action = '') { public openSnackBar(message: string, action = '') {
this.snackBar.open(message, action, { this.snackBar.open(message, action, {
duration: 2000, duration: 2000,

View File

@@ -5,8 +5,8 @@
</mat-grid-tile> </mat-grid-tile>
<mat-grid-tile [colspan]="13"> <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-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.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" matTooltip-i18n>error</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>
<mat-grid-tile [colspan]="4"> <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> <button style="margin-bottom: 2px;" (click)="cancelTheDownload()" mat-icon-button color="warn"><mat-icon fontSet="material-icons-outlined">cancel</mat-icon></button>

View File

@@ -2,10 +2,10 @@
<div style="padding:5px"> <div style="padding:5px">
<div style="height: 52px;"> <div style="height: 52px;">
<div> <div>
<b><a class="file-link" href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b> <b><a class="file-link" href="javascript:void(0)" (click)="!playlist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
</div> </div>
<span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container>&nbsp;{{name}}</span> <span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container>&nbsp;{{name}}</span>
<div *ngIf="isPlaylist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{count}}</div> <div *ngIf="playlist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{count}}</div>
</div> </div>
<div *ngIf="!image_errored && thumbnailURL" class="img-div"> <div *ngIf="!image_errored && thumbnailURL" class="img-div">
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail"> <img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
@@ -14,11 +14,16 @@
</span> </span>
</div> </div>
</div> </div>
<button *ngIf="isPlaylist" (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button> <button [matMenuTriggerFor]="playlist_menu" *ngIf="playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<button [matMenuTriggerFor]="action_menu" *ngIf="!isPlaylist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button> <mat-menu #playlist_menu="matMenu">
<button (click)="editPlaylistDialog()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</mat-menu>
<button [matMenuTriggerFor]="action_menu" *ngIf="!playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu"> <mat-menu #action_menu="matMenu">
<button (click)="openVideoInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button> <button (click)="openVideoInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button> <button (click)="deleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="use_youtubedl_archive" (click)="deleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button> <button *ngIf="use_youtubedl_archive" (click)="deleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</mat-menu> </mat-menu>
</mat-card> </mat-card>

View File

@@ -7,6 +7,7 @@ import { Subject, Observable } from 'rxjs';
import 'rxjs/add/observable/merge'; import 'rxjs/add/observable/merge';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component'; import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { ModifyPlaylistComponent } from '../dialogs/modify-playlist/modify-playlist.component';
@Component({ @Component({
selector: 'app-file-card', selector: 'app-file-card',
@@ -22,7 +23,7 @@ export class FileCardComponent implements OnInit {
@Input() thumbnailURL: string; @Input() thumbnailURL: string;
@Input() isAudio = true; @Input() isAudio = true;
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>(); @Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
@Input() isPlaylist = false; @Input() playlist = null;
@Input() count = null; @Input() count = null;
@Input() use_youtubedl_archive = false; @Input() use_youtubedl_archive = false;
type; type;
@@ -46,15 +47,15 @@ export class FileCardComponent implements OnInit {
this.type = this.isAudio ? 'audio' : 'video'; this.type = this.isAudio ? 'audio' : 'video';
if (this.file && this.file.url && this.file.url.includes('youtu')) { if (this.file && this.file.url && this.file.url.includes('youtu')) {
const string_id = (this.isPlaylist ? '?list=' : '?v=') const string_id = (this.playlist ? '?list=' : '?v=')
const index_offset = (this.isPlaylist ? 6 : 3); const index_offset = (this.playlist ? 6 : 3);
const end_index = this.file.url.indexOf(string_id) + index_offset; const end_index = this.file.url.indexOf(string_id) + index_offset;
this.name = this.file.url.substring(end_index, this.file.url.length); this.name = this.file.url.substring(end_index, this.file.url.length);
} }
} }
deleteFile(blacklistMode = false) { deleteFile(blacklistMode = false) {
if (!this.isPlaylist) { if (!this.playlist) {
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => { this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
if (result) { if (result) {
this.openSnackBar('Delete success!', 'OK.'); this.openSnackBar('Delete success!', 'OK.');
@@ -80,6 +81,24 @@ export class FileCardComponent implements OnInit {
}); });
} }
editPlaylistDialog() {
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: this.playlist,
width: '65vw'
}
});
dialogRef.afterClosed().subscribe(res => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlist = dialogRef.componentInstance.original_playlist;
this.title = this.playlist.name;
this.count = this.playlist.fileNames.length;
}
});
}
onImgError(event) { onImgError(event) {
this.image_errored = true; this.image_errored = true;
} }

View File

@@ -11,10 +11,7 @@
<div class="row"> <div class="row">
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12"> <div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
<mat-form-field color="accent" class="example-full-width"> <mat-form-field color="accent" class="example-full-width">
<input style="padding-right: 25px;" matInput (keyup.enter)="downloadClicked()" (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" #urlinput> <input style="padding-right: 25px;" matInput (keyup.enter)="downloadClicked()" (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput>
<mat-error *ngIf="urlError || urlForm.invalid">
<ng-container i18n="Enter valid URL error">Please enter a valid URL!</ng-container>
</mat-error>
</mat-form-field> </mat-form-field>
<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button> <button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
</div> </div>
@@ -216,7 +213,7 @@
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px"> <mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;"> <mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]" <app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card> [length]="null" [isAudio]="true" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile> </mat-grid-tile>
</mat-grid-list> </mat-grid-list>
@@ -258,7 +255,7 @@
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px"> <mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;"> <mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]" <app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card> [length]="null" [isAudio]="false" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile> </mat-grid-tile>
</mat-grid-list> </mat-grid-list>

View File

@@ -439,10 +439,11 @@ export class MainComponent implements OnInit {
public removeFromMp3(name: string) { public removeFromMp3(name: string) {
for (let i = 0; i < this.mp3s.length; i++) { for (let i = 0; i < this.mp3s.length; i++) {
if (this.mp3s[i].id === name) { if (this.mp3s[i].id === name || this.mp3s[i].id + '.mp3' === name) {
this.mp3s.splice(i, 1); this.mp3s.splice(i, 1);
} }
} }
this.getMp3s();
} }
public removePlaylistMp3(playlistID, index) { public removePlaylistMp3(playlistID, index) {
@@ -457,10 +458,11 @@ export class MainComponent implements OnInit {
public removeFromMp4(name: string) { public removeFromMp4(name: string) {
for (let i = 0; i < this.mp4s.length; i++) { for (let i = 0; i < this.mp4s.length; i++) {
if (this.mp4s[i].id === name) { if (this.mp4s[i].id === name || this.mp4s[i].id + '.mp4' === name) {
this.mp4s.splice(i, 1); this.mp4s.splice(i, 1);
} }
} }
this.getMp4s();
} }
public removePlaylistMp4(playlistID, index) { public removePlaylistMp4(playlistID, index) {

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core'; import { Component, OnInit, HostListener, EventEmitter, OnDestroy } from '@angular/core';
import { VgAPI } from 'ngx-videogular'; import { VgAPI } from 'ngx-videogular';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface IMedia {
templateUrl: './player.component.html', templateUrl: './player.component.html',
styleUrls: ['./player.component.css'] styleUrls: ['./player.component.css']
}) })
export class PlayerComponent implements OnInit { export class PlayerComponent implements OnInit, OnDestroy {
playlist: Array<IMedia> = []; playlist: Array<IMedia> = [];
original_playlist: string = null; original_playlist: string = null;
@@ -62,6 +62,9 @@ export class PlayerComponent implements OnInit {
downloading = false; downloading = false;
save_volume_timer = null;
original_volume = null;
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(event) { onResize(event) {
this.innerWidth = window.innerWidth; this.innerWidth = window.innerWidth;
@@ -92,6 +95,11 @@ export class PlayerComponent implements OnInit {
} }
} }
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, constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
public snackBar: MatSnackBar) { public snackBar: MatSnackBar) {
@@ -127,7 +135,7 @@ export class PlayerComponent implements OnInit {
this.currentItem = this.playlist[0]; this.currentItem = this.playlist[0];
this.currentIndex = 0; this.currentIndex = 0;
this.show_player = true; this.show_player = true;
} else if (this.type === 'subscription' || this.fileNames) { } else if (this.subscriptionName || this.fileNames) {
this.show_player = true; this.show_player = true;
this.parseFileNames(); this.parseFileNames();
} }
@@ -181,9 +189,6 @@ export class PlayerComponent implements OnInit {
fileType = 'audio/mp3'; fileType = 'audio/mp3';
} else if (this.type === 'video') { } else if (this.type === 'video') {
fileType = 'video/mp4'; fileType = 'video/mp4';
} else if (this.type === 'subscription') {
// only supports mp4 for now
fileType = 'video/mp4';
} else { } else {
// error // error
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.'); console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
@@ -198,7 +203,7 @@ export class PlayerComponent implements OnInit {
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName); fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
} else { } else {
// default to video but include subscription name param // default to video but include subscription name param
baseLocation = 'video/'; baseLocation = this.type === 'audio' ? 'audio/' : 'video/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName + fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist; '&subPlaylist=' + this.subPlaylist;
} }
@@ -238,6 +243,13 @@ export class PlayerComponent implements OnInit {
onPlayerReady(api: VgAPI) { onPlayerReady(api: VgAPI) {
this.api = api; this.api = api;
// checks if volume has been previously set. if so, use that as default
if (localStorage.getItem('player_volume')) {
this.api.volume = parseFloat(localStorage.getItem('player_volume'));
}
this.save_volume_timer = setInterval(() => this.saveVolume(this.api), 2000)
this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this)); this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this));
this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this)); this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this));
@@ -246,6 +258,13 @@ export class PlayerComponent implements OnInit {
} }
} }
saveVolume(api) {
if (this.original_volume !== api.volume) {
localStorage.setItem('player_volume', api.volume)
this.original_volume = api.volume;
}
}
nextVideo() { nextVideo() {
if (this.currentIndex === this.playlist.length - 1) { if (this.currentIndex === this.playlist.length - 1) {
// dont continue playing // dont continue playing
@@ -377,7 +396,7 @@ export class PlayerComponent implements OnInit {
updatePlaylist() { updatePlaylist() {
const fileNames = this.getFileNames(); const fileNames = this.getFileNames();
this.playlist_updating = true; this.playlist_updating = true;
this.postsService.updatePlaylist(this.id, fileNames, this.type).subscribe(res => { this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => {
this.playlist_updating = false; this.playlist_updating = false;
if (res['success']) { if (res['success']) {
const fileNamesEncoded = fileNames.join('|nvr|'); const fileNamesEncoded = fileNames.join('|nvr|');

View File

@@ -73,17 +73,22 @@ export class PostsService implements CanActivate {
this.httpOptions.params = this.httpOptions.params.set('sessionID', this.session_id); this.httpOptions.params = this.httpOptions.params.set('sessionID', this.session_id);
}); });
const redirect_not_required = window.location.href.includes('/player') || window.location.href.includes('/login');
// get config // get config
this.loadNavItems().subscribe(res => { this.loadNavItems().subscribe(res => {
const result = !this.debugMode ? res['config_file'] : res; const result = !this.debugMode ? res['config_file'] : res;
if (result) { if (result) {
this.config = result['YoutubeDLMaterial']; this.config = result['YoutubeDLMaterial'];
if (this.config['Advanced']['multi_user_mode']) { if (this.config['Advanced']['multi_user_mode']) {
this.checkAdminCreationStatus();
// login stuff // login stuff
if (localStorage.getItem('jwt_token')) { if (localStorage.getItem('jwt_token') && localStorage.getItem('jwt_token') !== 'null') {
this.token = localStorage.getItem('jwt_token'); this.token = localStorage.getItem('jwt_token');
this.httpOptions.params = this.httpOptions.params.set('jwt', this.token); this.httpOptions.params = this.httpOptions.params.set('jwt', this.token);
this.jwtAuth(); this.jwtAuth();
} else if (redirect_not_required) {
this.setInitialized();
} else { } else {
this.sendToLogin(); this.sendToLogin();
} }
@@ -159,12 +164,8 @@ export class PostsService implements CanActivate {
ui_uid: ui_uid}, this.httpOptions); ui_uid: ui_uid}, this.httpOptions);
} }
getFileStatusMp3(name: string) { killAllDownloads() {
return this.http.post(this.path + 'fileStatusMp3', {name: name}, this.httpOptions); return this.http.post(this.path + 'killAllDownloads', {}, this.httpOptions);
}
getFileStatusMp4(name: string) {
return this.http.post(this.path + 'fileStatusMp4', {name: name}, this.httpOptions);
} }
loadNavItems() { loadNavItems() {
@@ -230,16 +231,12 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode}, this.httpOptions); return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode}, this.httpOptions);
} }
isPinSet() { getLogs(lines = 50) {
return this.http.post(this.path + 'isPinSet', {}, this.httpOptions); return this.http.post(this.path + 'logs', {lines: lines}, this.httpOptions);
} }
setPin(unhashed_pin) { clearAllLogs() {
return this.http.post(this.path + 'setPin', {pin: unhashed_pin}, this.httpOptions); return this.http.post(this.path + 'clearAllLogs', {}, this.httpOptions);
}
checkPin(unhashed_pin) {
return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin}, this.httpOptions);
} }
generateNewAPIKey() { generateNewAPIKey() {
@@ -266,8 +263,12 @@ export class PostsService implements CanActivate {
type: type, uuid: uuid}, this.httpOptions); type: type, uuid: uuid}, this.httpOptions);
} }
updatePlaylist(playlistID, fileNames, type) { updatePlaylist(playlist) {
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID, return this.http.post(this.path + 'updatePlaylist', {playlist: playlist}, this.httpOptions);
}
updatePlaylistFiles(playlistID, fileNames, type) {
return this.http.post(this.path + 'updatePlaylistFiles', {playlistID: playlistID,
fileNames: fileNames, fileNames: fileNames,
type: type}, this.httpOptions); type: type}, this.httpOptions);
} }
@@ -276,17 +277,22 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions); return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
} }
createSubscription(url, name, timerange = null, streamingOnly = false) { createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) {
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly}, return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly,
this.httpOptions); 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) { unsubscribe(sub, deleteMode = false) {
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions) return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
} }
deleteSubscriptionFile(sub, file, deleteForever) { deleteSubscriptionFile(sub, file, deleteForever, file_uid) {
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever}, this.httpOptions) return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever,
file_uid: file_uid}, this.httpOptions)
} }
getSubscription(id) { getSubscription(id) {
@@ -377,6 +383,7 @@ export class PostsService implements CanActivate {
this.user = null; this.user = null;
this.permissions = null; this.permissions = null;
this.isLoggedIn = false; this.isLoggedIn = false;
this.token = null;
localStorage.setItem('jwt_token', null); localStorage.setItem('jwt_token', null);
if (this.router.url !== '/login') { if (this.router.url !== '/login') {
this.router.navigate(['/login']); this.router.navigate(['/login']);
@@ -397,17 +404,13 @@ export class PostsService implements CanActivate {
const call = this.http.post(this.path + 'auth/register', {userid: username, const call = this.http.post(this.path + 'auth/register', {userid: username,
username: username, username: username,
password: password}, this.httpOptions); password: password}, this.httpOptions);
/*call.subscribe(res => {
console.log(res['user']);
if (res['user']) {
// this.afterRegistration(res['user']);
}
});*/
return call; return call;
} }
sendToLogin() { sendToLogin() {
this.checkAdminCreationStatus(); if (!this.initialized) {
this.setInitialized();
}
if (this.router.url === '/login') { if (this.router.url === '/login') {
return; return;
} }
@@ -434,8 +437,8 @@ export class PostsService implements CanActivate {
password: password}, this.httpOptions); password: password}, this.httpOptions);
} }
checkAdminCreationStatus(skip_check = false) { checkAdminCreationStatus(force_show = false) {
if (!skip_check && !this.config['Advanced']['multi_user_mode']) { if (!force_show && !this.config['Advanced']['multi_user_mode']) {
return; return;
} }
this.adminExists().subscribe(res => { 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-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> </mat-form-field>
</div> </div>
<div class="col-12 mb-4"> <div class="col-12 mb-4 mt-3">
<mat-form-field class="text-field" color="accent"> <mat-form-field class="text-field" color="accent">
<input [(ngModel)]="new_config['Host']['port']" matInput placeholder="Port" i18n-placeholder="Port input placeholder" required> <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> <mat-hint><ng-container i18n="Port setting input hint">The desired port. Default is 17442.</ng-container></mat-hint>
@@ -140,7 +140,7 @@
</mat-form-field> </mat-form-field>
</div> </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"> <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> <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> <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,9 +148,17 @@
</mat-form-field> </mat-form-field>
</div> </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> <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> <p><ng-container i18n="youtubedl archive setting Note">Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head to the Main tab and activate this option there.</ng-container></p>
</div>
<div class="col-12 mt-2">
<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>
<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> </div>
</div> </div>
@@ -182,10 +190,6 @@
<div class="col-12"> <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> <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>
<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>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@@ -202,7 +206,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="api-key-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> </div>
</div> </div>
@@ -271,7 +275,7 @@
</div> </div>
<div class="col-12 mt-2 mb-1"> <div class="col-12 mt-2 mb-1">
<mat-form-field> <mat-form-field>
<mat-label><ng-container i18n="Logger level select label">Select a downloader</ng-container></mat-label> <mat-label><ng-container i18n="Logger level select label">Select a logger level</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']"> <mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
<mat-option value="debug">Debug</mat-option> <mat-option value="debug">Debug</mat-option>
<mat-option value="verbose">Verbose</mat-option> <mat-option value="verbose">Verbose</mat-option>
@@ -307,6 +311,13 @@
</div> </div>
<app-modify-users></app-modify-users> <app-modify-users></app-modify-users>
</mat-tab> </mat-tab>
<mat-tab *ngIf="postsService.config" label="Logs" i18n-label="Logs settings label">
<ng-template matTabContent>
<div style="margin-left: 48px; margin-top: 24px; height: 340px">
<app-logs-viewer></app-logs-viewer>
</div>
</ng-template>
</mat-tab>
</mat-tab-group> </mat-tab-group>
</mat-dialog-content> </mat-dialog-content>

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 { 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 { isoLangs } from './locales_list';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import {DomSanitizer} from '@angular/platform-browser'; 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 { CURRENT_VERSION } from 'app/consts';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component'; import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
@@ -17,7 +17,7 @@ import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dia
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
all_locales = isoLangs; all_locales = isoLangs;
supported_locales = ['en', 'es']; supported_locales = ['en', 'es', 'de'];
initialLocale = localStorage.getItem('locale'); initialLocale = localStorage.getItem('locale');
initial_config = null; initial_config = null;
@@ -77,14 +77,6 @@ export class SettingsComponent implements OnInit {
}) })
} }
setNewPin() {
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
data: {
resetMode: true
}
});
}
generateAPIKey() { generateAPIKey() {
this.postsService.generateNewAPIKey().subscribe(res => { this.postsService.generateNewAPIKey().subscribe(res => {
if (res['new_api_key']) { 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 // snackbar helper
public openSnackBar(message: string, action: string = '') { public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, { this.snackBar.open(message, action, {

View File

@@ -71,14 +71,14 @@ export class SubscriptionFileCardComponent implements OnInit {
} }
deleteAndRedownload() { deleteAndRedownload() {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false).subscribe(res => { this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false, this.file.uid).subscribe(res => {
this.reloadSubscription.emit(true); this.reloadSubscription.emit(true);
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.'); this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
}); });
} }
deleteForever() { deleteForever() {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true).subscribe(res => { this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true, this.file.uid).subscribe(res => {
this.reloadSubscription.emit(true); this.reloadSubscription.emit(true);
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.'); this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
}); });

View File

@@ -42,5 +42,6 @@
</div> </div>
</div> </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> <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> </div>

View File

@@ -58,6 +58,12 @@
bottom: 25px; bottom: 25px;
} }
.edit-button {
left: 25px;
position: absolute;
bottom: 25px;
}
.save-icon { .save-icon {
bottom: 1px; bottom: 1px;
position: relative; position: relative;

View File

@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
@Component({ @Component({
selector: 'app-subscription', selector: 'app-subscription',
@@ -43,7 +45,7 @@ export class SubscriptionComponent implements OnInit {
filterProperty = this.filterProperties['upload_date']; filterProperty = this.filterProperties['upload_date'];
downloading = false; downloading = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { } constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
ngOnInit() { ngOnInit() {
if (this.route.snapshot.paramMap.get('id')) { if (this.route.snapshot.paramMap.get('id')) {
@@ -92,8 +94,9 @@ export class SubscriptionComponent implements OnInit {
if (this.subscription.streamingOnly) { if (this.subscription.streamingOnly) {
this.router.navigate(['/player', {name: name, url: url}]); this.router.navigate(['/player', {name: name, url: url}]);
} else { } else {
this.router.navigate(['/player', {fileNames: name, type: 'subscription', subscriptionName: this.subscription.name, this.router.navigate(['/player', {fileNames: name,
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]); type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
} }
} }
@@ -147,4 +150,12 @@ export class SubscriptionComponent implements OnInit {
}); });
} }
editSubscription() {
this.dialog.open(EditSubscriptionDialogComponent, {
data: {
sub: this.subscription
}
});
}
} }

View File

@@ -13,7 +13,8 @@
"path-audio": "audio/", "path-audio": "audio/",
"path-video": "video/", "path-video": "video/",
"use_youtubedl_archive": false, "use_youtubedl_archive": false,
"custom_args": "" "custom_args": "",
"safe_download_override": false
}, },
"Extra": { "Extra": {
"title_top": "YoutubeDL-Material", "title_top": "YoutubeDL-Material",
@@ -37,7 +38,7 @@
"Subscriptions": { "Subscriptions": {
"allow_subscriptions": true, "allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/", "subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300", "subscriptions_check_interval": "30",
"subscriptions_use_youtubedl_archive": true "subscriptions_use_youtubedl_archive": true
}, },
"Users": { "Users": {
@@ -49,7 +50,8 @@
"custom_downloading_agent": "", "custom_downloading_agent": "",
"multi_user_mode": true, "multi_user_mode": true,
"allow_advanced_download": true, "allow_advanced_download": true,
"logger_level": "debug" "logger_level": "debug",
"use_cookies": true
} }
} }
} }

View File

@@ -0,0 +1,198 @@
{
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Youtube-dl Argumente ändern",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulierte neue Argumente",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Argument hinzufügen",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Nach Kategorie filtern",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Argument-Wert verwenden",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Argument-Wert",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Argument hinzufügen",
"d7b35c384aecd25a516200d6921836374613dfe7": "Abbrechen",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Ändern",
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "YouTube Downloader",
"6d2ec8898344c8955542b0542c942038ef28bb80": "Bitte geben Sie eine gültige URL ein.",
"a38ae1082fec79ba1f379978337385a539a28e73": "Qualität",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL verwenden",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Ansehen",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Nur Audio",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-Download Modus",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Download",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Abbrechen",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Erweitert",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulierter Befehl:",
"4e4c721129466be9c3862294dc40241b64045998": "Benutzerdefinierte Argumente verwenden",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Benutzerdefinierte Argumente",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Die URL muss nicht angegeben werden, sondern nur der Teil danach. Argumente werden mit zwei Kommata getrennt: ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Benutzerdefinierte Ausgabe verwenden",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Benutzerdefinierte Ausgabe",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentation",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Der Pfad ist relativ zum Konfigurations-Download-Pfad. Dateiendung auslassen.",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Authentifizierung verwenden",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Benutzername",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
"0f59c46ca29e9725898093c9ea6b586730d0624e": "Keine Playlisten verfügbar. Erstellen Sie eine aus heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Name:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Kanal:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Dateigröße:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Pfad:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Hochgeladen am:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Schließen",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Anzahl:",
"321e4419a943044e674beb55b8039f42a9761ca5": "Info",
"826b25211922a1b46436589233cb6f1a163d89b7": "Löschen",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Löschen und zur Blacklist hinzufügen",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Einstellungen",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL, über die auf diese Applikation zugegriffen wird, ohne Port.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Der gewünschte Port. Standard ist 17442.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Multi-User Modus",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Benutzer Basispfad",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Basispfad für Benutzer und deren heruntergeladene Videos.",
"cbe16a57be414e84b6a68309d08fad894df797d6": "Verschlüsselung verwenden",
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Dateipfad zum Zertifikat",
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Dateipfad zum Zertifikatsschlüssel",
"4e3120311801c4acd18de7146add2ee4a4417773": "Abonnements erlauben",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnements Basispfad",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Basispfad für Videos von abonnierten Kanälen und Wiedergabelisten. Dieser ist relativ zum Stammordner von YTDL-Material.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Prüfintervall",
"0f56a7449b77630c114615395bbda4cab398efd8": "Einheit ist Sekunden, nur Zahlen sind erlaubt.",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Youtube-DL Archiv verwenden",
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Mit der Archivfunktion",
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "werden Informationen über Videos, welche durch ein Abonnement heruntergeladen wurden, in einem Textdokument festgehalten. Diese befinden sich in dem Archiv Unterverzeichnis vom Abonnementsordner.",
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Dadurch können Videos permanent gelöscht werden, ohne das Abonnement beenden zu müssen. Außerdem kann dadurch aufgezeichnet werden, welche Videos heruntergeladen wurden. Z. B. im Falle eines Datenverlusts.",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Design",
"ff7cee38a2259526c519f878e71b964f41db4348": "Standard",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Dunkel",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Designänderung erlauben",
"fe46ccaae902ce974e2441abe752399288298619": "Sprache",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Allgemein",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Audio Basispfad",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Dateipfad für Audio-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Qualitätsauswahl erlauben",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Nur Download Modus",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Multi-Download Modus erlauben",
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Einstellungen durch PIN schützen",
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Neuen PIN festlegen",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Öffentliche API aktivieren",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Öffentlicher API-Schlüssel",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Dokumentation ansehen",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generieren",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "YouTube API verwenden",
"ce10d31febb3d9d60c160750570310f303a22c22": "Youtube API-Schlüssel",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Schlüsselgeneration ist einfach!",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Hier klicken",
"7f09776373995003161235c0c8d02b7f91dbc4df": "um die offizielle YoutubeDL-Material Chrome-Erweiterung manuell herunterzuladen.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Die Erweiterung muss manuell installiert werden und in den Einstellungen der Erweiterung muss die Frontend-URL eingetragen werden.",
"9a2ec6da48771128384887525bdcac992632c863": "um die offizielle YoutubeDL-Material Firefox-Erweiterung direkt aus dem Firefox-Addon-Store zu installieren.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Detaillierte Anleitung.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Die Frontend-URL muss in den Einstellungen der Erweiterung eingetragen werden.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Der untenstehende Link muss nur in die Lesezeichenleiste gezogen werden. Auf einer unterstützten Webseite können Sie danach einfach auf das Lesezeichen klicken, um das Video herunterzuladen.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "'Nur Audio' Lesezeichen generieren",
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Standard Download-Agent verwenden",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Downloader auswählen",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Erweiterte Download-Optionen aktivieren",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Erweitert",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Benutzerregistrierung zulassen",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Benutzer",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Sie sind nicht angemeldet.",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Anmelden",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Ausloggen",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Admin-Konto erstellen",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Es wurde kein Standard-Administratorkonto erkannt. Ein Administratorkonto mit dem Benutzernamen \"admin\" wird erstellt und ein Passwort wird festgelegt.",
"70a67e04629f6d412db0a12d51820b480788d795": "Erstellen",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Zeitstempel verwenden",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Sekunden",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "In die Zwischenablage kopieren",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Änderungen speichern",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Details",
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Ein Fehler ist aufgetreten:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Videos herunterladen aus den letzten",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Nur Streaming Modus",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonnieren",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Typ:",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archiv:",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Archiv exportieren",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Deabonnieren",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Ihre Abonnements",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nein",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Rolle verwalten",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Benutzername",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten"
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,10 @@
<context context-type="sourcefile">app/create-playlist/create-playlist.component.html</context> <context context-type="sourcefile">app/create-playlist/create-playlist.component.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
</context-group> </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> <note priority="1" from="description">Playlist name placeholder</note>
</trans-unit> </trans-unit>
<trans-unit id="f47e2d56dd8a145b2e9599da9730c049d52962a2" datatype="html"> <trans-unit id="f47e2d56dd8a145b2e9599da9730c049d52962a2" datatype="html">
@@ -102,7 +106,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context> <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> </context-group>
<note priority="1" from="description">Arg modifier cancel button</note> <note priority="1" from="description">Arg modifier cancel button</note>
</trans-unit> </trans-unit>
@@ -122,21 +126,13 @@
</context-group> </context-group>
<note priority="1" from="description">Youtube downloader home page label</note> <note priority="1" from="description">Youtube downloader home page label</note>
</trans-unit> </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"> <trans-unit id="a38ae1082fec79ba1f379978337385a539a28e73" datatype="html">
<source> <source>
Quality Quality
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">24</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
<note priority="1" from="description">Quality select label</note> <note priority="1" from="description">Quality select label</note>
</trans-unit> </trans-unit>
@@ -144,7 +140,7 @@
<source>Use URL</source> <source>Use URL</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
<note priority="1" from="description">YT search Use URL button for searched video</note> <note priority="1" from="description">YT search Use URL button for searched video</note>
</trans-unit> </trans-unit>
@@ -154,7 +150,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<note priority="1" from="description">YT search View button for searched video</note> <note priority="1" from="description">YT search View button for searched video</note>
</trans-unit> </trans-unit>
@@ -164,7 +160,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">62</context>
</context-group> </context-group>
<note priority="1" from="description">Only Audio checkbox</note> <note priority="1" from="description">Only Audio checkbox</note>
</trans-unit> </trans-unit>
@@ -174,7 +170,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">70</context> <context context-type="linenumber">67</context>
</context-group> </context-group>
<note priority="1" from="description">Multi-download Mode checkbox</note> <note priority="1" from="description">Multi-download Mode checkbox</note>
</trans-unit> </trans-unit>
@@ -184,7 +180,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">79</context> <context context-type="linenumber">76</context>
</context-group> </context-group>
<note priority="1" from="description">Main download button</note> <note priority="1" from="description">Main download button</note>
</trans-unit> </trans-unit>
@@ -194,7 +190,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">84</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
<note priority="1" from="description">Cancel download button</note> <note priority="1" from="description">Cancel download button</note>
</trans-unit> </trans-unit>
@@ -204,7 +200,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">96</context> <context context-type="linenumber">93</context>
</context-group> </context-group>
<note priority="1" from="description">Advanced download mode panel</note> <note priority="1" from="description">Advanced download mode panel</note>
</trans-unit> </trans-unit>
@@ -214,7 +210,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">102</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
<note priority="1" from="description">Simulated command label</note> <note priority="1" from="description">Simulated command label</note>
</trans-unit> </trans-unit>
@@ -224,7 +220,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">110</context> <context context-type="linenumber">107</context>
</context-group> </context-group>
<note priority="1" from="description">Use custom args checkbox</note> <note priority="1" from="description">Use custom args checkbox</note>
</trans-unit> </trans-unit>
@@ -232,12 +228,16 @@
<source>Custom args</source> <source>Custom args</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">145</context> <context context-type="linenumber">145</context>
</context-group> </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> <note priority="1" from="description">Custom args placeholder</note>
</trans-unit> </trans-unit>
<trans-unit id="a6911c2157f1b775284bbe9654ce5eb30cf45d7f" datatype="html"> <trans-unit id="a6911c2157f1b775284bbe9654ce5eb30cf45d7f" datatype="html">
@@ -246,7 +246,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">118</context> <context context-type="linenumber">115</context>
</context-group> </context-group>
<note priority="1" from="description">Custom Args input hint</note> <note priority="1" from="description">Custom Args input hint</note>
</trans-unit> </trans-unit>
@@ -256,7 +256,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">126</context> <context context-type="linenumber">123</context>
</context-group> </context-group>
<note priority="1" from="description">Use custom output checkbox</note> <note priority="1" from="description">Use custom output checkbox</note>
</trans-unit> </trans-unit>
@@ -264,7 +264,7 @@
<source>Custom output</source> <source>Custom output</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">131</context> <context context-type="linenumber">128</context>
</context-group> </context-group>
<note priority="1" from="description">Custom output placeholder</note> <note priority="1" from="description">Custom output placeholder</note>
</trans-unit> </trans-unit>
@@ -272,7 +272,11 @@
<source>Documentation</source> <source>Documentation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <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> </context-group>
<note priority="1" from="description">Youtube-dl output template documentation link</note> <note priority="1" from="description">Youtube-dl output template documentation link</note>
</trans-unit> </trans-unit>
@@ -280,7 +284,11 @@
<source>Path is relative to the config download path. Don&apos;t include extension.</source> <source>Path is relative to the config download path. Don&apos;t include extension.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <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> </context-group>
<note priority="1" from="description">Custom Output input hint</note> <note priority="1" from="description">Custom Output input hint</note>
</trans-unit> </trans-unit>
@@ -290,7 +298,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">140</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
<note priority="1" from="description">Use authentication checkbox</note> <note priority="1" from="description">Use authentication checkbox</note>
</trans-unit> </trans-unit>
@@ -298,7 +306,7 @@
<source>Username</source> <source>Username</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">145</context> <context context-type="linenumber">142</context>
</context-group> </context-group>
<note priority="1" from="description">YT Username placeholder</note> <note priority="1" from="description">YT Username placeholder</note>
</trans-unit> </trans-unit>
@@ -306,7 +314,7 @@
<source>Password</source> <source>Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html</context> <context context-type="sourcefile">app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html</context>
@@ -324,7 +332,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">194</context> <context context-type="linenumber">191</context>
</context-group> </context-group>
<note priority="1" from="description">Audio files title</note> <note priority="1" from="description">Audio files title</note>
</trans-unit> </trans-unit>
@@ -334,7 +342,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">199</context> <context context-type="linenumber">196</context>
</context-group> </context-group>
<note priority="1" from="description">Audio files description</note> <note priority="1" from="description">Audio files description</note>
</trans-unit> </trans-unit>
@@ -342,11 +350,11 @@
<source>Playlists</source> <source>Playlists</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/subscriptions/subscriptions.component.html</context> <context context-type="sourcefile">app/subscriptions/subscriptions.component.html</context>
@@ -360,7 +368,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">225</context> <context context-type="linenumber">222</context>
</context-group> </context-group>
<note priority="1" from="description">No video playlists available text</note> <note priority="1" from="description">No video playlists available text</note>
</trans-unit> </trans-unit>
@@ -370,7 +378,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">235</context> <context context-type="linenumber">232</context>
</context-group> </context-group>
<note priority="1" from="description">Video files title</note> <note priority="1" from="description">Video files title</note>
</trans-unit> </trans-unit>
@@ -380,7 +388,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">237</context>
</context-group> </context-group>
<note priority="1" from="description">Video files description</note> <note priority="1" from="description">Video files description</note>
</trans-unit> </trans-unit>
@@ -390,7 +398,7 @@
</source> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context> <context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">269</context> <context context-type="linenumber">266</context>
</context-group> </context-group>
<note priority="1" from="description">No video playlists available text</note> <note priority="1" from="description">No video playlists available text</note>
</trans-unit> </trans-unit>
@@ -456,6 +464,10 @@
<context context-type="sourcefile">app/dialogs/video-info-dialog/video-info-dialog.component.html</context> <context context-type="sourcefile">app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
</context-group> </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-group purpose="location">
<context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context> <context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">27</context> <context context-type="linenumber">27</context>
@@ -486,6 +498,14 @@
</context-group> </context-group>
<note priority="1" from="description">Close subscription info button</note> <note priority="1" from="description">Close subscription info button</note>
</trans-unit> </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"> <trans-unit id="ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1" datatype="html">
<source>ID:</source> <source>ID:</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -510,11 +530,31 @@
</context-group> </context-group>
<note priority="1" from="description">Playlist video count</note> <note priority="1" from="description">Playlist video count</note>
</trans-unit> </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"> <trans-unit id="321e4419a943044e674beb55b8039f42a9761ca5" datatype="html">
<source>Info</source> <source>Info</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/subscription/subscription-file-card/subscription-file-card.component.html</context> <context context-type="sourcefile">app/subscription/subscription-file-card/subscription-file-card.component.html</context>
@@ -522,22 +562,38 @@
</context-group> </context-group>
<note priority="1" from="description">Video info button</note> <note priority="1" from="description">Video info button</note>
</trans-unit> </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"> <trans-unit id="34504b488c24c27e68089be549f0eeae6ebaf30b" datatype="html">
<source>Delete and blacklist</source> <source>Delete and blacklist</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context> <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> </context-group>
<note priority="1" from="description">Delete and blacklist video button</note> <note priority="1" from="description">Delete and blacklist video button</note>
</trans-unit> </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"> <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
<source>Settings</source> <source>Settings</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -802,6 +858,14 @@
</context-group> </context-group>
<note priority="1" from="description">Custom args setting input hint</note> <note priority="1" from="description">Custom args setting input hint</note>
</trans-unit> </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"> <trans-unit id="0ba25ad86a240576c4f20a2fada4722ebba77b1e" datatype="html">
<source>Downloader</source> <source>Downloader</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -814,7 +878,7 @@
<source>Top title</source> <source>Top title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">166</context> <context context-type="linenumber">170</context>
</context-group> </context-group>
<note priority="1" from="description">Top title input placeholder</note> <note priority="1" from="description">Top title input placeholder</note>
</trans-unit> </trans-unit>
@@ -822,7 +886,7 @@
<source>File manager enabled</source> <source>File manager enabled</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">171</context> <context context-type="linenumber">175</context>
</context-group> </context-group>
<note priority="1" from="description">File manager enabled setting</note> <note priority="1" from="description">File manager enabled setting</note>
</trans-unit> </trans-unit>
@@ -830,7 +894,7 @@
<source>Downloads manager enabled</source> <source>Downloads manager enabled</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">174</context> <context context-type="linenumber">178</context>
</context-group> </context-group>
<note priority="1" from="description">Downloads manager enabled setting</note> <note priority="1" from="description">Downloads manager enabled setting</note>
</trans-unit> </trans-unit>
@@ -838,7 +902,7 @@
<source>Allow quality select</source> <source>Allow quality select</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">177</context> <context context-type="linenumber">181</context>
</context-group> </context-group>
<note priority="1" from="description">Allow quality seelct setting</note> <note priority="1" from="description">Allow quality seelct setting</note>
</trans-unit> </trans-unit>
@@ -846,7 +910,7 @@
<source>Download only mode</source> <source>Download only mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">184</context>
</context-group> </context-group>
<note priority="1" from="description">Download only mode setting</note> <note priority="1" from="description">Download only mode setting</note>
</trans-unit> </trans-unit>
@@ -854,7 +918,7 @@
<source>Allow multi-download mode</source> <source>Allow multi-download mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">187</context>
</context-group> </context-group>
<note priority="1" from="description">Allow multi-download mode setting</note> <note priority="1" from="description">Allow multi-download mode setting</note>
</trans-unit> </trans-unit>
@@ -862,7 +926,7 @@
<source>Require pin for settings</source> <source>Require pin for settings</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">186</context> <context context-type="linenumber">190</context>
</context-group> </context-group>
<note priority="1" from="description">Require pin for settings setting</note> <note priority="1" from="description">Require pin for settings setting</note>
</trans-unit> </trans-unit>
@@ -870,7 +934,7 @@
<source>Set New Pin</source> <source>Set New Pin</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">187</context> <context context-type="linenumber">191</context>
</context-group> </context-group>
<note priority="1" from="description">Set new pin button</note> <note priority="1" from="description">Set new pin button</note>
</trans-unit> </trans-unit>
@@ -878,7 +942,7 @@
<source>Enable Public API</source> <source>Enable Public API</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">195</context> <context context-type="linenumber">199</context>
</context-group> </context-group>
<note priority="1" from="description">Enable Public API key setting</note> <note priority="1" from="description">Enable Public API key setting</note>
</trans-unit> </trans-unit>
@@ -886,7 +950,7 @@
<source>Public API Key</source> <source>Public API Key</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">200</context> <context context-type="linenumber">204</context>
</context-group> </context-group>
<note priority="1" from="description">Public API Key setting placeholder</note> <note priority="1" from="description">Public API Key setting placeholder</note>
</trans-unit> </trans-unit>
@@ -894,7 +958,7 @@
<source>View documentation</source> <source>View documentation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">201</context> <context context-type="linenumber">205</context>
</context-group> </context-group>
<note priority="1" from="description">View API docs setting hint</note> <note priority="1" from="description">View API docs setting hint</note>
</trans-unit> </trans-unit>
@@ -902,7 +966,7 @@
<source>Generate</source> <source>Generate</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">205</context> <context context-type="linenumber">209</context>
</context-group> </context-group>
<note priority="1" from="description">Generate key button</note> <note priority="1" from="description">Generate key button</note>
</trans-unit> </trans-unit>
@@ -910,7 +974,7 @@
<source>Use YouTube API</source> <source>Use YouTube API</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">214</context> <context context-type="linenumber">218</context>
</context-group> </context-group>
<note priority="1" from="description">Use YouTube API setting</note> <note priority="1" from="description">Use YouTube API setting</note>
</trans-unit> </trans-unit>
@@ -918,7 +982,7 @@
<source>Youtube API Key</source> <source>Youtube API Key</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">218</context> <context context-type="linenumber">222</context>
</context-group> </context-group>
<note priority="1" from="description">Youtube API Key setting placeholder</note> <note priority="1" from="description">Youtube API Key setting placeholder</note>
</trans-unit> </trans-unit>
@@ -926,7 +990,7 @@
<source>Generating a key is easy!</source> <source>Generating a key is easy!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">219</context> <context context-type="linenumber">223</context>
</context-group> </context-group>
<note priority="1" from="description">Youtube API Key setting hint</note> <note priority="1" from="description">Youtube API Key setting hint</note>
</trans-unit> </trans-unit>
@@ -934,11 +998,11 @@
<source>Click here</source> <source>Click here</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context> <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> <source>to download the official YoutubeDL-Material Chrome extension manually.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <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>
<note priority="1" from="description">Chrome click here suffix</note> <note priority="1" from="description">Chrome click here suffix</note>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">234</context>
</context-group> </context-group>
<note priority="1" from="description">Chrome setup suffix</note> <note priority="1" from="description">Chrome setup suffix</note>
</trans-unit> </trans-unit>
@@ -966,7 +1030,7 @@
<source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source> <source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <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>
<note priority="1" from="description">Firefox click here suffix</note> <note priority="1" from="description">Firefox click here suffix</note>
</trans-unit> </trans-unit>
@@ -974,7 +1038,7 @@
<source>Detailed setup instructions.</source> <source>Detailed setup instructions.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">236</context> <context context-type="linenumber">240</context>
</context-group> </context-group>
<note priority="1" from="description">Firefox setup prefix link</note> <note priority="1" from="description">Firefox setup prefix link</note>
</trans-unit> </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> <source>Not much is required other than changing the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">236</context> <context context-type="linenumber">240</context>
</context-group> </context-group>
<note priority="1" from="description">Firefox setup suffix</note> <note priority="1" from="description">Firefox setup suffix</note>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">241</context> <context context-type="linenumber">245</context>
</context-group> </context-group>
<note priority="1" from="description">Bookmarklet instructions</note> <note priority="1" from="description">Bookmarklet instructions</note>
</trans-unit> </trans-unit>
@@ -998,7 +1062,7 @@
<source>Generate &apos;audio only&apos; bookmarklet</source> <source>Generate &apos;audio only&apos; bookmarklet</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">242</context> <context context-type="linenumber">246</context>
</context-group> </context-group>
<note priority="1" from="description">Generate audio only bookmarklet checkbox</note> <note priority="1" from="description">Generate audio only bookmarklet checkbox</note>
</trans-unit> </trans-unit>
@@ -1006,7 +1070,7 @@
<source>Extra</source> <source>Extra</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">160</context> <context context-type="linenumber">164</context>
</context-group> </context-group>
<note priority="1" from="description">Extra settings label</note> <note priority="1" from="description">Extra settings label</note>
</trans-unit> </trans-unit>
@@ -1014,7 +1078,7 @@
<source>Use default downloading agent</source> <source>Use default downloading agent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">256</context> <context context-type="linenumber">260</context>
</context-group> </context-group>
<note priority="1" from="description">Use default downloading agent setting</note> <note priority="1" from="description">Use default downloading agent setting</note>
</trans-unit> </trans-unit>
@@ -1022,27 +1086,47 @@
<source>Select a downloader</source> <source>Select a downloader</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">260</context> <context context-type="linenumber">264</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">274</context>
</context-group> </context-group>
<note priority="1" from="description">Custom downloader select label</note> <note priority="1" from="description">Custom downloader select label</note>
</trans-unit> </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"> <trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html">
<source>Allow advanced download</source> <source>Allow advanced download</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">285</context> <context context-type="linenumber">289</context>
</context-group> </context-group>
<note priority="1" from="description">Allow advanced downloading setting</note> <note priority="1" from="description">Allow advanced downloading setting</note>
</trans-unit> </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"> <trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
<source>Advanced</source> <source>Advanced</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">251</context> <context context-type="linenumber">255</context>
</context-group> </context-group>
<note priority="1" from="description">Host settings label</note> <note priority="1" from="description">Host settings label</note>
</trans-unit> </trans-unit>
@@ -1050,7 +1134,7 @@
<source>Allow user registration</source> <source>Allow user registration</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">297</context> <context context-type="linenumber">310</context>
</context-group> </context-group>
<note priority="1" from="description">Allow registration setting</note> <note priority="1" from="description">Allow registration setting</note>
</trans-unit> </trans-unit>
@@ -1058,15 +1142,23 @@
<source>Users</source> <source>Users</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">295</context> <context context-type="linenumber">308</context>
</context-group> </context-group>
<note priority="1" from="description">Users settings label</note> <note priority="1" from="description">Users settings label</note>
</trans-unit> </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"> <trans-unit id="52c9a103b812f258bcddc3d90a6e3f46871d25fe" datatype="html">
<source>Save</source> <source>Save</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">307</context> <context context-type="linenumber">327</context>
</context-group> </context-group>
<note priority="1" from="description">Settings save button</note> <note priority="1" from="description">Settings save button</note>
</trans-unit> </trans-unit>
@@ -1074,7 +1166,7 @@
<source>{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }</source> <source>{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context> <context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">310</context> <context context-type="linenumber">330</context>
</context-group> </context-group>
<note priority="1" from="description">Settings cancel and close button</note> <note priority="1" from="description">Settings cancel and close button</note>
</trans-unit> </trans-unit>
@@ -1188,6 +1280,10 @@
<context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context> <context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </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-group purpose="location">
<context context-type="sourcefile">app/components/login/login.component.html</context> <context context-type="sourcefile">app/components/login/login.component.html</context>
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
@@ -1254,7 +1350,7 @@
<source>Subscriptions</source> <source>Subscriptions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context> <context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">45</context>
</context-group> </context-group>
<note priority="1" from="description">Navigation menu Subscriptions Page title</note> <note priority="1" from="description">Navigation menu Subscriptions Page title</note>
</trans-unit> </trans-unit>
@@ -1262,7 +1358,7 @@
<source>Downloads</source> <source>Downloads</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context> <context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">46</context>
</context-group> </context-group>
<note priority="1" from="description">Navigation menu Downloads Page title</note> <note priority="1" from="description">Navigation menu Downloads Page title</note>
</trans-unit> </trans-unit>
@@ -1320,6 +1416,10 @@
<context context-type="sourcefile">app/dialogs/share-media-dialog/share-media-dialog.component.html</context> <context context-type="sourcefile">app/dialogs/share-media-dialog/share-media-dialog.component.html</context>
<context context-type="linenumber">24</context> <context context-type="linenumber">24</context>
</context-group> </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> <note priority="1" from="description">Copy to clipboard button</note>
</trans-unit> </trans-unit>
<trans-unit id="5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f" datatype="html"> <trans-unit id="5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f" datatype="html">
@@ -1390,23 +1490,15 @@
<source>Custom name</source> <source>Custom name</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context> <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> </context-group>
<note priority="1" from="description">Subscription custom name placeholder</note> <note priority="1" from="description">Subscription custom name placeholder</note>
</trans-unit> </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"> <trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html">
<source>Download all uploads</source> <source>Download all uploads</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context> <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> </context-group>
<note priority="1" from="description">Download all uploads subscription setting</note> <note priority="1" from="description">Download all uploads subscription setting</note>
</trans-unit> </trans-unit>
@@ -1414,23 +1506,47 @@
<source>Download videos uploaded in the last</source> <source>Download videos uploaded in the last</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context> <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> </context-group>
<note priority="1" from="description">Download time range prefix</note> <note priority="1" from="description">Download time range prefix</note>
</trans-unit> </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"> <trans-unit id="408ca4911457e84a348cecf214f02c69289aa8f1" datatype="html">
<source>Streaming-only mode</source> <source>Streaming-only mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context> <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> </context-group>
<note priority="1" from="description">Streaming-only mode</note> <note priority="1" from="description">Streaming-only mode</note>
</trans-unit> </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"> <trans-unit id="d0336848b0c375a1c25ba369b3481ee383217a4f" datatype="html">
<source>Subscribe</source> <source>Subscribe</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context> <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> </context-group>
<note priority="1" from="description">Subscribe button</note> <note priority="1" from="description">Subscribe button</note>
</trans-unit> </trans-unit>
@@ -1594,7 +1710,7 @@
<source>No downloads available!</source> <source>No downloads available!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">app/components/downloads/downloads.component.html</context> <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> </context-group>
<note priority="1" from="description">No downloads label</note> <note priority="1" from="description">No downloads label</note>
</trans-unit> </trans-unit>
@@ -1726,6 +1842,22 @@
</context-group> </context-group>
<note priority="1" from="description">Edit role</note> <note priority="1" from="description">Edit role</note>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -196,5 +196,20 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ", "52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ", "59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios", "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:"
} }