Compare commits

...

399 Commits
v3.3 ... v4.1

Author SHA1 Message Date
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
Isaac Grynsztein
80f214fdde Fixed bug that caused non-YT videos to be downloading using the best format 2020-05-27 00:12:17 -04:00
Isaac Grynsztein
57a9434b3c File cards now include the video's real ID (for YT videos). Otherwise the file name will be used as a fallback 2020-05-25 16:51:49 -04:00
Isaac Grynsztein
cec0ed78ec Fixed bug that prevented user registration from occuring, and added new "Login" option to the hamburger menu when appropriate
Related: made it so non-logged in users (in multi user mode) don't have the option to go "home" or "subscriptions" or "downloads". It would error regardless, but it looks cleaner now
2020-05-25 16:35:02 -04:00
Isaac Grynsztein
c8a8046056 Fixed bug where if a subscription name was missing, the wrong folder was used
Fixed bug that caused high CPU usage when the subscriptions check took longer than the interval in the settings (thus they piled on)
2020-05-25 15:35:49 -04:00
Tzahi12345
73cd142b77 Cookies now work in subscriptions, too 2020-05-19 21:52:45 -04:00
Tzahi12345
b071fb9e2e Merge pull request #94 from Tzahi12345/cookies-helper
Cookies upload dialog
2020-05-19 21:43:39 -04:00
Tzahi12345
f485da06b5 Implemented cookies upload dialog and the ability to "enable cookies" to hopefully circumvent 429 errors 2020-05-19 21:37:19 -04:00
Tzahi12345
59098d4693 Minor update to support alternate youtube links 2020-05-19 19:31:41 -04:00
Tzahi12345
4cf92b8f3d Fixed bugged override 2020-05-19 18:15:07 -04:00
Tzahi12345
e07acfd4b3 Added support for a popular adult website 2020-05-18 20:45:36 -04:00
Tzahi12345
05d962328b Fixed bug that prevented non-youtube videos from downloading 2020-05-18 19:32:00 -04:00
Tzahi12345
0816cb7046 Fixed bug that preventing reddit videos from downloading 2020-05-18 18:44:39 -04:00
Tzahi12345
c6553d99c6 Hotfix for bug that prevented large twitch videos from downloading 2020-05-10 05:20:53 -04:00
Tzahi12345
8bf3680b6f Fixed bug that prevented soundcloud audio files from downloading correctly 2020-05-10 04:56:21 -04:00
Tzahi12345
9e5ad66a9d Added scaffolding for custom paths in subscriptions 2020-05-10 04:53:49 -04:00
Tzahi12345
3487813cb5 Updated frontend files 2020-05-09 03:41:27 -04:00
Tzahi12345
ecc2737a05 Pressing enter on URL input now triggers the download 2020-05-09 03:34:59 -04:00
Tzahi12345
39e737024f Pressing enter on url input now triggers download 2020-05-09 03:03:37 -04:00
Tzahi12345
550013a2e7 Config file is now created when missing and set with default values 2020-05-08 14:28:19 -04:00
Tzahi12345
b6f8551cfa Updated docker-compose.yml to support multi-user mode 2020-05-06 02:49:18 -04:00
Tzahi12345
98e94d3c38 Updated frontend binaries 2020-05-06 02:46:44 -04:00
Tzahi12345
8c94255f61 Updated version number to 4.0 2020-05-06 02:43:46 -04:00
Tzahi12345
409fd0fe20 Updated translations and frontend binaries 2020-05-06 02:39:20 -04:00
Tzahi12345
d4ad1f9fce Added additional sentence to custom args hint and fixed issue where empty args could be added through the args modifier 2020-05-06 02:38:39 -04:00
Tzahi12345
cc47823b0c Updated gitignore and re-added package-lock.json
Translations updated
2020-05-05 20:12:15 -04:00
Tzahi12345
747735dffe Arg modifier chip list now supports auto complete and arg description as the chip tooltip
Fixed bug that caused custom args to reset after exiting arg modifier without hitting cancel
2020-05-05 20:11:23 -04:00
Tzahi12345
76b63329ca Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-05-05 19:32:08 -04:00
Tzahi12345
f094d18e03 Changed /api/changeUser endpoint to /api/updateUser 2020-05-05 19:31:47 -04:00
Tzahi12345
c1b0b583e4 Updated API to reflect changes for 4.0 2020-05-05 19:29:04 -04:00
Tzahi12345
a1c9c97616 Updated security schema in repository api docs 2020-05-05 16:47:28 -04:00
Tzahi12345
0504167734 Arg modifier improvements: args are now shown as removable chips which can be directly typed as well (w/o using the adder) 2020-05-04 15:39:33 -04:00
Tzahi12345
49081db8cb Config items are now checked on start. Missing ones will be autofilled with the default values automatically on startup 2020-05-04 05:18:06 -04:00
Tzahi12345
98e33ac399 Updated frontend files 2020-05-04 04:38:21 -04:00
Tzahi12345
a3424f973e Output on global args will now override specific output 2020-05-03 21:14:09 -04:00
Tzahi12345
8e5db3e9d1 Custom args and global custom args now use double comma as a delimiter. This should allow file names with spaces when using custom args (global and not) 2020-05-03 21:05:56 -04:00
Tzahi12345
1861011fb0 Cleaned up code and added missing translation units 2020-05-03 19:35:38 -04:00
Tzahi12345
74e47b7d04 Fixed bug that prevented audio files from being played after username change
Downloads with custom args or custom quality config now use the old downloader to ensure stability and prevent arg conflict
2020-05-03 19:34:01 -04:00
Tzahi12345
68f791eea3 Downloads now show new spinner after download completes, indicating processing has begun 2020-05-03 18:56:07 -04:00
Tzahi12345
f73ec2dd94 Fixed bug that caused users with large amounts of data to have extremely large tokens
Subfolders are now ensured to exist with the normal downloading method

Initialization now happens after token retrieval to avoid failed requests

Fixed bug that caused login to be called twice, introducing a possible race condition
2020-05-03 18:55:42 -04:00
Tzahi12345
26ad195597 Adds ability to set umask through an environment variable. Does not work on Windows, and it's untested on Linux 2020-05-03 13:40:54 -04:00
Tzahi12345
fb23d7c41e Audio downloads now work with progress bar, but it requires file conversion at the end. It ends up being around the same speed as the regular method 2020-05-03 03:24:25 -04:00
Tzahi12345
4e6d68d9e6 Updated video playing/sharing logic to support sharing of playlists in multi user mode and when multi user mode is disabled
Fixed bug that caused normal archive to be used in multi-user mode

Updated login logic when username is not found or user file is missing

Fixed bug that prevented playlist sharing from working

Added ability to use timestamps when sharing videos
2020-05-02 20:36:30 -04:00
Tzahi12345
8bc99fb557 Fixed bug that prevented registration from occuring 2020-05-02 17:25:35 -04:00
Tzahi12345
8277c95c4e Updated youtube-dl binary for windows 2020-05-02 17:10:08 -04:00
Tzahi12345
e5db376914 All config values are now reloaded on config set
Added 4 new settings: user files folder, enable registration, enable downloads manager, and logging level selection
2020-05-02 17:09:46 -04:00
Tzahi12345
661b96cfe5 Fixed bug that prevented default config items to be set 2020-05-02 15:06:43 -04:00
Tzahi12345
2eef1b062c Updated frontend binaries 2020-05-01 16:26:14 -04:00
Tzahi12345
d376ee4409 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-05-01 16:18:47 -04:00
Tzahi12345
7661b1e79e Fixed bug that prevented admin creation prompt from occuring in the settings 2020-05-01 16:18:39 -04:00
Tzahi12345
0401769968 Merge pull request #69 from SuperSandro2000/patch-1
Update badge links
2020-05-01 14:54:26 -04:00
Tzahi12345
c901efebc4 Merge pull request #68 from SuperSandro2000/docker
Docker improvements
2020-05-01 14:49:32 -04:00
Sandro
4e550da4f6 Update badge links 2020-05-01 20:41:01 +02:00
Sandro Jäckel
ae76e9db8d Install dependencies first, remove duplicated workdir 2020-05-01 19:45:19 +02:00
Sandro Jäckel
d763f88ceb Remove comments 2020-05-01 19:40:35 +02:00
Sandro Jäckel
a8b188cd22 Don't create cache with apk, use cdn network 2020-05-01 19:40:02 +02:00
Sandro Jäckel
1034aa1980 Don't copy Docker related files into image 2020-05-01 19:39:34 +02:00
Tzahi12345
da26d88ba9 Updated frontend binaries 2020-05-01 03:55:17 -04:00
Tzahi12345
93e117a7aa Adds fingerprintjs2 as frontend dependency 2020-05-01 03:52:31 -04:00
Tzahi12345
ae9c52a14d Merge pull request #67 from Tzahi12345/multi-user-mode
Adds multi-user mode
2020-05-01 03:48:50 -04:00
Tzahi12345
b685b955df Added roles and permissions system, as well as the ability to modify users and their roles
Downloads manager now uses device fingerprint as identifier rather than a randomly generated sessionID
2020-05-01 03:34:35 -04:00
Tzahi12345
e7b841c056 Added UI flow for creating default admin account. Dialog will show up after enabling or in the login menu if the admin account isn't present 2020-04-30 16:31:36 -04:00
Tzahi12345
e5f9694da0 Fixed bug where downloading individual files failed for channel subscriptions 2020-04-30 13:29:47 -04:00
Tzahi12345
81b0ef4a72 Refactored initialization process to better facilitate auth if necessary
Date in user profile dialog now shows date
2020-04-30 13:28:58 -04:00
Tzahi12345
31f581c642 Subscriptions now support multi-user-mode
Fixed bug where playlist subscription downloads would fail due to a mislabeled parameter

Components that are routes now make sure auth is finished before sending requests to the backend
2020-04-30 04:54:41 -04:00
Tzahi12345
d2af233a1f Fixed bug that when multi-download mode was enabled, videos could not be navigated to 2020-04-29 23:09:00 -04:00
Adam Verga
0fb00bac12 Initialization on auth component happens in a separate function, users_db primarily sits in app.js
Fixed bug where current download would set to null, but maincomponent still tried to parse it
2020-04-29 20:46:29 -04:00
Adam Verga
6980828853 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into multi-user-mode 2020-04-29 20:16:42 -04:00
Adam Verga
a48e122763 Settings are now more centralized in the frontend 2020-04-29 20:15:15 -04:00
Tzahi12345
2d66d653f6 Updated frontend files to reflect commits since v3.6 2020-04-29 00:52:45 -04:00
Tzahi12345
03ea04f8d8 Fixed missing double ampersand 2020-04-29 00:40:14 -04:00
Tzahi12345
8fbb1c9bbd Fixed repo for atomicparsley on docker/alpine 2020-04-29 00:35:51 -04:00
Tzahi12345
c67d6ea89a Added atomicparsley as a dependency to Docker, and listed it as an optional dependency for normal installs 2020-04-29 00:27:39 -04:00
Tzahi12345
a701d0fe83 Fixes bug (hopefully) that causes stale data to be saved to the db due to multiple adapters instances being used. Now the db adapter gets passed as a parameter 2020-04-29 00:12:50 -04:00
Tzahi12345
ff51a49d1b Removed unused import 2020-04-29 00:04:04 -04:00
Tzahi12345
46b595db45 Update README.md
Updated API info
2020-04-27 04:55:46 -04:00
Isaac Grynsztein
4b2b278439 Sharing and video downloads on shared videos now work for multi-user mode 2020-04-27 04:31:39 -04:00
Isaac Grynsztein
1ac6683f33 Custom quality configurations now use the old downloading method to avoid errors
postsservice now does jwt auth after checking if multi user mode is enabled

Minor update to user profile UI

Added setting for enabling and disabling multi user mode
2020-04-26 21:37:08 -04:00
Isaac Grynsztein
e790c9fadf File descriptors are now stored in the config_api until they find a better home
File deletion now works in multi-user mode. Sharing and subscriptions are the last holdouts for porting over to multi-user-mode

Fixed bug with archive mode that defaulted to storing the ID in the video archive all the time (rather than audio if it's an mp3)
2020-04-26 18:33:23 -04:00
Isaac Grynsztein
c18bf568c7 Fixed bug that prevented video download or archive download from occuring 2020-04-26 17:41:54 -04:00
Isaac Grynsztein
fa1b291f97 Added video downloading functionality to multi user mode, as well as playlist management and saving of videos locally. Still missing video deletions and subscriptions
Simplified code for downloading videos to client (locally)
2020-04-26 17:40:28 -04:00
Isaac Grynsztein
cb6451ef96 Added new settings: multi user mode and users base path 2020-04-26 17:37:49 -04:00
Isaac Grynsztein
912a419bd4 Getting current download refactored to work and display less errors
Player component now sends jwt token if logged in
2020-04-26 17:34:38 -04:00
Isaac Grynsztein
a7c810136b Added basic user profile component 2020-04-26 17:33:29 -04:00
Isaac Grynsztein
e6ea2238f8 Fixed bug where HTTP headers were sent when params should have been sent instead
sessionID now gets sent after logging in
2020-04-26 17:32:50 -04:00
Isaac Grynsztein
98f1d003c3 Fixed bug that prevented migrations from succeeding
Added scaffolding required for jwt authentication for certain routes

Added logger to auth_api

Added necessary routing rules for multi-user mode

Registration is now possible
2020-04-24 21:03:00 -04:00
Isaac Grynsztein
c3cc28540f Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into multi-user-mode 2020-04-23 14:56:14 -04:00
Tzahi12345
68ed66072e Merge pull request #57 from Tzahi12345/current-downloads-refactor
Downloads system refactor
2020-04-22 23:15:26 -04:00
Isaac Grynsztein
eca06a7fb1 Downloads on the home page now show the progress bar 2020-04-22 21:42:21 -04:00
Isaac Grynsztein
b583305940 Downloads in the download manager now get updated smoothly, preventing the DOM from updating on object reassign 2020-04-21 20:10:53 -04:00
Isaac Grynsztein
f361b8a974 Furrther simplified download process and fixed a couple bugs
Audio files will not show download progress as enabling this feature causes it to be really slow

Fixed bug where downloading the same video twice produced duplicate files in the file manager
2020-04-21 18:56:52 -04:00
Isaac Grynsztein
1565c328d5 If a video is a playlist, it will download the normal way 2020-04-21 16:19:19 -04:00
Isaac Grynsztein
a6534f66a6 migrated audio file downloads to new system. still untested with playlists
video/audio player now doesnt show share button when uid isn't present, user will be notified of this through a snackbar as well
2020-04-21 03:16:39 -04:00
Isaac Grynsztein
a78ccefc83 Updated package.json 2020-04-20 18:40:21 -04:00
Isaac Grynsztein
6fe7d20498 downloads refactor half done - videos are now implement, but audo files are now
Added downloads manager in the UI where downloads can be viewed/cleared
2020-04-20 18:39:55 -04:00
Isaac Grynsztein
d887380fd1 Added new methods to facilitate server-side download management 2020-04-18 01:31:32 -04:00
Isaac Grynsztein
1f3572a630 jwt auth scaffolding
logging in now works

UI login component created
2020-04-16 22:35:34 -04:00
Isaac Grynsztein
da8571fb1a Added additional info when requests are rejected due to no auth
Added two additional auth methods: registering and logging in. They have minimal functionality right now

Added auth module which will handle all auth-related requests
2020-04-16 16:33:32 -04:00
Isaac Grynsztein
4617362270 New default youtube-dl.exe binary
Updated public dir in backend
2020-04-15 18:52:18 -04:00
Isaac Grynsztein
bdb5072014 API key is now passed as a query param 2020-04-15 18:46:13 -04:00
Isaac Grynsztein
e5baf094c9 chmodsync will not run if app is running on windows 2020-04-15 03:40:26 -04:00
Isaac Grynsztein
264b3606d6 Modified automatic permissions for json files 2020-04-15 03:36:56 -04:00
Isaac Grynsztein
2408184cc7 new video json files created now get read perms across the board 2020-04-15 03:19:28 -04:00
Isaac Grynsztein
e4851253dd Docker now ignores executable files 2020-04-15 02:15:15 -04:00
Isaac Grynsztein
87696f71f8 Added subscription folders to repo
Added .dockerignore to ignore node_modules

Removed unnecessary whitespace from docker-compose.yml
2020-04-15 02:10:12 -04:00
Isaac Grynsztein
d6fe2a5720 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-04-15 02:02:27 -04:00
Isaac Grynsztein
90c2d3f70b Moved docker files to backend 2020-04-15 02:02:14 -04:00
Isaac Grynsztein
0d54cb9872 Updated player view to prevent video from being too large 2020-04-15 02:02:03 -04:00
Isaac Grynsztein
a8d6298cfd Adds preliminary support for tiktok and periscope
Added alternate json path for mp4s in case it's not found in the main location
2020-04-15 02:01:25 -04:00
Tzahi12345
65b31633d9 Update README.md
Changed start command to use npm
2020-04-14 15:41:18 -04:00
Isaac Grynsztein
3db3077dec Fixed misspelling in docs 2020-04-13 16:01:37 -04:00
Tzahi12345
61633e817b Updated API docs 2020-04-13 15:59:46 -04:00
Tzahi12345
6cc93ba4f9 Update README.md
Added badges to README, additional links to reverse proxy info, and updated the API section.
2020-04-13 02:16:35 -04:00
Isaac Grynsztein
9b8b92b8df Added latest release to repo 2020-04-12 23:54:16 -04:00
Isaac Grynsztein
d9f6b8c64c Simplified archive creation for subscriptions to reduce risk of error
If no subscriptions have ever been made, "No channel/playlist subscriptions" text will now show
2020-04-12 21:00:47 -04:00
Isaac Grynsztein
10b59191f6 Updated public directory 2020-04-12 20:47:29 -04:00
Isaac Grynsztein
a89787698b Fixed version in package.json 2020-04-12 20:42:47 -04:00
Isaac Grynsztein
3d3ab5180f fixed bug that prevented non-api routes from loading without an auth header 2020-04-10 21:03:37 -04:00
Isaac Grynsztein
eddc25566d Updated behavior of file card deletion to prevent compilation error 2020-04-10 20:49:34 -04:00
Isaac Grynsztein
b5a82b9385 Updated middleware to support API tokens. Frontend now uses an admin token for its requests
Fixed version numbers
2020-04-10 20:44:42 -04:00
Isaac Grynsztein
2082a78846 Updated version number 2020-04-10 20:41:14 -04:00
Isaac Grynsztein
fe170a4de8 Updated public API link 2020-04-10 15:25:23 -04:00
Tzahi12345
18dab72b51 Updated public API 2020-04-10 15:24:26 -04:00
Isaac Grynsztein
6849bd00d5 Adding public API docs 2020-04-10 15:13:07 -04:00
Isaac Grynsztein
1e96e31053 Added new API key and using API key config items to enable a public API
API key config items are implemented UI-side

Added ability to generate API keys through the settings

Switched getmp3s and getmp4s api calls to be GET requests rather than POST

Removed unused code from settings dialog
2020-04-10 14:46:36 -04:00
Isaac Grynsztein
02441ac846 Fixed bug where docker would start building on certain systems when using docker-compose up, and refuse to pull with docker-compose pull 2020-04-10 01:33:36 -04:00
Tzahi12345
4666aa15b3 Merge pull request #49 from Tzahi12345/streaming-only-mode
Streaming-only mode
2020-04-09 23:41:44 -04:00
Isaac Grynsztein
f6f54c0e53 Fixed bug where video infos caused an error for streaming-only subscriptions 2020-04-09 23:41:21 -04:00
Isaac Grynsztein
e15141c5e0 Added backend and database support for video streaming
Added UI support for video streaming. branch is now feature-complete
2020-04-09 23:33:58 -04:00
Isaac Grynsztein
a61606b69f Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into streaming-only-mode 2020-04-09 22:29:56 -04:00
Isaac Grynsztein
d96fab49f5 added ability to use url in player component. streaming mode will need this 2020-04-09 22:29:23 -04:00
Isaac Grynsztein
346d41d3e1 Fixed bug that broke server backups 2020-04-09 01:57:43 -04:00
Isaac Grynsztein
597e1f5b60 Fixed bug that prevented updates from succeeding 2020-04-09 01:47:10 -04:00
Isaac Grynsztein
dd0f58d421 Fixed bug where updater kept asking for updates on the update after it completed 2020-04-09 01:36:46 -04:00
Isaac Grynsztein
2ca6aa7bd7 Video URL now wraps in video info dialog 2020-04-09 01:36:02 -04:00
Isaac Grynsztein
ba2b837cc5 Fixed accidentally commented out functionality to download new updates 2020-04-09 01:35:22 -04:00
Isaac Grynsztein
22f0ee834b backups now occur in appdata/backups folder 2020-04-08 15:58:29 -04:00
Isaac Grynsztein
cb02227302 fixed bug where recently saved playlists could not be shared 2020-04-07 22:08:18 -04:00
Isaac Grynsztein
1b4f2830f5 changed logging in backend to support extra logging in debug mode 2020-04-07 21:41:13 -04:00
Isaac Grynsztein
720fceefb6 Fixed bug where subscription videos could not be downloaded from the player 2020-04-07 19:16:15 -04:00
Isaac Grynsztein
fa488015c3 Minor import change 2020-04-07 18:43:27 -04:00
Tzahi12345
88f1b3daff Merge pull request #48 from Tzahi12345/video-sharing
Video sharing
2020-04-07 18:29:29 -04:00
Isaac Grynsztein
0ea8a11c85 Subscriptions now don't have a share button (it's broken anyways) 2020-04-07 18:26:47 -04:00
Isaac Grynsztein
14bf2248cf Added UI support for sharing videos 2020-04-07 18:19:12 -04:00
Isaac Grynsztein
822aec4de8 added API endpoint to get file from database
video/audio files can now be retrieved by just uid, allowing for easy sharing

added API endpoints for sharing/unsharing a video (no UI support yet)
2020-04-07 14:49:05 -04:00
Isaac Grynsztein
2a1aa4036c New checkbox to select streaming only mode when subscribing 2020-04-07 01:52:59 -04:00
Isaac Grynsztein
2414e16021 videos now deleted by UID ui-side 2020-04-07 01:52:22 -04:00
Isaac Grynsztein
69cd22d992 file deletions now remove the file from the db as well 2020-04-07 01:38:35 -04:00
Isaac Grynsztein
1905129201 getMp3s and getMp4s now have dedicated functions
downloaded files now get recorded in db.json. So when the server wants to get audio/video files, it doesn't need to recursively go through the respective folders each time
- getMp4s/getMp3s API request latency is reduced ~2x (130ms -> 60ms) in testing

Modified tomp3/tomp4 code to automatically add newly downloaded files to the db

Added a migration so users on 3.5 or below will get their files automatically added to the db on the first run

All these changes are necessary to enable easy sharing with features like timestamps
2020-04-07 00:00:25 -04:00
Isaac Grynsztein
7ef6c78612 merged new checkbox for bookmarklet: enables ability to set bookmarklet to audio only
fixed two bugs for audio only files: sometimes downloads failed as extensions were improperly removed and readded, removing a single character from the filename.

Fixed another extension-related bug where metadata from deleted audio files persisted
2020-04-05 15:19:12 -04:00
Tzahi12345
1d9595d056 Merge pull request #47 from Tzahi12345/better-settings-menu
Better settings menu
2020-04-04 23:35:13 -04:00
Isaac Grynsztein
d258bc2218 Updated appearance of settings menu to improve organization and performance 2020-04-04 23:29:42 -04:00
Isaac Grynsztein
4d3a687d34 Fixed bug where toggling dark mode using the toggle rather than the adjacent menu item caused visual errors 2020-04-04 23:28:34 -04:00
Tzahi12345
2b91293abd Merge pull request #45 from Tzahi12345/better-logging
Better logging system using winston
2020-04-02 23:16:31 -04:00
Isaac Grynsztein
3990e25c18 added logging to config api and subscriptions api, meaning the entire backend has the new logging system 2020-04-02 23:14:07 -04:00
Isaac Grynsztein
2f0bbca15c added better logging in app.js using winston 2020-04-02 23:05:17 -04:00
Tzahi12345
717f6deb11 Merge pull request #44 from Tzahi12345/auto-update-youtubedl-material
Ability to update YoutubeDL-Material
2020-04-02 21:56:44 -04:00
Isaac Grynsztein
c36867d368 Added progress bar to file downloads
Added two new API calls, to update the server to a particular version and to get the updater status

You can now update through the UI, and a status dialog displays after
2020-04-02 21:53:08 -04:00
Isaac Grynsztein
458e4b45f8 Removed @locl dependency for translations
Added CommonModule to fix intellisense

Added ability to load json assets by name, and an http call to update youtubedl-material
2020-04-01 19:44:22 -04:00
Isaac Grynsztein
c40513ba4a docker-compose now uses latest version tag 2020-04-01 19:42:44 -04:00
Isaac Grynsztein
6fa52cecbc Updated docker compose version 2020-04-01 19:42:21 -04:00
Isaac Grynsztein
a5474141bb Removed unused dependencies 2020-04-01 19:36:57 -04:00
Isaac Grynsztein
89ececdbeb Dependencies now install during update 2020-04-01 19:31:41 -04:00
Isaac Grynsztein
58718b6e3b Removed @ngular/http dependency 2020-04-01 01:11:26 -04:00
Isaac Grynsztein
a5224f80a8 nodemon now runs silently 2020-04-01 01:07:08 -04:00
Isaac Grynsztein
c2ee6b6230 update package.json version 2020-04-01 01:06:55 -04:00
Isaac Grynsztein
37614a1611 Changed backend logging for server start to give more information (namely version) 2020-03-31 04:20:46 -04:00
Isaac Grynsztein
b71bdfcec2 Updated nodemon package.json config 2020-03-31 04:19:42 -04:00
Isaac Grynsztein
1b09bf4881 nodemon now supported 2020-03-31 01:56:15 -04:00
Tzahi12345
82df232f03 Update process now properly gets required backend files 2020-03-30 23:36:35 -04:00
Tzahi12345
af4de44016 Further merge 2020-03-30 23:25:58 -04:00
Tzahi12345
61f27d6fe9 merged changes 2020-03-30 23:25:48 -04:00
Tzahi12345
b3dbdd1790 Cleaning working folder 2020-03-30 23:24:33 -04:00
Isaac Grynsztein
785306c59a Added debug statements 2020-03-30 23:20:52 -04:00
Tzahi12345
38774d8593 Updater now grab new backend files
youtube-dl auto updater now guesses binary path, which makes the update process work much more reliably
2020-03-30 23:15:31 -04:00
Isaac Grynsztein
df11aca1e0 Added preliminary support for updating YoutubeDL-Material
Config items that are not found use and set the default value

Fixed potential error while updated youtube-dl binaries
2020-03-30 18:35:44 -04:00
Isaac Grynsztein
bcff631936 Updated translations and 'backend/public' folder 2020-03-29 10:46:35 -04:00
Isaac Grynsztein
347df89aa7 Updated default title_top value in config 2020-03-29 10:45:31 -04:00
Isaac Grynsztein
eb084d03b2 Downloaded file names are now converted to their fully decoded forms 2020-03-28 08:04:45 -04:00
Isaac Grynsztein
8c942b0343 Updated top bar color 2020-03-27 20:02:18 -04:00
Isaac Grynsztein
baad97182a Updated version tag 2020-03-27 20:02:06 -04:00
Isaac Grynsztein
26ca5d51a5 Fixed about dialog translations 2020-03-27 16:59:50 -04:00
Isaac Grynsztein
0c5cd291fe Changed comment for clarity 2020-03-27 15:57:51 -04:00
Isaac Grynsztein
57234b4690 File card download progress bars now maintain same width as file cards, appear rounded at the bottom, and are positioned right at the bottom of each card 2020-03-27 15:56:34 -04:00
Isaac Grynsztein
b993c8e1d6 Fixed bug were downloadOnlyMode failed to work when multiDownloadMode was enabled 2020-03-27 15:54:14 -04:00
Isaac Grynsztein
66cb0af762 Settings dialog now says "cancel" when settings are changed and "close" otherwise 2020-03-27 13:56:55 -04:00
Isaac Grynsztein
8331c319ce File formats are not searched anymore if video is a playlist. Prior to this they simply errored 2020-03-27 13:55:50 -04:00
Isaac Grynsztein
da9dcc0249 Added border radius to progress bar to make it look less "blocky" 2020-03-27 13:55:21 -04:00
Isaac Grynsztein
4b9dc4a950 Updated gitignore 2020-03-27 13:55:01 -04:00
Isaac Grynsztein
5af0d742ef Fixed bug where updating an audio playlist would cause it to believe it was a video playlist 2020-03-26 13:28:26 -04:00
Isaac Grynsztein
ca3a42c075 Changed location of archive path to appdata/archives. If the folder doesn't exist, it gets auto-generated. In the future this path will be configurable 2020-03-26 10:52:43 -04:00
Isaac Grynsztein
4906e52c57 Adjusted styling for advanced download bar to make it look more natural and part of the page 2020-03-26 10:48:46 -04:00
Isaac Grynsztein
d33346b11d Final fix for bug where failed downloads still appeared in the multi download menu 2020-03-26 10:47:56 -04:00
Isaac Grynsztein
b07f16bdd0 Added subtle drop shadow to topBarTitle 2020-03-25 23:28:14 -04:00
Isaac Grynsztein
a5c47737c7 Fixed bug where simulated output did not include the base path 2020-03-25 23:27:51 -04:00
Isaac Grynsztein
603c13eb4c Modified about dialog to have a more consistent design, and added the logo & github logo to the top 2020-03-25 17:07:23 -04:00
Isaac Grynsztein
a35d85d7df Added elevation to top toolbar and made it "sticky". That means when users scroll, the toolbar will scroll with 2020-03-25 17:05:50 -04:00
Isaac Grynsztein
25b5b28df8 Minor visual change to settings 2020-03-25 07:40:14 -04:00
Isaac Grynsztein
0c60c12124 Append to last commit 2020-03-25 07:39:50 -04:00
Isaac Grynsztein
72b42dea5a About dialog now informs user when update is available or when they are up to date based on the github release api 2020-03-25 07:39:39 -04:00
Isaac Grynsztein
d0221f2233 Updated gitignore and public dir 2020-03-25 04:22:44 -04:00
Isaac Grynsztein
43ea401d53 Removed old releases from repo 2020-03-25 04:16:37 -04:00
Isaac Grynsztein
1ed415d733 Fixed bug where if using download-only mode, downloading additonal videos would be blocked 2020-03-25 04:16:11 -04:00
Isaac Grynsztein
1808281b50 updated gitignore 2020-03-25 04:05:40 -04:00
Isaac Grynsztein
b4dc655f2f Re-added contents of public directory to repo 2020-03-25 04:05:00 -04:00
Isaac Grynsztein
47a1173a80 Updated app.js to remove error when not run from backend directory 2020-03-25 03:57:15 -04:00
Isaac Grynsztein
6c22c0e708 fixed heroku build process so it skips building the frontend 2020-03-25 03:46:56 -04:00
Isaac Grynsztein
0d756c4c97 If no config exists, one will be auto generated from the default 2020-03-24 20:59:58 -04:00
Isaac Grynsztein
d4664bad45 Additional fix for bug that caused server to crash when failing to update youtube-dl 2020-03-24 19:36:45 -04:00
Isaac Grynsztein
03e3eb9a81 Fixed bug where failed youtube-dl updates crashed the server (it should just continue uninterrupted) 2020-03-24 19:27:48 -04:00
Isaac Grynsztein
9e4d36e6ed Updated look of about dialog's close button 2020-03-24 19:27:25 -04:00
Isaac Grynsztein
7c605d83cd Updated look of dark mode to be more "dark" and more friendly 2020-03-24 19:26:56 -04:00
Isaac Grynsztein
a4f97d3814 Removed config writing in docker compose 2020-03-22 03:32:21 -04:00
Isaac Grynsztein
c003b02153 updated gitignores 2020-03-22 03:12:24 -04:00
Isaac Grynsztein
f478f14de1 heroku fix (7) 2020-03-22 03:01:18 -04:00
Isaac Grynsztein
d0ffb4d90e heroku fix (6) 2020-03-22 02:47:25 -04:00
Isaac Grynsztein
6d62669a43 heroku fix (5) 2020-03-22 02:33:44 -04:00
Isaac Grynsztein
8c6478bfef Final fix for heroku procfile (3) 2020-03-22 02:25:35 -04:00
Isaac Grynsztein
9384436f7e fixed procfile for heroku (2) 2020-03-22 02:07:56 -04:00
Isaac Grynsztein
bb3c85b0e1 Made changes to support heroku (1) 2020-03-22 01:38:15 -04:00
Isaac Grynsztein
d7068953a8 changed env to prod 2020-03-22 01:11:15 -04:00
Isaac Grynsztein
7d9ad0fce1 removed debug logging 2020-03-22 00:55:00 -04:00
Isaac Grynsztein
b3b2175c67 added debug messages to debug heroku 2020-03-22 00:37:06 -04:00
Isaac Grynsztein
d8ea848e26 Attempted to fix heroku server port 2020-03-22 00:13:08 -04:00
Isaac Grynsztein
fb5054a1d7 when using heroku, port is auto set as heroku's port 2020-03-21 22:11:50 -04:00
Isaac Grynsztein
25dc8d137a Fixed heroku port 2020-03-21 21:58:13 -04:00
Isaac Grynsztein
1d6fddf386 removed package-lock 2020-03-21 21:41:20 -04:00
Isaac Grynsztein
5dce997e3a Updated built location 2020-03-21 21:30:59 -04:00
Isaac Grynsztein
3a6d0f38d7 updated procfile and made server heroku compatible 2020-03-21 21:28:29 -04:00
Isaac Grynsztein
17d877f44a Added heroku-required app.json 2020-03-21 19:51:57 -04:00
Isaac Grynsztein
c9a95e48fb Added new step to docker setup 2020-03-21 01:02:38 -04:00
Isaac Grynsztein
5cced5aed8 Updated README docker instructions to reflect changes in v3.5 2020-03-21 00:50:54 -04:00
Isaac Grynsztein
fdba83bd91 image urls hotfix 2020-03-20 23:56:15 -04:00
Isaac Grynsztein
8bcf46d6aa Updated images and instructions on README to reflect v3.5 2020-03-20 23:55:28 -04:00
Isaac Grynsztein
945d2abd1a UI hotfix 2020-03-20 23:07:35 -04:00
Isaac Grynsztein
4094fda71d Updated spanish translations to final 3.5 version
Removed erroneous log
2020-03-20 22:59:56 -04:00
Tzahi12345
aed64b09fe Merge pull request #36 from Tzahi12345/settings-pin
Added ability to set pin for settings
2020-03-20 22:48:55 -04:00
Isaac Grynsztein
fa2800a391 merge fixd import 2020-03-20 22:47:04 -04:00
Isaac Grynsztein
4aaaddac92 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into settings-pin 2020-03-20 22:46:06 -04:00
Isaac Grynsztein
c3220dcb60 Updated language select size 2020-03-20 16:35:28 -04:00
Isaac Grynsztein
996d5989e7 Updated spanish translation 2020-03-20 16:34:57 -04:00
Isaac Grynsztein
7681ac2e5a Updated source translation file 2020-03-20 16:21:15 -04:00
Isaac Grynsztein
87e7f00aef Updated chrome extension 2020-03-20 16:19:23 -04:00
Isaac Grynsztein
0ac87fe86d Updated spanish language string label to avoid confusion
Fixed bug where if no locale was set, it would *not* default to English
2020-03-20 16:18:51 -04:00
Isaac Grynsztein
ce2f294a3d Removed potential race condition with youtube-dl archives in main downloader
Fixed bug where downloaded subscriptions' zip files would include erroneous folders for their videos
2020-03-20 16:17:15 -04:00
Isaac Grynsztein
2b2a033b7e Added extensions settings where information on extensions can be found and bookmarklet is generated
Created arg modifier dialog to assist in editing youtube-dl args
- This arg dialog contains all the available args and their description
- Includes a convenient search bar and categorized list of args to help you find the one you're looking for, or just explore what's available.

Arg modifier is available for both global args (in settings) and local args (in the advanced mode)
2020-03-20 16:16:10 -04:00
Isaac Grynsztein
f796a5863c Added package-lock to .gitignore 2020-03-18 00:33:12 -04:00
Isaac Grynsztein
57e3f1b2ac removed frivolous logging 2020-03-18 00:32:43 -04:00
Tzahi12345
8f17305710 Merge pull request #35 from Tzahi12345/id3-tagging
Added automatic ID3 tagging for mp3 files
2020-03-18 00:29:31 -04:00
Tzahi12345
4b67527bd5 Merge branch 'master' into id3-tagging 2020-03-18 00:29:23 -04:00
Tzahi12345
4f119d081e Merge pull request #34 from Tzahi12345/auto-update-youtubedl
Auto update youtube-dl binaries on startup
2020-03-18 00:17:25 -04:00
Isaac Grynsztein
2e71a0bef1 fixed bug that caused youtube downloader update to fail when the binary was being used to check for new subscription videos. now it waits for file access with a 10 second timeout 2020-03-18 00:04:18 -04:00
Isaac Grynsztein
1f9f07ac56 youtube-dl now auto updates on server restart 2020-03-17 23:22:26 -04:00
Tzahi12345
c4fe1a2fb0 Merge pull request #32 from Tzahi12345/subscriptions-v2
Subscriptions v2
2020-03-17 22:35:32 -04:00
Isaac Grynsztein
b987e258b5 Updated firefox extension zip 2020-03-17 22:25:21 -04:00
Isaac Grynsztein
bdb6a08274 Added ability to download subscription videos as zip 2020-03-17 22:24:52 -04:00
Isaac Grynsztein
b2730926c8 Updated translation details to improve clarity
Added upload date property to files in UI

Subscription videos can now be filtered by some of their properties (size, upload date, name, duration)

Subscription videos are now centered
2020-03-17 21:38:49 -04:00
Isaac Grynsztein
393267b919 fixed bug where youtube search API failed to load when enabled through the settings 2020-03-17 18:49:19 -04:00
Isaac Grynsztein
35f8454db3 Fixed bug where auto started downloads would begin if navigated back from the player 2020-03-17 18:20:25 -04:00
Isaac Grynsztein
1837101083 Updated manifest 2020-03-17 18:15:17 -04:00
Isaac Grynsztein
aa80f52838 Updated firefox extension to v0.3, focused on making the settings menu look nicer 2020-03-17 18:15:07 -04:00
Isaac Grynsztein
a5e1906196 Fixed bug that prevented auto start from working 2020-03-17 18:14:11 -04:00
Isaac Grynsztein
9dc607e7ee fixed bug where no subscriptions resulted in a client error 2020-03-17 18:07:35 -04:00
Isaac Grynsztein
1a79b489ab Added video info dialog
File cards and subscription file cards now use video info dialog so that users can see info on each individual video

Ellipsis are now added client-side to video titles in file cards
2020-03-17 06:58:05 -04:00
Isaac Grynsztein
3bdacd4b52 Added additional volumes to docker compose 2020-03-17 05:29:25 -04:00
Isaac Grynsztein
f08506d690 When locale 'en' is selected, no translation is retrieved 2020-03-16 01:23:35 -04:00
Isaac Grynsztein
1d29dd8df4 Updated translations 2020-03-16 01:23:08 -04:00
Isaac Grynsztein
25b953cebd Updated look of file cards again to prevent aspect ratio from being changed 2020-03-16 01:22:48 -04:00
Isaac Grynsztein
ff2f5c89da Changed location of db and config to one unified directory, 'appdata'
Archive files now get generated if nonexistent during deletions

Simplified docker-compose.yml to not require environment variables. Added volume for appdata folder which will be automatically shipped with docker builds
2020-03-16 01:22:09 -04:00
Isaac Grynsztein
2cd35cccd1 Added about page 2020-03-15 22:05:00 -04:00
Isaac Grynsztein
da399601e1 Added ability to select any supported custom downloader 2020-03-15 20:59:21 -04:00
Tzahi12345
4ceec26a0e Merge pull request #31 from Tzahi12345/use-youtubedl-archive-with-downloader
Implements youtube-dl archive functionality for downloader
2020-03-15 20:39:18 -04:00
Isaac Grynsztein
b765830584 Using youtubedl archive for downloads defaults to false 2020-03-15 20:37:26 -04:00
Isaac Grynsztein
44bff55a88 Adds youtube dl args to simulated output 2020-03-15 20:35:51 -04:00
Isaac Grynsztein
84c9ec1300 Updated file card image appearance 2020-03-15 20:31:04 -04:00
Isaac Grynsztein
f1c09a5fa9 Added firefox extension zip to repo 2020-03-15 20:30:48 -04:00
Isaac Grynsztein
d39f6f7a17 File cards modified to support blacklisting videos when using youtube-dl archive 2020-03-15 20:28:18 -04:00
Isaac Grynsztein
190d1567ca youtube-dl archive can now be used for regular downloads. a new blacklist system exists which subscriptions will shortly follow
Added new setting whether to use youtubedl archive
2020-03-15 20:27:06 -04:00
Isaac Grynsztein
e71dc0b5e5 Updated angular version on readme 2020-03-15 15:39:19 -04:00
Tzahi12345
7d6ad6336c Merge pull request #30 from Tzahi12345/localization
Added localization support
2020-03-15 15:18:00 -04:00
Isaac Grynsztein
087b9aac7e updated translations v2 2020-03-15 15:05:10 -04:00
Isaac Grynsztein
d61573d8af Updated spanish translation 2020-03-15 14:55:41 -04:00
Isaac Grynsztein
c7885e8726 Added ability to select language in settings
Fixed locale support so that it actually works at runtime

Removed unused library
2020-03-15 14:52:07 -04:00
Isaac Grynsztein
ebd0a419df Further fix for videogular compilation error 2020-03-15 04:09:30 -04:00
Isaac Grynsztein
c96aec7ba3 fixed videogular in angular 9 2020-03-15 03:59:00 -04:00
Isaac Grynsztein
ddcd1d0772 Updated spanish translation xliff and json
locale is now retrieved from cookies
2020-03-15 03:58:31 -04:00
Tzahi12345
bcd706f477 Merge pull request #29 from Xanthus1/custom-videopath-fix
Fix for custom video folder path
2020-03-15 03:55:38 -04:00
Isaac Grynsztein
c17d29075e Updated angular material to v9
Added further translation support
2020-03-15 00:48:16 -04:00
WScottN
25b65b08d5 Fix for custom video folder path
This fixes an issue with video playback when using a different video path setting than the default "video/" .  Using the videoFolderPath variable rather than "video/" string.
2020-03-15 00:24:44 -04:00
Isaac Grynsztein
0981dd216d Migrated to angular 9 2020-03-14 23:37:27 -04:00
Isaac Grynsztein
20b580b76f updated to latest angular 8 version 2020-03-14 23:32:44 -04:00
Isaac Grynsztein
bc4cc3ccce Added spanish language support 2020-03-14 23:29:38 -04:00
Isaac Grynsztein
21797f3901 Added preliminary localization support to almost all strings in the program 2020-03-14 17:32:52 -04:00
Isaac Grynsztein
91f6dbcb5b added ability to set pin for settings. pin is md5'd by the backend 2020-03-13 02:20:07 -04:00
Isaac Grynsztein
d9edb40cd5 Added basic id3 tagging functionality and simplified mp3 downloading logic 2020-03-13 00:34:27 -04:00
Isaac Grynsztein
c752b13732 Updated look of file cards so that the images are of equal size 2020-03-13 00:33:02 -04:00
Isaac Grynsztein
bad4b8630b potentially made it so saved settings get applied with no reload in most situations 2020-03-11 22:04:22 -04:00
Isaac Grynsztein
4f4a82c3c4 updated firefox/chrome extension to support firefox with no errors 2020-03-11 03:35:57 -04:00
Isaac Grynsztein
25307dc46b fixed bug where downloaded audio files did not properly get converted to mp3s 2020-03-10 16:24:04 -04:00
Isaac Grynsztein
d873597ea2 fixed formatting in readme 2020-03-10 03:36:46 -04:00
Isaac Grynsztein
f92a5549b5 implemented allowSubscriptions in frontend 2020-03-10 02:28:48 -04:00
Isaac Grynsztein
69bf4e1ad5 updated docker version 2020-03-10 02:10:36 -04:00
Isaac Grynsztein
9d1aaf95ed Refactored subscribing process to remove bugs in the old system
images are now deleted from subscription videos when unsubscribing
2020-03-10 02:03:10 -04:00
Isaac Grynsztein
bb925ac0c8 fixed bug where video titles were used instead of IDs for the player component
fixed bug that caused a crash when no subscriptions existed
2020-03-09 01:19:16 -04:00
Isaac Grynsztein
74cda25c63 added search functionality
made subscription file cards more responsive on mobile layouts

removed unused shell code
2020-03-09 00:22:03 -04:00
Isaac Grynsztein
54dcbe452e fixed compiler error 2020-03-08 22:52:29 -04:00
Isaac Grynsztein
946abd2e92 implemented global custom args functionality
fixed bad logic in settings
2020-03-08 22:47:08 -04:00
Isaac Grynsztein
73d4cca615 added new config items to docker compose 2020-03-08 22:30:43 -04:00
Isaac Grynsztein
846dd7e250 Added the ability to download (export) archives from subscriptions 2020-03-08 22:24:59 -04:00
Isaac Grynsztein
6f3e94cf24 hamburger menu button now avoids focus and has no outline
theme change behavior slightly modified to improve accessibility

added hammerjs

settings menu now has minimum width, updated colors, and additional hints
2020-03-08 22:23:50 -04:00
Isaac Grynsztein
3cbb517d64 cleaned up some code
youtube-dl commands are now simulated and displayed in the advanced panel
2020-03-08 22:21:34 -04:00
Isaac Grynsztein
5d945d729b hotfix to readme formatting 2020-03-08 17:27:15 -04:00
Isaac Grynsztein
cda842a6c7 Updated readme to include API info 2020-03-08 17:26:13 -04:00
Isaac Grynsztein
480ed7d000 added new custom args setting 2020-03-08 10:15:24 -04:00
Isaac Grynsztein
881a103051 Added duration of video in subscription file card along with implementations of deleting subscribed videos. Subscribed videos now get reloaded after deletion
sidenav now closes when navigating

Updated subscription info to include more info
2020-03-07 17:00:50 -05:00
Isaac Grynsztein
4172b0b355 fixed bug where in chrome, sometimes the video player would not appear 2020-03-07 13:16:16 -05:00
Isaac Grynsztein
f6b7c41666 fixed router outlet in sidenav
subscription settings implemented
2020-03-06 03:05:51 -05:00
Tzahi12345
3d1874c69b Merge pull request #22 from Tzahi12345/subscribe_to_channel_and_playlist
Adds the ability to subscribe to channels and playlists
2020-03-05 22:59:08 -05:00
Tzahi12345
ccfe7901c9 Merge branch 'master' into subscribe_to_channel_and_playlist 2020-03-05 22:57:57 -05:00
Tzahi12345
17e85196ae Merge pull request #21 from Tzahi12345/settings
Add settings page
2020-03-05 22:48:45 -05:00
Isaac Grynsztein
ae605d5f70 Added ability to set config from settings
theme slide toggle is now in top right menu
2020-03-05 22:38:23 -05:00
Isaac Grynsztein
e57839e8de updated .gitignore 2020-03-05 21:25:29 -05:00
Isaac Grynsztein
09bcac1c14 added settings 2020-03-05 21:24:29 -05:00
Isaac Grynsztein
f5073b83ed subscriptions without names will not have files retrieved any longer 2020-03-05 21:19:36 -05:00
Isaac Grynsztein
41bfc80c4e fixed bug in retrieving videos for subscription when name was not present 2020-03-05 21:18:36 -05:00
Isaac Grynsztein
3d2e138f50 updated chrome extension 2020-03-05 21:17:30 -05:00
Isaac Grynsztein
717f024c42 updated .gitignore 2020-03-05 20:16:23 -05:00
Isaac Grynsztein
a70abb3945 added basic subscriptions support for playlists and channels
update youtube-dl binary on windows

updated favicon to the new icon
2020-03-05 20:14:36 -05:00
Isaac Grynsztein
a755b0b281 fixed bug that prevented custom quality path from working 2020-03-01 16:18:01 -05:00
Isaac Grynsztein
434f6751d0 added releases to repo (will only include latest release) 2020-03-01 16:17:41 -05:00
Isaac Grynsztein
dfecf3645b updated README
renamed chrome extension
2020-03-01 03:07:52 -05:00
Isaac Grynsztein
62a000b631 fixed bug where custom paths failed to stream 2020-03-01 02:31:47 -05:00
Isaac Grynsztein
2673f4ee98 updated README 2020-03-01 00:54:11 -05:00
202 changed files with 28111 additions and 4519 deletions

23
.gitignore vendored
View File

@@ -43,10 +43,25 @@ Thumbs.db
node_modules/*
backend/node_modules/*
backend/public/*
YoutubeDL-Material/node_modules/*
backend/video/*
backend/audio/*
backend/public/*
backend/subscriptions/archives/*
backend/subscriptions/playlists/*
backend/subscriptions/channels/*
backend/db.json
backend/public/assets/i18n/*.xlf
backend/public/assets/default.json
backend/subscriptions/channels/*
backend/subscriptions/playlists/*
backend/subscriptions/archives/*
backend/*.exe
src/assets/default.json
backend/appdata/db.json
backend/appdata/archives/archive_audio.txt
backend/appdata/archives/archive_video.txt
backend/appdata/archives/blacklist_audio.txt
backend/appdata/archives/blacklist_video.txt
backend/appdata/logs/combined.log
backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt

View File

@@ -1,21 +0,0 @@
FROM alpine:3.11
RUN apk add --update npm python ffmpeg
# Change directory so that our commands run inside this new directory
WORKDIR /app
# Copy dependency definitions
COPY ./ /app/
# Change directory to backend
WORKDIR /app
# Install dependencies on backend
RUN npm install
# Expose the port the app runs in
EXPOSE 17442
# Run the specified command within the container.
CMD [ "node", "app.js" ]

View File

@@ -1 +1 @@
web: cd backend && node app.js
web: npm start --prefix backend

1700
Public API v1.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
# YoutubeDL-Material
[![](https://img.shields.io/docker/pulls/tzahi12345/youtubedl-material.svg)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![](https://img.shields.io/docker/image-size/tzahi12345/youtubedl-material?sort=date)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![](https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg)](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
[![](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 8](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
Now with [Docker](#Docker) support!
@@ -10,36 +16,40 @@ Check out the prerequisites, and go to the installation section. Easy as pie!
Here's an image of what it'll look like once you're done:
![frontpage](https://i.imgur.com/rOxWIys.png)
![frontpage](https://i.imgur.com/w8iofbb.png)
With optional file management enabled (default):
![frontpage_with_files](https://i.imgur.com/UTUROLl.png)
![frontpage_with_files](https://i.imgur.com/FTATqBM.png)
Dark mode:
![dark_mode](https://i.imgur.com/9TMkHF6.png?1)
![dark_mode](https://i.imgur.com/r5ZtBqd.png)
### Prerequisites
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
```
sudo apt-get install nodejs youtube-dl
```
Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
### Installing
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
2. Drag all the files in `youtubedl-material` to an easily accessible directory. Navigate to the `config` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup), this next step is not necessary
NOTE: If you are intending to use a reverse proxy, this next step is not necessary
3. Port forward the port listed in `default.json`, which defaults to `17442`.
4. Once the configuration is done, run `npm install` to install all the backend dependencies. Once that is finished, type `nodejs app.js`. This will run the backend server, which serves the frontend as well. On your browser, navigate to to the server (url with the specified port). Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
4. Once the configuration is done, run `npm install` to install all the backend dependencies. Once that is finished, type `npm start`. This will run the backend server, which serves the frontend as well. On your browser, navigate to to the server (url with the specified port). Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
@@ -69,29 +79,40 @@ Here is an explanation for the configuration entries. Check out the [default con
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
| allow_advanced_download | true if you want to use the Advanced download options - NOT FULLY IMPLEMENTED | false |
| allow_advanced_download | true if you want to use the Advanced download options | false |
## Deployment
## Build it yourself
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/backend/config`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` folder.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `nodejs app.js`.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
Finally, port forward the port specified in the config (defaults to `17442`) and point it to the server's IP address. Make sure the port is also allowed through the server's firewall.
Finally, if you want your instance to be available from outside your network, you can set up a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup).
Alternatively, you can port forward the port specified in the config (defaults to `17442`) and point it to the server's IP address. Make sure the port is also allowed through the server's firewall.
## Docker
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest release of `docker-compose.yml`, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
2. Modify the config items in the `environment` section of `docker-compose.yml` to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
3. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
4. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
5. Make sure you can connect to the specified URL + port, and if so, you are done!
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/youtubedl-material-docker.zip -o youtubedl-material-docker.zip` to download the latest Docker zip release, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
2. Unzip the `youtubedl-material-docker.zip` and navigate into the root folder.
3. Modify the config items in the `appdata` folder to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
4. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
5. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
6. Make sure you can connect to the specified URL + port, and if so, you are done!
## API
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
Once you have enabled the API and have the key, you can start sending requests by adding the query param `apiKey=API_KEY`. Replace `API_KEY` with your actual API key, and you should be good to go! Nearly all of the backend should be at your disposal. View available endpoints in the link above.
## Contributing

View File

@@ -7,11 +7,18 @@
"root": "",
"sourceRoot": "src",
"projectType": "application",
"i18n": {
"sourceLocale": "en-US",
"locales": {
"es": "src/locale/messages.es.xlf"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"aot": true,
"outputPath": "backend/public",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
@@ -30,6 +37,12 @@
},
"configurations": {
"production": {
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
@@ -45,6 +58,9 @@
"with": "src/environments/environment.prod.ts"
}
]
},
"es": {
"localize": ["es"]
}
}
},
@@ -56,6 +72,9 @@
"configurations": {
"production": {
"browserTarget": "youtube-dl-material:build:production"
},
"es": {
"browserTarget": "youtube-dl-material:build:es"
}
}
},
@@ -176,7 +195,7 @@
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "scss"
"style": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"

7
app.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "YoutubeDL-Material",
"description": "An open-source and self-hosted YouTube downloader based on Google's Material Design specifications.",
"repository": "https://github.com/Tzahi12345/YoutubeDL-Material",
"logo": "https://i.imgur.com/GPzvPiU.png",
"keywords": ["youtube-dl", "youtubedl-material", "nodejs"]
}

4
backend/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
*.exe
docker-compose.yml
Dockerfile

27
backend/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM alpine:3.12
ENV UID=1000 \
GID=1000 \
USER=youtube
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
RUN apk add --no-cache \
ffmpeg \
npm \
python2 \
su-exec \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
WORKDIR /app
COPY --chown=$UID:$GID [ "package.json", "package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID [ "./", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
{
"YoutubeDLMaterial": {
"Host": {
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
},
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
},
"Users": {
"base_path": "users/",
"allow_registration": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"logger_level": "info"
}
}
}

View File

@@ -11,16 +11,23 @@
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/"
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "Youtube Downloader",
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},
@@ -28,10 +35,23 @@
"default_theme": "default",
"allow_theme_change": true
},
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
},
"Users": {
"base_path": "users/",
"allow_registration": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"allow_advanced_download": false
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"logger_level": "info"
}
}
}

Binary file not shown.

4
backend/audio/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@@ -0,0 +1,540 @@
const path = require('path');
const config_api = require('../config');
const consts = require('../consts');
var subscriptions_api = require('../subscriptions')
const fs = require('fs-extra');
var jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4');
var bcrypt = require('bcrypt');
var LocalStrategy = require('passport-local').Strategy;
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
// other required vars
let logger = null;
var users_db = null;
let SERVER_SECRET = null;
let JWT_EXPIRATION = null;
let opts = null;
let saltRounds = null;
exports.initialize = function(input_users_db, input_logger) {
setLogger(input_logger)
setDB(input_users_db);
/*************************
* Authentication module
************************/
saltRounds = 10;
JWT_EXPIRATION = (60 * 60); // one hour
SERVER_SECRET = null;
if (users_db.get('jwt_secret').value()) {
SERVER_SECRET = users_db.get('jwt_secret').value();
} else {
SERVER_SECRET = uuid();
users_db.set('jwt_secret', SERVER_SECRET).write();
}
opts = {}
opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
opts.secretOrKey = SERVER_SECRET;
/*opts.issuer = 'example.com';
opts.audience = 'example.com';*/
exports.passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
const user = users_db.get('users').find({uid: jwt_payload.user}).value();
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
}));
}
function setLogger(input_logger) {
logger = input_logger;
}
function setDB(input_users_db) {
users_db = input_users_db;
}
exports.passport = require('passport');
exports.passport.serializeUser(function(user, done) {
done(null, user);
});
exports.passport.deserializeUser(function(user, done) {
done(null, user);
});
/***************************************
* Register user with hashed password
**************************************/
exports.registerUser = function(req, res) {
var userid = req.body.userid;
var username = req.body.username;
var plaintextPassword = req.body.password;
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
res.sendStatus(409);
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
return;
}
bcrypt.hash(plaintextPassword, saltRounds)
.then(function(hash) {
let new_user = {
name: username,
uid: userid,
passhash: hash,
files: {
audio: [],
video: []
},
playlists: {
audio: [],
video: []
},
subscriptions: [],
created: Date.now(),
role: userid === 'admin' ? 'admin' : 'user',
permissions: [],
permission_overrides: []
};
// check if user exists
if (users_db.get('users').find({uid: userid}).value()) {
// user id is taken!
logger.error('Registration failed: UID is already taken!');
res.status(409).send('UID is already taken!');
} else if (users_db.get('users').find({name: username}).value()) {
// user name is taken!
logger.error('Registration failed: User name is already taken!');
res.status(409).send('User name is already taken!');
} else {
// add to db
users_db.get('users').push(new_user).write();
logger.verbose(`New user created: ${new_user.name}`);
res.send({
user: new_user
});
}
})
.then(function(result) {
})
.catch(function(err) {
logger.error(err);
if( err.code == 'ER_DUP_ENTRY' ) {
res.status(409).send('UserId already taken');
} else {
res.sendStatus(409);
}
});
}
/***************************************
* Login methods
**************************************/
/*************************************************
* This gets called when passport.authenticate()
* gets called.
*
* This checks that the credentials are valid.
* If so, passes the user info to the next middleware.
************************************************/
exports.passport.use(new LocalStrategy({
usernameField: 'userid',
passwordField: 'password'},
function(username, password, done) {
const user = users_db.get('users').find({name: username}).value();
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
if (user) {
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
}
}
));
/*passport.use(new BasicStrategy(
function(userid, plainTextPassword, done) {
const user = users_db.get('users').find({name: userid}).value();
if (user) {
var hashedPwd = user.passhash;
return bcrypt.compare(plainTextPassword, hashedPwd);
} else {
return false;
}
}
));
*/
/*************************************************************
* This is a wrapper for auth.passport.authenticate().
* We use this to change WWW-Authenticate header so
* the browser doesn't pop-up challenge dialog box by default.
* Browser's will pop-up up dialog when status is 401 and
* "WWW-Authenticate:Basic..."
*************************************************************/
/*
exports.authenticateViaPassport = function(req, res, next) {
exports.passport.authenticate('basic',{session:false},
function(err, user, info) {
if(!user){
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
res.status(401).send('Invalid Authentication');
} else {
req.user = user;
next();
}
}
)(req, res, next);
};
*/
/**********************************
* Generating/Signing a JWT token
* And attaches the user info into
* the payload to be sent on every
* request.
*********************************/
exports.generateJWT = function(req, res, next) {
var payload = {
exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION
, user: req.user.uid
};
req.token = jwt.sign(payload, SERVER_SECRET);
next();
}
exports.returnAuthResponse = function(req, res) {
res.status(200).json({
user: req.user,
token: req.token,
permissions: exports.userPermissions(req.user.uid),
available_permissions: consts['AVAILABLE_PERMISSIONS']
});
}
/***************************************
* Authorization: middleware that checks the
* JWT token for validity before allowing
* the user to access anything.
*
* It also passes the user object to the next
* middleware through res.locals
**************************************/
exports.ensureAuthenticatedElseError = function(req, res, next) {
var token = getToken(req.query);
if( token ) {
try {
var payload = jwt.verify(token, SERVER_SECRET);
// console.log('payload: ' + JSON.stringify(payload));
// check if user still exists in database if you'd like
res.locals.user = payload.user;
next();
} catch(err) {
res.status(401).send('Invalid Authentication');
}
} else {
res.status(401).send('Missing Authorization header');
}
}
// change password
exports.changeUserPassword = async function(user_uid, new_pass) {
return new Promise(resolve => {
bcrypt.hash(new_pass, saltRounds)
.then(function(hash) {
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
resolve(true);
}).catch(err => {
resolve(false);
});
});
}
// change user permissions
exports.changeUserPermissions = function(user_uid, permission, new_value) {
try {
const user_db_obj = users_db.get('users').find({uid: user_uid});
user_db_obj.get('permissions').pull(permission).write();
user_db_obj.get('permission_overrides').pull(permission).write();
if (new_value === 'yes') {
user_db_obj.get('permissions').push(permission).write();
user_db_obj.get('permission_overrides').push(permission).write();
} else if (new_value === 'no') {
user_db_obj.get('permission_overrides').push(permission).write();
}
return true;
} catch (err) {
logger.error(err);
return false;
}
}
// change role permissions
exports.changeRolePermissions = function(role, permission, new_value) {
try {
const role_db_obj = users_db.get('roles').get(role);
role_db_obj.get('permissions').pull(permission).write();
if (new_value === 'yes') {
role_db_obj.get('permissions').push(permission).write();
}
return true;
} catch (err) {
logger.error(err);
return false;
}
}
exports.adminExists = function() {
return !!users_db.get('users').find({uid: 'admin'}).value();
}
// video stuff
exports.getUserVideos = function(user_uid, type) {
const user = users_db.get('users').find({uid: user_uid}).value();
return user['files'][type];
}
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
if (!type) {
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
if (!file) {
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
if (file) type = 'video';
} else {
type = 'audio';
}
}
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
// prevent unauthorized users from accessing the file info
if (requireSharing && !file['sharingEnabled']) file = null;
return file;
}
exports.addPlaylist = function(user_uid, new_playlist, type) {
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
return true;
}
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames, type) {
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
return true;
}
exports.removePlaylist = function(user_uid, playlistID, type) {
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
return true;
}
exports.getUserPlaylists = function(user_uid, type) {
const user = users_db.get('users').find({uid: user_uid}).value();
return user['playlists'][type];
}
exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
let playlist = null;
if (!type) {
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
if (!playlist) {
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
if (playlist) type = 'video';
} else {
type = 'audio';
}
}
if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
// prevent unauthorized users from accessing the file info
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
return playlist;
}
exports.registerUserFile = function(user_uid, file_object, type) {
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.remove({
path: file_object['path']
}).write();
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.push(file_object)
.write();
}
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
let success = false;
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
if (file_obj) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
// close descriptors
if (config_api.descriptors[file_obj.id]) {
try {
for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) {
config_api.descriptors[file_obj.id][i].destroy();
}
} catch(e) {
}
}
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.remove({
uid: file_uid
}).write();
if (fs.existsSync(full_path)) {
// remove json and file
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
let youtube_id = null;
if (fs.existsSync(json_path)) {
youtube_id = fs.readJSONSync(json_path).id;
fs.unlinkSync(json_path);
} else if (fs.existsSync(alternate_json_path)) {
youtube_id = fs.readJSONSync(alternate_json_path).id;
fs.unlinkSync(alternate_json_path);
}
fs.unlinkSync(full_path);
// do archive stuff
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if (fs.existsSync(archive_path)) {
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
if (blacklistMode && line) {
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
// adds newline to the beginning of the line
line = '\n' + line;
fs.appendFileSync(blacklistPath, line);
}
} else {
logger.info(`Could not find archive file for ${type} files. Creating...`);
fs.ensureFileSync(archive_path);
}
}
}
success = true;
} else {
success = false;
logger.warn(`User file ${file_uid} does not exist!`);
}
return success;
}
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
let success = false;
const user_db_obj = users_db.get('users').find({uid: user_uid});
if (user_db_obj.value()) {
const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
if (file_db_obj.value()) {
success = true;
file_db_obj.assign({sharingEnabled: enabled}).write();
}
}
return success;
}
exports.userHasPermission = function(user_uid, permission) {
const user_obj = users_db.get('users').find({uid: user_uid}).value();
const role = user_obj['role'];
if (!role) {
// role doesn't exist
logger.error('Invalid role ' + role);
return false;
}
const role_permissions = (users_db.get('roles').value())['permissions'];
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
// check if user has a negative/positive override
if (user_has_explicit_permission && permission_in_overrides) {
// positive override
return true;
} else if (!user_has_explicit_permission && permission_in_overrides) {
// negative override
return false;
}
// no overrides, let's check if the role has the permission
if (role_permissions.includes(permission)) {
return true;
} else {
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
return false;
}
}
exports.userPermissions = function(user_uid) {
let user_permissions = [];
const user_obj = users_db.get('users').find({uid: user_uid}).value();
const role = user_obj['role'];
if (!role) {
// role doesn't exist
logger.error('Invalid role ' + role);
return null;
}
const role_permissions = users_db.get('roles').get(role).get('permissions').value()
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
let permission = consts['AVAILABLE_PERMISSIONS'][i];
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
// check if user has a negative/positive override
if (user_has_explicit_permission && permission_in_overrides) {
// positive override
user_permissions.push(permission);
} else if (!user_has_explicit_permission && permission_in_overrides) {
// negative override
continue;
}
// no overrides, let's check if the role has the permission
if (role_permissions.includes(permission)) {
user_permissions.push(permission);
} else {
continue;
}
}
return user_permissions;
}
function getToken(queryParams) {
if (queryParams && queryParams.jwt) {
var parted = queryParams.jwt.split(' ');
if (parted.length === 2) {
return parted[1];
} else {
return null;
}
} else {
return null;
}
};

View File

@@ -1,8 +1,33 @@
const fs = require('fs');
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
const debugMode = process.env.YTDL_MODE === 'debug';
let configPath = 'config/default.json';
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
var logger = null;
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_logger) {
setLogger(input_logger);
ensureConfigFileExists();
ensureConfigItemsExist();
}
function ensureConfigItemsExist() {
const config_keys = Object.keys(CONFIG_ITEMS);
for (let i = 0; i < config_keys.length; i++) {
const config_key = config_keys[i];
getConfigItem(config_key);
}
}
function ensureConfigFileExists() {
if (!fs.existsSync(configPath)) {
logger.info('Cannot find config file. Creating one with default values...');
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
}
}
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
Object.byString = function(o, s) {
@@ -31,16 +56,26 @@ function getElementNameInConfig(path) {
return elements[elements.length - 1];
}
/**
* Check if config exists. If not, write default config to config path
*/
function configExistsCheck() {
let exists = fs.existsSync(configPath);
if (!exists) {
setConfigFile(DEFAULT_CONFIG);
}
}
/*
* Gets config file and returns as a json
*/
function getConfigFile() {
let raw_data = fs.readFileSync(configPath);
try {
let raw_data = fs.readFileSync(configPath);
let parsed_data = JSON.parse(raw_data);
return parsed_data;
} catch(e) {
console.log('ERROR: Failed to get config file');
logger.error('Failed to get config file');
return null;
}
}
@@ -56,8 +91,17 @@ function setConfigFile(config) {
function getConfigItem(key) {
let config_json = getConfigFile();
if (!CONFIG_ITEMS[key]) console.log('cannot find config with key ' + key);
if (!CONFIG_ITEMS[key]) {
logger.error(`Config item with key '${key}' is not recognized.`);
return null;
}
let path = CONFIG_ITEMS[key]['path'];
const val = Object.byString(config_json, path);
if (val === undefined && Object.byString(DEFAULT_CONFIG, path)) {
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
return Object.byString(DEFAULT_CONFIG, path);
}
return Object.byString(config_json, path);
};
@@ -65,16 +109,23 @@ function setConfigItem(key, value) {
let success = false;
let config_json = getConfigFile();
let path = CONFIG_ITEMS[key]['path'];
let parent_path = getParentPath(path);
let element_name = getElementNameInConfig(path);
let parent_path = getParentPath(path);
let parent_object = Object.byString(config_json, parent_path);
if (!parent_object) {
let parent_parent_path = getParentPath(parent_path);
let parent_parent_object = Object.byString(config_json, parent_parent_path);
let parent_path_arr = parent_path.split('.');
let parent_parent_single_key = parent_path_arr[parent_path_arr.length-1];
parent_parent_object[parent_parent_single_key] = {};
parent_object = Object.byString(config_json, parent_path);
}
if (value === 'false' || value === 'true') {
parent_object[element_name] = (value === 'true');
} else {
parent_object[element_name] = value;
}
success = setConfigFile(config_json);
return success;
@@ -95,7 +146,7 @@ function setConfigItems(items) {
let item_path = CONFIG_ITEMS[key]['path'];
let item_parent_path = getParentPath(item_path);
let item_element_name = getElementNameInConfig(item_path);
let item_parent_object = Object.byString(config_json, item_parent_path);
item_parent_object[item_element_name] = value;
}
@@ -104,10 +155,80 @@ function setConfigItems(items) {
return success;
}
function globalArgsRequiresSafeDownload() {
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
return failedArgs && failedArgs.length > 0;
}
module.exports = {
getConfigItem: getConfigItem,
setConfigItem: setConfigItem,
setConfigItems: setConfigItems,
getConfigFile: getConfigFile,
CONFIG_ITEMS: CONFIG_ITEMS
}
setConfigFile: setConfigFile,
configExistsCheck: configExistsCheck,
CONFIG_ITEMS: CONFIG_ITEMS,
initialize: initialize,
descriptors: {},
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
}
DEFAULT_CONFIG = {
"YoutubeDLMaterial": {
"Host": {
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
},
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
},
"Users": {
"base_path": "users/",
"allow_registration": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"logger_level": "info"
}
}
}

View File

@@ -1,37 +0,0 @@
{
"YoutubeDLMaterial": {
"Host": {
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/"
},
"Extra": {
"title_top": "Youtube Downloader",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true
},
"API": {
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"allow_advanced_download": false
}
}
}

View File

@@ -1,5 +1,3 @@
var config = require('config');
let CONFIG_ITEMS = {
// Host
'ytdl_url': {
@@ -34,6 +32,18 @@ let CONFIG_ITEMS = {
'key': 'ytdl_video_folder_path',
'path': 'YoutubeDLMaterial.Downloader.path-video'
},
'ytdl_use_youtubedl_archive': {
'key': 'ytdl_use_youtubedl_archive',
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
},
'ytdl_custom_args': {
'key': 'ytdl_custom_args',
'path': 'YoutubeDLMaterial.Downloader.custom_args'
},
'ytdl_safe_download_override': {
'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
},
// Extra
'ytdl_title_top': {
@@ -56,9 +66,24 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_multi_download_mode',
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
},
'ytdl_settings_pin_required': {
'key': 'ytdl_settings_pin_required',
'path': 'YoutubeDLMaterial.Extra.settings_pin_required'
},
'ytdl_enable_downloads_manager': {
'key': 'ytdl_enable_downloads_manager',
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
},
// API
'ytdl_use_api_key': {
'key': 'ytdl_use_api_key',
'path': 'YoutubeDLMaterial.API.use_API_key'
},
'ytdl_api_key': {
'key': 'ytdl_api_key',
'path': 'YoutubeDLMaterial.API.API_key'
},
'ytdl_use_youtube_api': {
'key': 'ytdl_use_youtube_api',
'path': 'YoutubeDLMaterial.API.use_youtube_API'
@@ -78,6 +103,38 @@ let CONFIG_ITEMS = {
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
},
// Subscriptions
'ytdl_allow_subscriptions': {
'key': 'ytdl_allow_subscriptions',
'path': 'YoutubeDLMaterial.Subscriptions.allow_subscriptions'
},
'ytdl_subscriptions_base_path': {
'key': 'ytdl_subscriptions_base_path',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_base_path'
},
'ytdl_subscriptions_check_interval': {
'key': 'ytdl_subscriptions_check_interval',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
},
'ytdl_subscriptions_check_interval': {
'key': 'ytdl_subscriptions_check_interval',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
},
'ytdl_subscriptions_use_youtubedl_archive': {
'key': 'ytdl_use_youtubedl_archive',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
},
// Users
'ytdl_users_base_path': {
'key': 'ytdl_users_base_path',
'path': 'YoutubeDLMaterial.Users.base_path'
},
'ytdl_allow_registration': {
'key': 'ytdl_allow_registration',
'path': 'YoutubeDLMaterial.Users.allow_registration'
},
// Advanced
'ytdl_use_default_downloading_agent': {
'key': 'ytdl_use_default_downloading_agent',
@@ -87,10 +144,35 @@ let CONFIG_ITEMS = {
'key': 'ytdl_custom_downloading_agent',
'path': 'YoutubeDLMaterial.Advanced.custom_downloading_agent'
},
'ytdl_multi_user_mode': {
'key': 'ytdl_multi_user_mode',
'path': 'YoutubeDLMaterial.Advanced.multi_user_mode'
},
'ytdl_allow_advanced_download': {
'key': 'ytdl_allow_advanced_download',
'path': 'YoutubeDLMaterial.Advanced.allow_advanced_download'
},
'ytdl_use_cookies': {
'key': 'ytdl_use_cookies',
'path': 'YoutubeDLMaterial.Advanced.use_cookies'
},
'ytdl_logger_level': {
'key': 'ytdl_logger_level',
'path': 'YoutubeDLMaterial.Advanced.logger_level'
}
};
module.exports.CONFIG_ITEMS = CONFIG_ITEMS;
AVAILABLE_PERMISSIONS = [
'filemanager',
'settings',
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
];
module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.0'
}

119
backend/db.js Normal file
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
}

View File

@@ -0,0 +1,15 @@
version: "2"
services:
ytdl_material:
environment:
ALLOW_CONFIG_MUTATIONS: 'true'
restart: always
volumes:
- ./appdata:/app/appdata
- ./audio:/app/audio
- ./video:/app/video
- ./subscriptions:/app/subscriptions
- ./users:/app/users
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:latest

17
backend/entrypoint.sh Executable file
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 '{}' +
exec su-exec "$UID:$GID" "$0" "$@"
fi
exec "$@"

Binary file not shown.

Binary file not shown.

Binary file not shown.

2005
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,17 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
"start": "nodemon -q app.js"
},
"nodemonConfig": {
"ignore": [
"*.js",
"appdata/*",
"public/*"
],
"watch": [
"restart.json"
]
},
"repository": {
"type": "git",
@@ -20,12 +30,31 @@
"dependencies": {
"archiver": "^3.1.1",
"async": "^3.1.0",
"bcrypt": "^4.0.1",
"compression": "^1.7.4",
"config": "^3.2.3",
"exe": "^1.0.2",
"express": "^4.17.1",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
"jsonwebtoken": "^8.5.1",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"merge-files": "^0.1.2",
"multer": "^1.4.2",
"node-fetch": "^2.6.0",
"node-id3": "^0.1.14",
"nodemon": "^2.0.2",
"passport": "^0.4.1",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"read-last-lines": "^1.7.2",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"winston": "^3.2.1",
"youtube-dl": "^3.0.2"
}
}

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,794 @@
@angular-devkit/build-angular
MIT
The MIT License
Copyright (c) 2017 Google, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@angular/animations
MIT
@angular/cdk
MIT
The MIT License
Copyright (c) 2020 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/common
MIT
@angular/compiler
MIT
@angular/core
MIT
@angular/forms
MIT
@angular/localize
MIT
@angular/material
MIT
The MIT License
Copyright (c) 2020 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/platform-browser
MIT
@angular/platform-browser-dynamic
MIT
@angular/router
MIT
core-js
MIT
Copyright (c) 2014-2020 Denis Pushkarev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
file-saver
MIT
The MIT License
Copyright © 2016 [Eli Grey][1].
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[1]: http://eligrey.com
filesize
BSD-3-Clause
Copyright (c) 2020, Jason Mulligan
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of filesize nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
fingerprintjs2
MIT
Fingerprintjs2 Modern & flexible browser fingerprint library v2
https://github.com/Valve/fingerprintjs2
Copyright (c) 2018 Jonas Haag (jonas@lophus.org)
Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com)
Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ng-lazyload-image
MIT
The MIT License (MIT)
Copyright (c) 2016 Oskar Karlsson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
ngx-file-drop
MIT
ngx-videogular
MIT
regenerator-runtime
MIT
MIT License
Copyright (c) 2014-present, Facebook, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
rxjs
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
rxjs-compat
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
tslib
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
uuid
MIT
The MIT License (MIT)
Copyright (c) 2010-2016 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
web-animations-js
Apache-2.0
webpack
MIT
Copyright JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
zone.js
MIT
The MIT License
Copyright (c) 2010-2020 Google LLC. http://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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

@@ -0,0 +1,200 @@
{
"ccc7e92cbdd35e901acf9ad80941abee07bd8f60": "No es necesario incluir URL, solo todo después ",
"f41145afc02fd47ef0576ac79acd2c47ebbf4901": "Argumentos personalizados globales para descargas en la página de inicio.",
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una lista de reproducción",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nombre",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Archivos de sonido",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Archivos de video",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modificar args de youtube-dl",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Args nuevos simulados",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Añadir un arg",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Busqueda por categoria",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usar valor de arg",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor de arg",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Añadir arg",
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancelar",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modificar",
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Descargador de Youtube",
"6d2ec8898344c8955542b0542c942038ef28bb80": "Por favor entre una URL válida",
"a38ae1082fec79ba1f379978337385a539a28e73": " Calidad ",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Usa URL",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": " Ver ",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Solo audio",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Descarga múltiple",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Descarga",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancelar",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avanzado",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Commando simulado:",
"4e4c721129466be9c3862294dc40241b64045998": "Usar argumentos personalizados",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Argumentos personalizados",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usar salida personalizada",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Salida personalizada",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentación",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "La ruta es relativa a la ruta de descarga de la config. No incluya el extensión.",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Usa autenticación",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nombre de usuario",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Contraseña",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Tus archivos de audio están aquí",
"47546e45bbb476baaaad38244db444c427ddc502": "Listas de reproducción",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más.",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídeo",
"960582a8b9d7942716866ecfb7718309728f2916": "Tus archivos de video son aquí",
"0f59c46ca29e9725898093c9ea6b586730d0624e": "No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más.",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nombre:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Cargador:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Tamaño del archivo:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Ruta:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Subido:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Cerca",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Cuenta:",
"321e4419a943044e674beb55b8039f42a9761ca5": "Información",
"826b25211922a1b46436589233cb6f1a163d89b7": "Eliminar",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Eliminar y pones en la lista negra",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Configuraciones",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL desde la que se accederá a esta aplicación, sin el puerto.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Puerto",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Puerto deseado. El valor predeterminado es 17442.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Modo multiusuario",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Ruta base de usuarios",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Ruta base para los usuarios y sus videos descargados.",
"cbe16a57be414e84b6a68309d08fad894df797d6": "Usa cifrado",
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Ruta del archivo de certificado",
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Ruta de archivo de clave",
"4e3120311801c4acd18de7146add2ee4a4417773": "Permitir suscripciones",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Ruta base de suscripciones",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervalo de comprobación",
"0f56a7449b77630c114615395bbda4cab398efd8": "La unidad es segundos, solo incluye números.",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Usa el archivo de youtube-dl",
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Con la función de archivo de youtube-dl,",
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones.",
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos.",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
"ff7cee38a2259526c519f878e71b964f41db4348": "Defecto",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Oscura",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permitir cambio de tema",
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Ruta de la carpeta de audio",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material.",
"46826331da1949bd6fb74624447057099c9d20cd": "Ruta de la carpeta de video",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,,",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Descargador",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Título superior",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Administrador de archivos habilitado",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Administrador de descargas habilitado",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permitir selección de calidad",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modo de solo descarga",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permitir el modo de descarga múltiple",
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Requiere pin para la configuración",
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Establecer nuevo pin",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilitar API pública",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clave API pública",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Ver documentación",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generar",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utilizar la API de YouTube",
"ce10d31febb3d9d60c160750570310f303a22c22": "Clave API de YouTube",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "¡Generar una clave es fácil!",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "¡Haga clic aquí",
"7f09776373995003161235c0c8d02b7f91dbc4df": "para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz.",
"9a2ec6da48771128384887525bdcac992632c863": "para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instrucciones detalladas de configuración.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Generar bookmarklet solo de audio",
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleccione un descargador",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avanzado",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permitir registro de usuario",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuarios",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Puede actualizar desde el menú de configuración.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Tu perfil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creado:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Usted no se ha identificado.",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identificarse",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Salir",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crear cuenta de administrador",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'.",
"70a67e04629f6d412db0a12d51820b480788d795": "Crear",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Suscripciones",
"822fab38216f64e8166d368b59fe756ca39d301b": "Descargas",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Compartir lista de reproducción",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Compartir vídeo",
"1d540dcd271b316545d070f9d182c372d923aadd": "Compartir audio",
"1f6d14a780a37a97899dc611881e6bc971268285": "Habilitar compartir",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usar marca de tiempo",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segundos",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copiar al Portapapeles",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalles",
"383000ab16bf415d5a1d61d7eb7b5959c72a9515": "Se ha producido un error:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Inicio de descarga:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin de descarga:",
"ad127117f9471612f47d01eae09709da444a36a4": "Ruta(s) del archivo:",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Suscríbase a la lista de reproducción o al canal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "La lista de reproducción o la URL del canal",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nombre personalizado",
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Esto es opcional",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Descargar todas las cargas",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Descargar videos subidos en el último",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Modo de solo transmisión",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Subscribe",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archivo:",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportar el archivo",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Darse de baja",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Sus suscripciones",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canales",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nombre no disponible. Recuperación de canales en progreso.",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "No tienes suscripciones de canal.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Nombre no disponible. Recuperación de listas de reproducción en progreso.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "No tienes suscripciones a listas de reproducción.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Buscar",
"2054791b822475aeaea95c0119113de3200f5e1c": "Duración:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Eliminar y volver a descargar",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Borrar para siempre",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Seleccione una versión:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrarse",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de sesión:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "¡No hay descargas disponibles!",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrar un usuario",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nombre de usuario",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Administrar usuario",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID de usuario:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nueva contraseña",
"6498fa1b8f563988f769654a75411bb8060134b9": "Establecer nueva contraseña",
"40da072004086c9ec00d125165da91eaade7f541": "Uso por defecto",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Si",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestionar rol",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": " Nombre de usuario ",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
backend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

18
backend/public/index.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>YoutubeDLMaterial</title>
<base href="./">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.1f02852de81190376ba1.js" type="module"></script><script src="runtime-es5.1f02852de81190376ba1.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.0cbc545a4a3bee376826.js" type="module"></script><script src="main-es5.0cbc545a4a3bee376826.js" nomodule defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es2015."+{1:"6bc1f7cd24dfb6add92c"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

View File

@@ -0,0 +1 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es5."+{1:"6bc1f7cd24dfb6add92c"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

File diff suppressed because one or more lines are too long

486
backend/subscriptions.js Normal file
View File

@@ -0,0 +1,486 @@
const FileSync = require('lowdb/adapters/FileSync')
var fs = require('fs-extra');
const { uuid } = require('uuidv4');
var path = require('path');
var youtubedl = require('youtube-dl');
const config_api = require('./config');
var utils = require('./utils')
const debugMode = process.env.YTDL_MODE === 'debug';
var logger = null;
var db = null;
var users_db = null;
var db_api = null;
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger, input_db_api) {
setDB(input_db, input_users_db, input_db_api);
setLogger(input_logger);
}
async function subscribe(sub, user_uid = null) {
const result_obj = {
success: false,
error: ''
};
return new Promise(async resolve => {
// sub should just have url and name. here we will get isPlaylist and path
sub.isPlaylist = sub.url.includes('playlist');
sub.videos = [];
let url_exists = false;
if (user_uid)
url_exists = !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({url: sub.url}).value()
else
url_exists = !!db.get('subscriptions').find({url: sub.url}).value();
if (url_exists) {
logger.info('Sub already exists');
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
resolve(result_obj);
return;
}
// add sub to db
let sub_db = null;
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
db.get('subscriptions').push(sub).write();
sub_db = db.get('subscriptions').find({id: sub.id});
}
let success = await getSubscriptionInfo(sub, user_uid);
if (success) {
sub = sub_db.value();
getVideosForSub(sub, user_uid);
} else {
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
};
result_obj.success = success;
result_obj.sub = sub;
resolve(result_obj);
});
}
async function getSubscriptionInfo(sub, user_uid = null) {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
return new Promise(resolve => {
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1']
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
}
if (err) {
logger.error(err.stderr);
resolve(false);
} else if (output) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('Could not get info for ' + sub.id);
resolve(false);
}
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
if (!sub.name) {
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
// if it's now valid, update
if (sub.name) {
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
else
db.get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
}
}
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
if (useArchive && !sub.archive) {
// must create the archive
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
const archive_path = path.join(archive_dir, 'archive.txt');
// creates archive directory and text file if it doesn't exist
fs.ensureDirSync(archive_dir);
fs.ensureFileSync(archive_path);
// updates subscription
sub.archive = archive_dir;
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
else
db.get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
}
// TODO: get even more info
resolve(true);
}
resolve(false);
}
});
});
}
async function unsubscribe(sub, deleteMode, user_uid = null) {
return new Promise(async resolve => {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let result_obj = { success: false, error: '' };
let id = sub.id;
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
else
db.get('subscriptions').remove({id: id}).write();
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && fs.existsSync(appendedBasePath)) {
if (sub.archive && fs.existsSync(sub.archive)) {
const archive_file_path = path.join(sub.archive, 'archive.txt');
// deletes archive if it exists
if (fs.existsSync(archive_file_path)) {
fs.unlinkSync(archive_file_path);
}
fs.rmdirSync(sub.archive);
}
deleteFolderRecursive(appendedBasePath);
}
});
}
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
let basePath = null;
let sub_db = null;
if (user_uid) {
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
sub_db = db.get('subscriptions').find({id: sub.id});
}
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const appendedBasePath = getAppendedBasePath(sub, basePath);
const name = file;
let retrievedID = null;
sub_db.get('videos').remove({uid: file_uid}).write();
return new Promise(resolve => {
let filePath = appendedBasePath;
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+ext);
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath);
imageFileExists = fs.existsSync(imageFilePath);
altImageFileExists = fs.existsSync(altImageFilePath);
if (jsonExists) {
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
fs.unlinkSync(jsonPath);
}
if (imageFileExists) {
fs.unlinkSync(imageFilePath);
}
if (altImageFileExists) {
fs.unlinkSync(altImageFilePath);
}
if (videoFileExists) {
fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
resolve(false);
} else {
// check if the user wants the video to be redownloaded (deleteForever === false)
if (!deleteForever && useArchive && sub.archive && retrievedID) {
const archive_path = path.join(sub.archive, 'archive.txt')
// if archive exists, remove line with video ID
if (fs.existsSync(archive_path)) {
removeIDFromArchive(archive_path, retrievedID);
}
}
resolve(true);
}
});
} else {
// TODO: tell user that the file didn't exist
resolve(true);
}
});
}
async function getVideosForSub(sub, user_uid = null) {
return new Promise(resolve => {
if (!subExists(sub.id, user_uid)) {
resolve(false);
return;
}
// get sub_db
let sub_db = null;
if (user_uid)
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
else
sub_db = db.get('subscriptions').find({id: sub.id});
// get basePath
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
let appendedBasePath = null
appendedBasePath = getAppendedBasePath(sub, basePath);
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
}
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
let fullOutput = appendedBasePath + '/%(title)s' + ext;
if (sub.custom_output) {
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
}
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
} else {
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
}
downloadConfig.push(...qualityPath)
if (sub.custom_args) {
customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {
// if custom args has a custom quality, replce the original quality with that of custom args
const original_output_index = downloadConfig.indexOf('-f');
downloadConfig.splice(original_output_index, 2);
}
downloadConfig.push(...customArgsArray);
}
let archive_dir = null;
let archive_path = null;
if (useArchive) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
}
downloadConfig.push('--download-archive', archive_path);
}
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
logger.error(err.stderr);
if (err.stderr.includes('This video is unavailable')) {
logger.info('An error was encountered with at least one video, backup method will be used.')
try {
const outputs = err.stdout.split(/\r\n|\r|\n/);
for (let i = 0; i < outputs.length; i++) {
const output = JSON.parse(outputs[i]);
handleOutputJSON(sub, sub_db, output, i === 0, multiUserMode)
if (err.stderr.includes(output['id']) && archive_path) {
// we found a video that errored! add it to the archive to prevent future errors
fs.appendFileSync(archive_path, output['id']);
}
}
} catch(e) {
logger.error('Backup method failed. See error below:');
logger.error(e);
}
}
resolve(false);
} else if (output) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name);
resolve(true);
}
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
const reset_videos = i === 0;
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
// TODO: Potentially store downloaded files in db?
}
resolve(true);
}
});
}, err => {
logger.error(err);
});
}
function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
if (sub.streamingOnly) {
if (reset_videos) {
sub_db.assign({videos: []}).write();
}
// remove unnecessary info
output_json.formats = null;
// add to db
sub_db.get('videos').push(output_json).write();
} else {
// TODO: make multiUserMode obj
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
}
}
function getAllSubscriptions(user_uid = null) {
if (user_uid)
return users_db.get('users').find({uid: user_uid}).get('subscriptions').value();
else
return db.get('subscriptions').value();
}
function getSubscription(subID, user_uid = null) {
if (user_uid)
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
else
return db.get('subscriptions').find({id: subID}).value();
}
function subExists(subID, user_uid = null) {
if (user_uid)
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
else
return !!db.get('subscriptions').find({id: subID}).value();
}
// helper functions
function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
// https://stackoverflow.com/a/32197381/8088021
const deleteFolderRecursive = function(folder_to_delete) {
if (fs.existsSync(folder_to_delete)) {
fs.readdirSync(folder_to_delete).forEach((file, index) => {
const curPath = path.join(folder_to_delete, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(folder_to_delete);
}
};
function removeIDFromArchive(archive_path, id) {
let data = fs.readFileSync(archive_path, {encoding: 'utf-8'});
if (!data) {
logger.error('Archive could not be found.');
return;
}
let dataArray = data.split('\n'); // convert file data in an array
const searchKeyword = id; // we are looking for a line, contains, key word id in the file
let lastIndex = -1; // let say, we have not found the keyword
for (let index=0; index<dataArray.length; index++) {
if (dataArray[index].includes(searchKeyword)) { // check if a line contains the id keyword
lastIndex = index; // found a line includes a id keyword
break;
}
}
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
// UPDATE FILE WITH NEW DATA
const updatedData = dataArray.join('\n');
fs.writeFileSync(archive_path, updatedData);
if (line) return line;
if (err) throw err;
}
module.exports = {
getSubscription : getSubscription,
getAllSubscriptions : getAllSubscriptions,
subscribe : subscribe,
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
removeIDFromArchive : removeIDFromArchive,
setLogger : setLogger,
initialize : initialize
}

View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

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
}

4
backend/video/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

7
chrome-extension/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
chrome-extension/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 2,
"name": "YoutubeDL-Material",
"version": "0.1",
"description": "The official chrome extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
"version": "0.3",
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
"background": {
"scripts": ["background.js"]
},
@@ -16,5 +16,11 @@
"options_ui": {
"page": "options.html",
"open_in_tab": false
},
"browser_specific_settings": {
"gecko": {
"id": "IsaacMGrynsztein@gmail.com",
"strict_min_version": "57.0"
}
}
}

View File

@@ -1,29 +1,42 @@
<!DOCTYPE html>
<html>
<head><title>YoutubeDL-Material Extension Options</title></head>
<head>
<title>YoutubeDL-Material Extension Options</title>
<!-- Scripts -->
<script src="js/jquery-3.4.1.min.js"></script>
<script src="js/popper.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<!-- Cascading Style Sheets -->
<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<h2>Settings</h2>
<div>
<h4>Frontend URL</h4>
<input placeholder="Frontend URL" type="text" id="frontend_url">
<div style="width: 95%; margin: 0 auto;">
<div class="form-group">
<label for="frontend_url">Frontend URL:</label>
<input class="form-control" type="text" id="frontend_url">
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="audio_only">
Audio only
</label>
</div>
<div id="status"></div>
<button style="margin-bottom: 10px;" class="btn btn-primary" data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample" id="save">Save</button>
<div class="collapse" id="collapseExample">
<div class="card card-body">
Settings successfully saved!
</div>
</div>
</div>
<br/>
<div>
<label>
<input type="checkbox" id="audio_only">
Audio only
</label>
</div>
<br/>
<div id="status"></div>
<button id="save">Save</button>
<script src="options.js"></script>
</body>
</html>

View File

@@ -7,11 +7,10 @@ function save_options() {
audio_only: audio_only
}, function() {
// Update status to let user know options were saved.
var status = document.getElementById('status');
status.textContent = 'Options saved.';
$('#collapseExample').collapse('show');
setTimeout(function() {
status.textContent = '';
}, 750);
$('#collapseExample').collapse('hide');
}, 2000);
});
}

View File

@@ -1,33 +0,0 @@
version: "2"
services:
ytdl_material:
build: .
environment:
# config items
ytdl_url: http://localhost:8998
ytdl_port: '17442'
ytdl_use_encryption: 'false'
ytdl_cert_file_path: /etc/letsencrypt/live/example.com/fullchain.pem
ytdl_key_file_path: /etc/letsencrypt/live/example.com/privkey.pem
ytdl_audio_folder_path: audio/
ytdl_video_folder_path: video/
ytdl_title_top: Youtube Downloader
ytdl_file_manager_enabled: 'true'
ytdl_allow_quality_select: 'true'
ytdl_download_only_mode: 'false'
ytdl_allow_multi_download_mode: 'true'
ytdl_use_youtube_api: 'false'
ytdl_youtube_api_key: 'false'
ytdl_default_theme: default
ytdl_allow_theme_change: 'true'
ytdl_use_default_downloading_agent: 'true'
ytdl_custom_downloading_agent: 'false'
ytdl_allow_advanced_download: 'false'
# do not touch this
write_ytdl_config: 'true'
ALLOW_CONFIG_MUTATIONS: 'true'
restart: always
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:3.3

View File

@@ -1,39 +0,0 @@
#!/bin/bash
cd backend
# Start the first process
node app.js &
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start my_first_process: $status"
exit $status
fi
# Start the second process
apachectl -DFOREGROUND
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start my_second_process: $status"
exit $status
fi
# Naive check runs checks once a minute to see if either of the processes exited.
# This illustrates part of the heavy lifting you need to do if you want to run
# more than one service in a container. The container will exit with an error
# if it detects that either of the processes has exited.
# Otherwise it will loop forever, waking up every 60 seconds
while /bin/true; do
ps aux |grep node\ app.js # |grep -q -v grep
PROCESS_1_STATUS=$?
ps aux |grep apache2 # |grep -q -v grep
PROCESS_2_STATUS=$?
# If the greps above find anything, they will exit with 0 status
# If they are not both 0, then something is wrong
if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
echo "One of the processes has already exited."
exit -1
fi
sleep 60
done

10279
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{
"name": "youtube-dl-material",
"version": "0.0.0",
"version": "4.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"heroku-postbuild": "npm install --prefix backend",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
@@ -17,41 +18,43 @@
},
"private": true,
"dependencies": {
"@angular-devkit/core": "^8.3.12",
"@angular/animations": "^8.2.11",
"@angular/cdk": "^8.2.3",
"@angular/common": "^8.2.11",
"@angular/compiler": "^8.2.11",
"@angular/core": "^8.2.11",
"@angular/forms": "^8.2.11",
"@angular/http": "^7.2.15",
"@angular/material": "^8.2.3",
"@angular/platform-browser": "^8.2.11",
"@angular/platform-browser-dynamic": "^8.2.11",
"@angular/router": "^8.2.11",
"@angular-devkit/core": "^9.0.6",
"@angular/animations": "^9.1.0",
"@angular/cdk": "^9.2.0",
"@angular/common": "^9.1.0",
"@angular/compiler": "^9.1.0",
"@angular/core": "^9.0.7",
"@angular/forms": "^9.1.0",
"@angular/localize": "^9.1.0",
"@angular/material": "^9.2.0",
"@angular/platform-browser": "^9.1.0",
"@angular/platform-browser-dynamic": "^9.1.0",
"@angular/router": "^9.1.0",
"core-js": "^2.4.1",
"file-saver": "^2.0.2",
"filesize": "^6.1.0",
"fingerprintjs2": "^2.1.0",
"nan": "^2.14.1",
"ng-lazyload-image": "^7.0.1",
"ng4-configure": "^0.1.7",
"ngx-content-loading": "^0.1.3",
"ngx-file-drop": "^9.0.1",
"ngx-videogular": "^9.0.1",
"rxjs": "^6.5.3",
"rxjs-compat": "^6.0.0-rc.0",
"tslib": "^1.10.0",
"videogular2": "^7.0.1",
"typescript": "~3.7.5",
"web-animations-js": "^2.3.2",
"zone.js": "~0.9.1",
"typescript": "~3.5.3"
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.803.24",
"@angular/cli": "^8.3.12",
"@angular/compiler-cli": "^8.2.11",
"@angular/language-service": "^8.2.11",
"@angular-devkit/build-angular": "^0.901.0",
"@angular/cli": "^9.0.7",
"@angular/compiler-cli": "^9.0.7",
"@angular/language-service": "^9.0.7",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "2.5.45",
"@types/node": "~6.0.60",
"codelyzer": "^5.0.1",
"@types/node": "^12.11.1",
"codelyzer": "^5.1.2",
"electron": "^8.0.1",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
@@ -63,7 +66,6 @@
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.0.4",
"tslint": "~5.3.2",
"typescript": "~3.5.3"
"tslint": "~5.3.2"
}
}

Binary file not shown.

View File

@@ -2,10 +2,20 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MainComponent } from './main/main.component';
import { PlayerComponent } from './player/player.component';
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
import { SubscriptionComponent } from './subscription/subscription/subscription.component';
import { PostsService } from './posts.services';
import { LoginComponent } from './components/login/login.component';
import { DownloadsComponent } from './components/downloads/downloads.component';
const routes: Routes = [
{ path: 'home', component: MainComponent },
{ path: 'player', component: PlayerComponent},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: MainComponent, canActivate: [PostsService] },
{ path: 'player', component: PlayerComponent, canActivate: [PostsService]},
{ path: 'subscriptions', component: SubscriptionsComponent, canActivate: [PostsService] },
{ path: 'subscription', component: SubscriptionComponent, canActivate: [PostsService] },
{ path: 'login', component: LoginComponent },
{ path: 'downloads', component: DownloadsComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' }
];
@NgModule({

View File

@@ -10,4 +10,19 @@
flex-direction: column;
flex-basis: 100%;
flex: 1;
}
.theme-slide-toggle {
top: 2px;
left: 10px;
position: relative;
pointer-events: none;
}
.sidenav-container {
z-index: -1 !important;
}
.top-toolbar {
height: 64px;
}

View File

@@ -1,17 +1,54 @@
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
<mat-toolbar color="primary" class="top">
<div class="flex-row" width="100%" height="100%">
<div class="flex-column" style="text-align: left; margin-top: 1px;">
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
<div class="mat-elevation-z3" style="position: relative; z-index: 10;">
<mat-toolbar color="primary" class="sticky-toolbar top-toolbar">
<div class="flex-row" width="100%" height="100%">
<div class="flex-column" style="text-align: left; margin-top: 1px;">
<button #hamburgerMenu style="outline: none" *ngIf="router.url.split(';')[0] !== '/player'" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
</div>
<div class="flex-column" style="text-align: center; margin-top: 5px;">
<div style="font-size: 22px; text-shadow: #141414 0.25px 0.25px 1px;">
{{topBarTitle}}
</div>
</div>
<div class="flex-column" style="text-align: right; align-items: flex-end;">
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #menuSettings="matMenu">
<button (click)="openProfileDialog()" *ngIf="postsService.isLoggedIn" mat-menu-item>
<mat-icon>person</mat-icon>
<span i18n="Profile menu label">Profile</span>
</button>
<button (click)="themeMenuItemClicked($event)" *ngIf="allowThemeChange" mat-menu-item>
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
<span i18n="Dark mode toggle label">Dark</span>
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
</button>
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('settings')" (click)="openSettingsDialog()" mat-menu-item>
<mat-icon>settings</mat-icon>
<span i18n="Settings menu label">Settings</span>
</button>
<button (click)="openAboutDialog()" mat-menu-item>
<mat-icon>info</mat-icon>
<span i18n="About menu label">About</span>
</button>
</mat-menu>
</div>
</div>
<div class="flex-column" style="text-align: center; margin-top: 5px;">
<div>{{topBarTitle}}</div>
</div>
<div class="flex-column" style="text-align: right; align-items: flex-end;">
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
</div>
</div>
</mat-toolbar>
<router-outlet></router-outlet>
</mat-toolbar>
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav #sidenav>
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
</div>

View File

@@ -4,7 +4,9 @@ import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
import {FormControl, Validators} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatSnackBar} from '@angular/material';
import { MatDialog } from '@angular/material/dialog';
import { MatSidenav } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';
import { saveAs } from 'file-saver';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/mapTo';
@@ -15,9 +17,14 @@ import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/do'
import 'rxjs/add/operator/switch'
import { YoutubeSearchService, Result } from './youtube-search.service';
import { Router } from '@angular/router';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { OverlayContainer } from '@angular/cdk/overlay';
import { THEMES_CONFIG } from '../themes';
import { SettingsComponent } from './settings/settings.component';
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
@Component({
selector: 'app-root',
@@ -33,28 +40,56 @@ export class AppComponent implements OnInit {
topBarTitle = 'Youtube Downloader';
defaultTheme = null;
allowThemeChange = null;
allowSubscriptions = false;
enableDownloadsManager = false;
// defaults to true to prevent attack
settingsPinRequired = true;
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
@ViewChild('sidenav') sidenav: MatSidenav;
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
navigator: string = null;
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
constructor(public postsService: PostsService, public snackBar: MatSnackBar, private dialog: MatDialog,
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
// loading config
this.postsService.loadNavItems().subscribe(res => { // loads settings
const result = !this.postsService.debugMode ? res['config_file'] : res;
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
const themingExists = result['YoutubeDLMaterial']['Themes'];
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true;
// sets theme to config default if it doesn't exist
if (!localStorage.getItem('theme')) {
this.setTheme(themingExists ? this.defaultTheme : 'default');
this.navigator = localStorage.getItem('player_navigator');
// runs on navigate, captures the route that navigated to the player (if needed)
this.router.events.subscribe((e) => { if (e instanceof NavigationStart) {
this.navigator = localStorage.getItem('player_navigator');
} else if (e instanceof NavigationEnd) {
// blurs hamburger menu if it exists, as the sidenav likes to focus on it after closing
if (this.hamburgerMenuButton && this.hamburgerMenuButton.nativeElement) {
this.hamburgerMenuButton.nativeElement.blur();
}
}, error => {
console.log(error);
}
});
this.postsService.config_reloaded.subscribe(changed => {
if (changed) {
this.loadConfig();
}
});
}
toggleSidenav() {
this.sidenav.toggle();
}
loadConfig() {
// loading config
this.topBarTitle = this.postsService.config['Extra']['title_top'];
this.settingsPinRequired = this.postsService.config['Extra']['settings_pin_required'];
const themingExists = this.postsService.config['Themes'];
this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default';
this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true;
this.allowSubscriptions = this.postsService.config['Subscriptions']['allow_subscriptions'];
this.enableDownloadsManager = this.postsService.config['Extra']['enable_downloads_manager'];
// sets theme to config default if it doesn't exist
if (!localStorage.getItem('theme')) {
this.setTheme(themingExists ? this.defaultTheme : 'default');
}
}
// theme stuff
@@ -105,17 +140,77 @@ onSetTheme(theme, old_theme) {
}
}
themeMenuItemClicked(event) {
this.flipTheme();
event.stopPropagation();
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
} else {
//
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
goBack() {
this.router.navigate(['/home']);
if (!this.navigator) {
this.router.navigate(['/home']);
} else {
this.router.navigateByUrl(this.navigator);
}
}
openSettingsDialog() {
if (this.settingsPinRequired) {
this.openPinDialog();
} else {
this.actuallyOpenSettingsDialog();
}
}
actuallyOpenSettingsDialog() {
const dialogRef = this.dialog.open(SettingsComponent, {
width: '80vw'
});
}
openPinDialog() {
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.actuallyOpenSettingsDialog();
}
});
}
openAboutDialog() {
const dialogRef = this.dialog.open(AboutDialogComponent, {
width: '80vw'
});
}
openProfileDialog() {
const dialogRef = this.dialog.open(UserProfileDialogComponent, {
width: '60vw'
});
}
}

View File

@@ -1,33 +1,80 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
MatProgressBarModule, MatExpansionModule,
MatProgressSpinnerModule,
MatButtonToggleModule,
MatDialogModule} from '@angular/material';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, LOCALE_ID } from '@angular/core';
import { registerLocaleData, CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { HttpModule } from '@angular/http';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { PostsService } from 'app/posts.services';
import { FileCardComponent } from './file-card/file-card.component';
import {RouterModule} from '@angular/router';
import { RouterModule } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { MainComponent } from './main/main.component';
import { PlayerComponent } from './player/player.component';
import {VgCoreModule} from 'videogular2/compiled/core';
import {VgControlsModule} from 'videogular2/compiled/controls';
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
import {VgBufferingModule} from 'videogular2/compiled/buffering';
import { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
import { InputDialogComponent } from './input-dialog/input-dialog.component';
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
import { NgxContentLoadingModule } from 'ngx-content-loading';
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
import { DownloadItemComponent } from './download-item/download-item.component';
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component';
import { SubscriptionComponent } from './subscription//subscription/subscription.component';
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
import { SettingsComponent } from './settings/settings.component';
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
import { MatChipsModule } from '@angular/material/chips';
import { NgxFileDropModule } from 'ngx-file-drop';
import es from '@angular/common/locales/es';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
import { VideoInfoDialogComponent } from './dialogs/video-info-dialog/video-info-dialog.component';
import { ArgModifierDialogComponent, HighlightPipe } from './dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
import { UpdaterComponent } from './updater/updater.component';
import { UpdateProgressDialogComponent } from './dialogs/update-progress-dialog/update-progress-dialog.component';
import { ShareMediaDialogComponent } from './dialogs/share-media-dialog/share-media-dialog.component';
import { LoginComponent } from './components/login/login.component';
import { DownloadsComponent } from './components/downloads/downloads.component';
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
import { ModifyUsersComponent } from './components/modify-users/modify-users.component';
import { AddUserDialogComponent } from './dialogs/add-user-dialog/add-user-dialog.component';
import { ManageUserComponent } from './components/manage-user/manage-user.component';
import { ManageRoleComponent } from './components/manage-role/manage-role.component';
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
registerLocaleData(es, 'es');
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
@@ -41,9 +88,35 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
PlayerComponent,
InputDialogComponent,
CreatePlaylistComponent,
DownloadItemComponent
DownloadItemComponent,
SubscriptionsComponent,
SubscribeDialogComponent,
SubscriptionComponent,
SubscriptionFileCardComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent,
AboutDialogComponent,
VideoInfoDialogComponent,
ArgModifierDialogComponent,
HighlightPipe,
UpdaterComponent,
UpdateProgressDialogComponent,
ShareMediaDialogComponent,
LoginComponent,
DownloadsComponent,
UserProfileDialogComponent,
SetDefaultAdminDialogComponent,
ModifyUsersComponent,
AddUserDialogComponent,
ManageUserComponent,
ManageRoleComponent,
CookiesUploaderDialogComponent,
LogsViewerComponent,
ModifyPlaylistComponent
],
imports: [
CommonModule,
BrowserModule,
BrowserAnimationsModule,
MatNativeDateModule,
@@ -52,7 +125,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatInputModule,
MatSelectModule,
ReactiveFormsModule,
HttpModule,
HttpClientModule,
MatToolbarModule,
MatCardModule,
@@ -67,22 +139,44 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatProgressBarModule,
MatProgressSpinnerModule,
MatButtonToggleModule,
MatRippleModule,
MatMenuModule,
MatDialogModule,
MatSlideToggleModule,
MatMenuModule,
MatAutocompleteModule,
MatTabsModule,
MatTooltipModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
MatChipsModule,
DragDropModule,
ClipboardModule,
NgxFileDropModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
VgBufferingModule,
LazyLoadImageModule.forRoot({ isVisible }),
NgxContentLoadingModule,
RouterModule,
AppRoutingModule,
],
entryComponents: [
InputDialogComponent,
CreatePlaylistComponent
CreatePlaylistComponent,
SubscribeDialogComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent
],
providers: [
PostsService
],
exports: [
HighlightPipe
],
providers: [PostsService],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,27 @@
<div style="padding: 20px;">
<div *ngFor="let session_downloads of downloads | keyvalue">
<ng-container *ngIf="keys(session_downloads.value).length > 0">
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container>&nbsp;{{session_downloads.key}}
<span *ngIf="session_downloads.key === postsService.session_id">&nbsp;<ng-container i18n="Current session">(current)</ng-container></span>
</h4>
<div class="container">
<div class="row">
<div *ngFor="let download of session_downloads.value | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
<mat-card *ngIf="download.value" class="mat-elevation-z3">
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads.key, download.value.uid)"></app-download-item>
</mat-card>
</div>
</div>
</div>
<div>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn">Clear all downloads</button>
</div>
</mat-card>
</ng-container>
</div>
<div *ngIf="downloads && !downloadsValid()">
<h4 style="text-align: center;" i18n="No downloads label">No downloads available!</h4>
</div>
</div>

View File

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

View File

@@ -0,0 +1,171 @@
import { Component, OnInit, ViewChildren, QueryList, ElementRef, OnDestroy } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
import { Router } from '@angular/router';
@Component({
selector: 'app-downloads',
templateUrl: './downloads.component.html',
styleUrls: ['./downloads.component.scss'],
animations: [
// nice stagger effect when showing existing elements
trigger('list', [
transition(':enter', [
// child animation selector + stagger
query('@items',
stagger(100, animateChild()), { optional: true }
)
]),
]),
trigger('items', [
// cubic-bezier for a tiny bouncing feel
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate('500ms cubic-bezier(.8,-0.6,0.2,1.5)',
style({ transform: 'scale(1)', opacity: 1 }))
]),
transition(':leave', [
style({ transform: 'scale(1)', opacity: 1, height: '*' }),
animate('1s cubic-bezier(.8,-0.6,0.2,1.5)',
style({ transform: 'scale(0.5)', opacity: 0, height: '0px', margin: '0px' }))
]),
])
],
})
export class DownloadsComponent implements OnInit, OnDestroy {
downloads_check_interval = 1000;
downloads = {};
interval_id = null;
keys = Object.keys;
valid_sessions_length = 0;
sort_downloads = (a, b) => {
const result = b.value.timestamp_start - a.value.timestamp_start;
return result;
}
constructor(public postsService: PostsService, private router: Router) { }
ngOnInit(): void {
this.getCurrentDownloads();
this.interval_id = setInterval(() => {
this.getCurrentDownloads();
}, this.downloads_check_interval);
this.postsService.service_initialized.subscribe(init => {
if (init) {
if (!this.postsService.config['Extra']['enable_downloads_manager']) {
this.router.navigate(['/home']);
}
}
});
}
ngOnDestroy() {
if (this.interval_id) { clearInterval(this.interval_id) }
}
getCurrentDownloads() {
this.postsService.getCurrentDownloads().subscribe(res => {
if (res['downloads']) {
this.assignNewValues(res['downloads']);
} else {
// failed to get downloads
}
});
}
clearDownload(session_id, download_uid) {
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
if (res['success']) {
// this.downloads = res['downloads'];
} else {
}
});
}
clearDownloads(session_id) {
this.postsService.clearDownloads(false, session_id).subscribe(res => {
if (res['success']) {
this.downloads = res['downloads'];
} else {
}
});
}
clearAllDownloads() {
this.postsService.clearDownloads(true).subscribe(res => {
if (res['success']) {
this.downloads = res['downloads'];
} else {
}
});
}
assignNewValues(new_downloads_by_session) {
const session_keys = Object.keys(new_downloads_by_session);
// remove missing session IDs
const current_session_ids = Object.keys(this.downloads);
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
for (const missing_session_id of missing_session_ids) {
delete this.downloads[missing_session_id];
}
// loop through sessions
for (let i = 0; i < session_keys.length; i++) {
const session_id = session_keys[i];
const session_downloads_by_id = new_downloads_by_session[session_id];
const session_download_ids = Object.keys(session_downloads_by_id);
if (this.downloads[session_id]) {
// remove missing download IDs
const current_download_ids = Object.keys(this.downloads[session_id]);
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
for (const missing_download_id of missing_download_ids) {
console.log('removing missing download id');
delete this.downloads[session_id][missing_download_id];
}
}
if (!this.downloads[session_id]) {
this.downloads[session_id] = session_downloads_by_id;
} else {
for (let j = 0; j < session_download_ids.length; j++) {
const download_id = session_download_ids[j];
const download = new_downloads_by_session[session_id][download_id]
if (!this.downloads[session_id][download_id]) {
this.downloads[session_id][download_id] = download;
} else {
const download_to_update = this.downloads[session_id][download_id];
download_to_update['percent_complete'] = download['percent_complete'];
download_to_update['complete'] = download['complete'];
download_to_update['timestamp_end'] = download['timestamp_end'];
download_to_update['downloading'] = download['downloading'];
download_to_update['error'] = download['error'];
}
}
}
}
}
downloadsValid() {
let valid = false;
const keys = this.keys(this.downloads);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = this.downloads[key];
if (this.keys(value).length > 0) {
valid = true;
break;
}
}
return valid;
}
}

View File

@@ -0,0 +1,39 @@
<mat-card class="login-card">
<mat-tab-group [(selectedIndex)]="selectedTabIndex">
<mat-tab label="Login">
<div style="margin-top: 10px;">
<mat-form-field>
<input [(ngModel)]="loginUsernameInput" matInput placeholder="User name">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input [(ngModel)]="loginPasswordInput" (keyup.enter)="login()" type="password" matInput placeholder="Password">
</mat-form-field>
</div>
<div style="margin-bottom: 10px; margin-top: 10px;">
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
</div>
</mat-tab>
<mat-tab *ngIf="registrationEnabled" label="Register">
<div style="margin-top: 10px;">
<mat-form-field>
<input [(ngModel)]="registrationUsernameInput" matInput placeholder="User name">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input [(ngModel)]="registrationPasswordInput" type="password" matInput placeholder="Password">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input [(ngModel)]="registrationPasswordConfirmationInput" type="password" matInput placeholder="Confirm Password">
</mat-form-field>
</div>
<div style="margin-bottom: 10px; margin-top: 10px;">
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
</div>
</mat-tab>
</mat-tab-group>
</mat-card>

View File

@@ -0,0 +1,6 @@
.login-card {
max-width: 600px;
width: 80%;
margin: 0 auto;
margin-top: 20px;
}

View File

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

View File

@@ -0,0 +1,115 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
selectedTabIndex = 0;
// login
loginUsernameInput = '';
loginPasswordInput = '';
loggingIn = false;
// registration
registrationEnabled = false;
registrationUsernameInput = '';
registrationPasswordInput = '';
registrationPasswordConfirmationInput = '';
registering = false;
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
ngOnInit(): void {
if (this.postsService.isLoggedIn) {
this.router.navigate(['/home']);
}
this.postsService.service_initialized.subscribe(init => {
if (init) {
if (!this.postsService.config['Advanced']['multi_user_mode']) {
this.router.navigate(['/home']);
}
this.registrationEnabled = this.postsService.config['Users'] && this.postsService.config['Users']['allow_registration'];
}
});
}
login() {
if (this.loginPasswordInput === '') {
return;
}
this.loggingIn = true;
this.postsService.login(this.loginUsernameInput, this.loginPasswordInput).subscribe(res => {
this.loggingIn = false;
if (res['token']) {
this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
} else {
this.openSnackBar('Login failed, unknown error.');
}
}, err => {
this.loggingIn = false;
const error_code = err.status;
if (error_code === 401) {
this.openSnackBar('User name or password is incorrect!');
} else if (error_code === 404) {
this.openSnackBar('Login failed, cannot connect to the server.');
} else {
this.openSnackBar('Login failed, unknown error.');
}
});
}
register() {
if (!this.registrationUsernameInput || this.registrationUsernameInput === '') {
this.openSnackBar('User name is required!');
return;
}
if (!this.registrationPasswordInput || this.registrationPasswordInput === '') {
this.openSnackBar('Password is required!');
return;
}
if (!this.registrationPasswordConfirmationInput || this.registrationPasswordConfirmationInput === '') {
this.openSnackBar('Password confirmation is required!');
return;
}
if (this.registrationPasswordInput !== this.registrationPasswordConfirmationInput) {
this.openSnackBar('Password confirmation is incorrect!');
return;
}
this.registering = true;
this.postsService.register(this.registrationUsernameInput, this.registrationPasswordInput).subscribe(res => {
this.registering = false;
if (res && res['user']) {
this.openSnackBar(`User ${res['user']['name']} successfully registered.`);
this.loginUsernameInput = res['user']['name'];
this.selectedTabIndex = 0;
} else {
this.openSnackBar('Failed to register user, unknown error.');
}
}, err => {
this.registering = false;
if (err && err.error && typeof err.error === 'string') {
this.openSnackBar(err.error);
} else {
console.log(err);
}
});
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -0,0 +1,21 @@
<div style="height: 100%">
<div *ngIf="logs_loading" style="position: absolute; top: 40%; left: 50%">
<mat-spinner [diameter]="32"></mat-spinner>
</div>
<textarea style="height: 275px" matInput readonly [(ngModel)]="logs" placeholder="Logs will appear here" i18n-placeholder="Logs placeholder"></textarea>
<div>
<button style="margin-top: 12px;" [cdkCopyToClipboard]="logs" (click)="copiedLogsToClipboard()" mat-flat-button color="primary"><ng-container i18n="Copy to clipboard button text">Copy to clipboard</ng-container></button>
<div style="float: right">
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>&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>
</div>
</div>

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,40 @@
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { PostsService } from '../../posts.services';
@Component({
selector: 'app-logs-viewer',
templateUrl: './logs-viewer.component.html',
styleUrls: ['./logs-viewer.component.scss']
})
export class LogsViewerComponent implements OnInit {
logs: string = null;
requested_lines = 50;
logs_loading = false;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getLogs();
}
getLogs() {
if (!this.logs) { this.logs_loading = true; } // only show loading spinner at the first load
this.postsService.getLogs(this.requested_lines !== 0 ? this.requested_lines : null).subscribe(res => {
this.logs_loading = false;
if (res['logs']) {
this.logs = res['logs'];
} else {
this.postsService.openSnackBar('Failed to retrieve logs!');
}
}, err => {
this.logs_loading = false;
console.error(err);
this.postsService.openSnackBar('Failed to retrieve logs!');
});
}
copiedLogsToClipboard() {
this.postsService.openSnackBar('Logs copied to clipboard!');
}
}

View File

@@ -0,0 +1,19 @@
<h4 *ngIf="role" mat-dialog-title><ng-container i18n="Manage role dialog title">Manage role</ng-container>&nbsp;-&nbsp;{{role.name}}</h4>
<mat-dialog-content *ngIf="role">
<mat-list>
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
<span matLine>
<mat-radio-group [disabled]="permission === 'settings' && role.name === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
</span>
</mat-list-item>
</mat-list>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -0,0 +1,4 @@
.mat-radio-button {
margin-right: 10px;
margin-top: 5px;
}

View File

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

View File

@@ -0,0 +1,61 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-manage-role',
templateUrl: './manage-role.component.html',
styleUrls: ['./manage-role.component.scss']
})
export class ManageRoleComponent implements OnInit {
role = null;
available_permissions = null;
permissions = null;
permissionToLabel = {
'filemanager': 'File manager',
'settings': 'Settings access',
'subscriptions': 'Subscriptions',
'sharing': 'Share files',
'advanced_download': 'Use advanced download mode',
'downloads_manager': 'Use downloads manager'
}
constructor(public postsService: PostsService, private dialogRef: MatDialogRef<ManageRoleComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
if (this.data) {
this.role = this.data.role;
this.available_permissions = this.postsService.available_permissions;
this.parsePermissions();
}
}
ngOnInit(): void {
}
parsePermissions() {
this.permissions = {};
for (let i = 0; i < this.available_permissions.length; i++) {
const permission = this.available_permissions[i];
if (this.role.permissions.includes(permission)) {
this.permissions[permission] = 'yes';
} else {
this.permissions[permission] = 'no';
}
}
}
changeRolePermissions(change, permission) {
this.postsService.setRolePermission(this.role.name, permission, change.value).subscribe(res => {
if (res['success']) {
} else {
this.permissions[permission] = this.permissions[permission] === 'yes' ? 'no' : 'yes';
}
}, err => {
this.permissions[permission] = this.permissions[permission] === 'yes' ? 'no' : 'yes';
});
}
}

View File

@@ -0,0 +1,31 @@
<h4 *ngIf="user" mat-dialog-title><ng-container i18n="Manage user dialog title">Manage user</ng-container>&nbsp;-&nbsp;{{user.name}}</h4>
<mat-dialog-content *ngIf="user">
<p><ng-container i18n="User UID">User UID:</ng-container>&nbsp;{{user.uid}}</p>
<div>
<mat-form-field style="margin-right: 15px;">
<input matInput [(ngModel)]="newPasswordInput" type="password" placeholder="New password" i18n-placeholder="New password placeholder">
</mat-form-field>
<button mat-raised-button color="accent" (click)="setNewPassword()" [disabled]="newPasswordInput.length === 0"><ng-container i18n="Set new password">Set new password</ng-container></button>
</div>
<div>
<mat-list>
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
<span matLine>
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
<mat-radio-button value="default"><ng-container i18n="Use default">Use default</ng-container></mat-radio-button>
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
</span>
</mat-list-item>
</mat-list>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -0,0 +1,4 @@
.mat-radio-button {
margin-right: 10px;
margin-top: 5px;
}

View File

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

View File

@@ -0,0 +1,69 @@
import { Component, OnInit, Inject } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-manage-user',
templateUrl: './manage-user.component.html',
styleUrls: ['./manage-user.component.scss']
})
export class ManageUserComponent implements OnInit {
user = null;
newPasswordInput = '';
available_permissions = null;
permissions = null;
permissionToLabel = {
'filemanager': 'File manager',
'settings': 'Settings access',
'subscriptions': 'Subscriptions',
'sharing': 'Share files',
'advanced_download': 'Use advanced download mode',
'downloads_manager': 'Use downloads manager'
}
settingNewPassword = false;
constructor(public postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: any) {
if (this.data) {
this.user = this.data.user;
this.available_permissions = this.postsService.available_permissions;
this.parsePermissions();
}
}
ngOnInit(): void {
}
parsePermissions() {
this.permissions = {};
for (let i = 0; i < this.available_permissions.length; i++) {
const permission = this.available_permissions[i];
if (this.user.permission_overrides.includes(permission)) {
if (this.user.permissions.includes(permission)) {
this.permissions[permission] = 'yes';
} else {
this.permissions[permission] = 'no';
}
} else {
this.permissions[permission] = 'default';
}
}
}
changeUserPermissions(change, permission) {
this.postsService.setUserPermission(this.user.uid, permission, change.value).subscribe(res => {
// console.log(res);
});
}
setNewPassword() {
this.settingNewPassword = true;
this.postsService.changeUserPassword(this.user.uid, this.newPasswordInput).subscribe(res => {
this.newPasswordInput = '';
this.settingNewPassword = false;
});
}
}

View File

@@ -0,0 +1,107 @@
<div *ngIf="dataSource; else loading">
<div style="padding: 15px">
<div class="row">
<div class="table table-responsive px-5 pb-4 pt-2">
<div class="example-header">
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search">
</mat-form-field>
</div>
<div class="example-container mat-elevation-z8">
<mat-table #table [dataSource]="dataSource" matSort>
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Username users table header"> User name </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingname">
<span style="width: 80%;">
<mat-form-field>
<input matInput [(ngModel)]="constructedObject['name']" type="text" style="font-size: 12px">
</mat-form-field>
</span>
</span>
<ng-template #noteditingname>
{{row.name}}
</ng-template>
</mat-cell>
</ng-container>
<!-- Email Column -->
<ng-container matColumnDef="role">
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Role users table header"> Role </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingemail">
<span style="width: 80%;">
<mat-form-field>
<mat-select [(ngModel)]="constructedObject['role']">
<mat-option value="admin">Admin</mat-option>
<mat-option value="user">User</mat-option>
</mat-select>
</mat-form-field>
</span>
</span>
<ng-template #noteditingemail>
{{row.role}}
</ng-template>
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
<span *ngIf="editObject && editObject.uid === row.uid; else notediting">
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Finish editing user">
<mat-icon>done</mat-icon>
</button>
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel editing user">
<mat-icon>cancel</mat-icon>
</button>
</span>
<ng-template #notediting>
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user">
<mat-icon>edit</mat-icon>
</button>
</ng-template>
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;">
</mat-row>
</mat-table>
<mat-paginator #paginator [length]="length"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions">
</mat-paginator>
<button color="primary" [disabled]="!this.users" mat-raised-button (click)="openAddUserDialog()" style="float: left; top: -45px; left: 15px">
<ng-container i18n="Add users button">Add Users</ng-container>
</button>
</div>
</div>
</div>
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
<mat-menu #edit_roles_menu="matMenu">
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.name}}</button>
</mat-menu>
</div>
</div>
<div style="position: absolute" class="centered">
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</div>

View File

@@ -0,0 +1,5 @@
.edit-role {
position: relative;
top: -50px;
left: 35px;
}

View File

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

View File

@@ -0,0 +1,219 @@
import { Component, OnInit, Input, ViewChild, AfterViewInit } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { PostsService } from 'app/posts.services';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AddUserDialogComponent } from 'app/dialogs/add-user-dialog/add-user-dialog.component';
import { ManageUserComponent } from '../manage-user/manage-user.component';
import { ManageRoleComponent } from '../manage-role/manage-role.component';
@Component({
selector: 'app-modify-users',
templateUrl: './modify-users.component.html',
styleUrls: ['./modify-users.component.scss']
})
export class ModifyUsersComponent implements OnInit, AfterViewInit {
displayedColumns = ['name', 'role', 'actions'];
dataSource = new MatTableDataSource();
deleteDialogContentSubstring = 'Are you sure you want delete user ';
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
// MatPaginator Inputs
length = 100;
@Input() pageSize = 5;
pageSizeOptions: number[] = [5, 10, 25, 100];
// MatPaginator Output
pageEvent: PageEvent;
users: any;
editObject = null;
constructedObject = {};
roles = null;
constructor(public postsService: PostsService, public snackBar: MatSnackBar, public dialog: MatDialog,
private dialogRef: MatDialogRef<ModifyUsersComponent>) { }
ngOnInit() {
this.getArray();
this.getRoles();
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
/**
* Set the paginator and sort after the view init since this component will
* be able to query its view for the initialized paginator and sort.
*/
afterGetData() {
this.dataSource.sort = this.sort;
}
setPageSizeOptions(setPageSizeOptionsInput: string) {
this.pageSizeOptions = setPageSizeOptionsInput.split(',').map(str => +str);
}
applyFilter(filterValue: string) {
filterValue = filterValue.trim(); // Remove whitespace
filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches
this.dataSource.filter = filterValue;
}
private getArray() {
this.postsService.getUsers().subscribe(res => {
this.users = res['users'];
this.createAndSortData();
this.afterGetData();
});
}
getRoles() {
this.postsService.getRoles().subscribe(res => {
this.roles = [];
const roles = res['roles'];
const role_names = Object.keys(roles);
for (let i = 0; i < role_names.length; i++) {
const role_name = role_names[i];
this.roles.push({
name: role_name,
permissions: roles[role_name]['permissions']
});
}
});
}
openAddUserDialog() {
const dialogRef = this.dialog.open(AddUserDialogComponent);
dialogRef.afterClosed().subscribe(user => {
if (user && !user.error) {
this.openSnackBar('Successfully added user ' + user.name);
this.getArray();
} else if (user && user.error) {
this.openSnackBar('Failed to add user');
}
});
}
finishEditing(user_uid) {
let has_finished = false;
if (this.constructedObject && this.constructedObject['name'] && this.constructedObject['role']) {
if (!isEmptyOrSpaces(this.constructedObject['name']) && !isEmptyOrSpaces(this.constructedObject['role'])) {
has_finished = true;
const index_of_object = this.indexOfUser(user_uid);
this.users[index_of_object] = this.constructedObject;
this.constructedObject = {};
this.editObject = null;
this.setUser(this.users[index_of_object]);
this.createAndSortData();
}
}
}
enableEditMode(user_uid) {
if (this.uidInUserList(user_uid) && this.indexOfUser(user_uid) > -1) {
const users_index = this.indexOfUser(user_uid);
this.editObject = this.users[users_index];
this.constructedObject['name'] = this.users[users_index].name;
this.constructedObject['uid'] = this.users[users_index].uid;
this.constructedObject['role'] = this.users[users_index].role;
}
}
disableEditMode() {
this.editObject = null;
}
// checks if user is in users array by name
uidInUserList(user_uid) {
for (let i = 0; i < this.users.length; i++) {
if (this.users[i].uid === user_uid) {
return true;
}
}
return false;
}
// gets index of user in users array by name
indexOfUser(user_uid) {
for (let i = 0; i < this.users.length; i++) {
if (this.users[i].uid === user_uid) {
return i;
}
}
return -1;
}
setUser(change_obj) {
this.postsService.changeUser(change_obj).subscribe(res => {
this.getArray();
});
}
manageUser(user_uid) {
const index_of_object = this.indexOfUser(user_uid);
const user_obj = this.users[index_of_object];
this.dialog.open(ManageUserComponent, {
data: {
user: user_obj
},
width: '65vw'
});
}
removeUser(user_uid) {
this.postsService.deleteUser(user_uid).subscribe(res => {
this.getArray();
}, err => {
this.getArray();
});
}
createAndSortData() {
// Sorts the data by last finished
this.users.sort((a, b) => b.name > a.name);
const filteredData = [];
for (let i = 0; i < this.users.length; i++) {
filteredData.push(JSON.parse(JSON.stringify(this.users[i])));
}
// Assign the data to the data source for the table to render
this.dataSource.data = filteredData;
}
openModifyRole(role) {
const dialogRef = this.dialog.open(ManageRoleComponent, {
data: {
role: role
}
});
dialogRef.afterClosed().subscribe(success => {
this.getRoles();
});
}
closeDialog() {
this.dialogRef.close();
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}
function isEmptyOrSpaces(str){
return str === null || str.match(/^ *$/) !== null;
}

1
src/app/consts.ts Normal file
View File

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

View File

@@ -1,13 +1,14 @@
<h4 mat-dialog-title>Create a playlist</h4>
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
<form>
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" type="text" required aria-required [ngModelOptions]="{standalone: true}">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div>
<mat-form-field color="accent">
<mat-label>{{(type === 'audio') ? 'Audio files' : 'Videos'}}</mat-label>
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
</mat-select>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl } from '@angular/forms';
import { PostsService } from 'app/posts.services';

View File

@@ -0,0 +1,32 @@
<h4 style="position: relative" mat-dialog-title><ng-container i18n="About dialog title">About YoutubeDL-Material</ng-container>
<span class="logo-image">
<a [href]="projectLink" target="_blank">
<img style="width: 32px;" src="assets/images/GitHub-64px.png">
</a>
<img style="width: 32px; margin-left: 15px;" src="assets/images/logo_128px.png">
</span>
</h4>
<mat-dialog-content>
<div style="margin-bottom: 5px;">
<p>
<i>YoutubeDL-Material</i>&nbsp;<ng-container i18n="About first paragraph">is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos.</ng-container>
</p>
<p>
<i>YoutubeDL-Material</i>&nbsp;<ng-container i18n="About second paragraph">has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above.</ng-container>
</p>
<mat-divider></mat-divider>
<h5 style="margin-top: 10px;">Installation details:</h5>
<p>
<ng-container i18n="Version label">Installed version:</ng-container>&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>
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p>
<p>
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
</p>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close>Close</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,23 @@
i {
margin-right: 1px;
}
.version-spinner {
top: 4px;
margin-right: 5px;
margin-left: 5px;
display: inline-block;
}
.version-checked-icon {
top: 5px;
margin-left: 2px;
position: relative;
margin-right: -3px;
}
.logo-image {
position: absolute;
top: -10px;
right: -10px;
}

View File

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

View File

@@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { CURRENT_VERSION } from 'app/consts';
@Component({
selector: 'app-about-dialog',
templateUrl: './about-dialog.component.html',
styleUrls: ['./about-dialog.component.scss']
})
export class AboutDialogComponent implements OnInit {
projectLink = 'https://github.com/Tzahi12345/YoutubeDL-Material';
issuesLink = 'https://github.com/Tzahi12345/YoutubeDL-Material/issues';
latestUpdateLink = 'https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest'
latestGithubRelease = null;
checking_for_updates = true;
current_version_tag = CURRENT_VERSION;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getLatestGithubRelease()
}
getLatestGithubRelease() {
this.postsService.getLatestGithubRelease().subscribe(res => {
this.checking_for_updates = false;
this.latestGithubRelease = res;
});
}
}

Some files were not shown because too many files have changed in this diff Show More