Compare commits

...

335 Commits

Author SHA1 Message Date
Tzahi12345
cc5fd7e997 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into downloading-improvements 2020-05-27 21:42:19 -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
b7b77995b9 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into downloading-improvements 2020-05-20 10:34:31 -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
d7eb3f35ac Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into downloading-improvements 2020-05-09 04:43:56 -04:00
Tzahi12345
3487813cb5 Updated frontend files 2020-05-09 03:41:27 -04:00
Tzahi12345
0ea826487d minor improvements to downloading process, mainly scaffolding for future improvements 2020-05-09 03:37:50 -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
184 changed files with 25002 additions and 4482 deletions

21
.gitignore vendored
View File

@@ -43,10 +43,23 @@ 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/*
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/*

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/dist` folder. Drag those files into the `public` directory in the `backend` 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

18
backend/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM alpine:3.11
RUN \
apk add --no-cache npm python ffmpeg && \
apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
WORKDIR /app
COPY package.json /app/
RUN npm install
COPY ./ /app/
EXPOSE 17442
CMD [ "node", "app.js" ]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
{
"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": ""
},
"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,22 @@
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/"
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
},
"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 +34,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"
}
}
}

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.updatePlaylist = 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 audio files. Creating...');
fs.closeSync(fs.openSync(archive_path, 'w'));
}
}
}
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;
@@ -109,5 +160,66 @@ module.exports = {
setConfigItem: setConfigItem,
setConfigItems: setConfigItems,
getConfigFile: getConfigFile,
CONFIG_ITEMS: CONFIG_ITEMS
}
setConfigFile: setConfigFile,
configExistsCheck: configExistsCheck,
CONFIG_ITEMS: CONFIG_ITEMS,
initialize: initialize,
descriptors: {}
}
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": ""
},
"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,14 @@ 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'
},
// Extra
'ytdl_title_top': {
@@ -56,9 +62,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 +99,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 +140,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'
}

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

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,30 @@
"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",
"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,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.06b6262a0d981fd4885e.js" type="module"></script><script src="runtime-es5.06b6262a0d981fd4885e.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.0cbc545a4a3bee376826.js" type="module"></script><script src="main-es5.0cbc545a4a3bee376826.js" nomodule defer></script></body>
</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:"cc1ef452b2945b55327a"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

View File

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

File diff suppressed because one or more lines are too long

423
backend/subscriptions.js Normal file
View File

@@ -0,0 +1,423 @@
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');
const debugMode = process.env.YTDL_MODE === 'debug';
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);
}
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');
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
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
else
db.get('subscriptions').push(sub).write();
let success = await getSubscriptionInfo(sub, user_uid);
result_obj.success = success;
result_obj.sub = sub;
getVideosForSub(sub, user_uid);
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, 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');
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const appendedBasePath = getAppendedBasePath(sub, basePath);
const name = file;
let retrievedID = null;
return new Promise(resolve => {
let filePath = appendedBasePath;
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+'.mp4');
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath);
imageFileExists = fs.existsSync(imageFilePath);
if (jsonExists) {
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
fs.unlinkSync(jsonPath);
}
if (imageFileExists) {
fs.unlinkSync(imageFilePath);
}
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
if (sub.name) {
appendedBasePath = getAppendedBasePath(sub, basePath);
} else {
appendedBasePath = path.join(basePath, (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s'));
}
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-info-json', '--print-json'];
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)
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, reset_videos);
// TODO: Potentially store downloaded files in db?
}
resolve(true);
}
});
});
}
function handleOutputJSON(sub, sub_db, output_json, 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();
}
}
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

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

10255
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,42 @@
},
"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",
"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 +65,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,78 @@
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';
registerLocaleData(es, 'es');
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
@@ -41,9 +86,33 @@ 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
],
imports: [
CommonModule,
BrowserModule,
BrowserAnimationsModule,
MatNativeDateModule,
@@ -52,7 +121,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatInputModule,
MatSelectModule,
ReactiveFormsModule,
HttpModule,
HttpClientModule,
MatToolbarModule,
MatCardModule,
@@ -67,22 +135,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,24 @@
<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>
</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,150 @@
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 = a.value.timestamp_start < b.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);
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]) {
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,105 @@
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']);
}
}, err => {
this.loggingIn = false;
});
}
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 {
}
}, 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,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;
});
}
}

View File

@@ -0,0 +1,19 @@
<h4 mat-dialog-title i18n="Register user dialog title">Register a user</h4>
<mat-dialog-content>
<div>
<mat-form-field>
<input matInput placeholder="User name" i18n-placeholder="User name placeholder" [(ngModel)]="usernameInput">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput placeholder="Password" i18n-placeholder="Password placeholder" [(ngModel)]="passwordInput" type="password">
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button color="accent" (click)="createUser()" mat-raised-button><ng-container i18n="Register user button">Register</ng-container></button>
<button mat-dialog-close mat-button><ng-container i18n="Close button">Close</ng-container></button>
</mat-dialog-actions>

View File

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

View File

@@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-add-user-dialog',
templateUrl: './add-user-dialog.component.html',
styleUrls: ['./add-user-dialog.component.scss']
})
export class AddUserDialogComponent implements OnInit {
usernameInput = '';
passwordInput = '';
constructor(private postsService: PostsService, public dialogRef: MatDialogRef<AddUserDialogComponent>) { }
ngOnInit(): void {
}
createUser() {
this.postsService.register(this.usernameInput, this.passwordInput).subscribe(res => {
if (res['user']) {
this.dialogRef.close(res['user']);
} else {
this.dialogRef.close({error: 'Unknown error'});
}
}, err => {
this.dialogRef.close({error: err});
});
}
}

View File

@@ -0,0 +1,86 @@
<h4 i18n="Modify args title" mat-dialog-title>Modify youtube-dl args</h4>
<mat-dialog-content>
<div class="container">
<div class="row">
<div class="col-12">
<mat-card class="mat-elevation-z6">
<h6 i18n="Simulated args title">Simulated new args</h6>
<mat-chip-list class="example-chip" #chipList aria-label="Args array" cdkDropList cdkDropListDisabled
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)">
<mat-chip [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" *ngFor="let arg of args_array; let i = index;" [selectable]="selectable" [removable]="removable" (removed)="remove(i)" cdkDrag>
{{arg}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<mat-form-field style="width: 100%" color="accent">
<input #chipper style="width: 100%;" [formControl]="chipCtrl" matInput [matAutocomplete]="autochip" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event)">
</mat-form-field>
<mat-autocomplete #autochip="matAutocomplete">
<mat-option *ngFor="let arg of filteredChipOptions | async" [value]="arg.key">
<span [innerHTML]="arg.key | highlight : chipCtrl.value"></span>
<button style="float: right" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
</mat-option>
</mat-autocomplete>
</mat-card>
</div>
<div class="col-12">
<mat-card class="mat-elevation-z6 my-2">
<h6 i18n="Add arg card title">Add an arg</h6>
<form >
<div>
<mat-form-field style="width: 75%" color="accent">
<input matInput placeholder="Arg" [matAutocomplete]="auto" [formControl]="stateCtrl">
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let arg of filteredOptions | async" [value]="arg.key">
<span [innerHTML]="arg.key | highlight : stateCtrl.value"></span>
<button style="float: right" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
</mat-option>
</mat-autocomplete>
<div>
<mat-menu #argsByCategoryMenu="matMenu">
<ng-container *ngFor="let argsInCategory of argsByCategory | keyvalue">
<button mat-menu-item [matMenuTriggerFor]="subMenu">{{argsInfo[argsInCategory.key].label}}</button>
<mat-menu #subMenu="matMenu">
<button mat-menu-item *ngFor="let arg of argsInCategory.value" (click)="setFirstArg(arg.key)"><div style="display: inline-block;">{{arg.key}}</div>&nbsp;&nbsp;<div class="info-menu-icon"><mat-icon [matTooltip]="arg.description">info</mat-icon></div></button>
</mat-menu>
</ng-container>
</mat-menu>
<button style="margin-bottom: 15px" mat-stroked-button [matMenuTriggerFor]="argsByCategoryMenu"><ng-container i18n="Search args by category button">Search by category</ng-container></button>
</div>
</div>
<div>
<mat-checkbox color="accent" [ngModelOptions]="{standalone: true}" [(ngModel)]="secondArgEnabled"><ng-container i18n="Use arg value checkbox">Use arg value</ng-container></mat-checkbox>
</div>
<div *ngIf="secondArgEnabled">
<mat-form-field style="width: 75%" color="accent">
<input [ngModelOptions]="{standalone: true}" matInput [disabled]="!secondArgEnabled" placeholder="Arg value" i18n-placeholder="Arg value placeholder" [(ngModel)]="secondArg">
</mat-form-field>
</div>
</form>
<div>
<button (click)="addArg()" [disabled]="!canAddArg()" mat-stroked-button color="accent"><ng-container i18n="Search args by category button">Add arg</ng-container></button>
</div>
</mat-card>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button [mat-dialog-close]="null"><ng-container i18n="Arg modifier cancel button">Cancel</ng-container></button>
<button mat-button color="accent" [mat-dialog-close]="modified_args"><ng-container i18n="Arg modifier modify button">Modify</ng-container></button>
</mat-dialog-actions>

View File

@@ -0,0 +1,3 @@
.info-menu-icon {
float: right;
}

View File

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

View File

@@ -0,0 +1,195 @@
import { Component, OnInit, Inject, Pipe, PipeTransform, ViewChild, AfterViewInit } from '@angular/core';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { FormControl } from '@angular/forms';
import { args, args_info } from './youtubedl_args';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators/map';
import { startWith } from 'rxjs/operators/startWith';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
@Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform {
transform(text: string, search): string {
const pattern = search ? search
.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&')
.split(' ')
.filter(t => t.length > 0)
.join('|') : undefined;
const regex = new RegExp(pattern, 'gi');
return search ? text.replace(regex, match => `<b>${match}</b>`) : text;
}
};
@Component({
selector: 'app-arg-modifier-dialog',
templateUrl: './arg-modifier-dialog.component.html',
providers: [HighlightPipe],
styleUrls: ['./arg-modifier-dialog.component.scss'],
})
export class ArgModifierDialogComponent implements OnInit, AfterViewInit {
myGroup = new FormControl();
firstArg = '';
secondArg = '';
secondArgEnabled = false;
modified_args = '';
stateCtrl = new FormControl();
chipCtrl = new FormControl();
availableArgs = null;
argsByCategory = null;
argsByKey = null;
argsInfo = null;
filteredOptions: Observable<any>;
filteredChipOptions: Observable<any>;
// chip list
chipInput = '';
visible = true;
selectable = true;
removable = true;
addOnBlur = false;
args_array = null;
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
@ViewChild( 'chipper', {read: MatAutocompleteTrigger}) autoTrigger: MatAutocompleteTrigger;
static forRoot() {
return {
ngModule: ArgModifierDialogComponent,
providers: [],
};
}
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ArgModifierDialogComponent>,
private dialog: MatDialog) { }
ngOnInit(): void {
if (this.data) {
this.modified_args = this.data.initial_args;
this.generateArgsArray();
}
this.getAllPossibleArgs();
// autocomplete setup
this.filteredOptions = this.stateCtrl.valueChanges
.pipe(
startWith(''),
map(val => this.filter(val))
);
this.filteredChipOptions = this.chipCtrl.valueChanges
.pipe(
startWith(''),
map(val => this.filter(val))
);
}
ngAfterViewInit() {
this.autoTrigger.panelClosingActions.subscribe( x => {
if (this.autoTrigger.activeOption) {
console.log(this.autoTrigger.activeOption.value)
this.chipCtrl.setValue(this.autoTrigger.activeOption.value)
}
} )
}
// autocomplete filter
filter(val) {
if (this.availableArgs) {
return this.availableArgs.filter(option =>
option.key.toLowerCase().includes(val.toLowerCase()));
}
}
addArg() {
if (!this.modified_args) {
this.modified_args = '';
}
// adds space
if (this.modified_args !== '') {
this.modified_args += ',,';
}
this.modified_args += this.stateCtrl.value + (this.secondArgEnabled ? ',,' + this.secondArg : '');
this.generateArgsArray();
}
canAddArg() {
return this.stateCtrl.value && this.stateCtrl.value !== '' && (!this.secondArgEnabled || (this.secondArg && this.secondArg !== ''));
}
getFirstArg() {
return new Promise(resolve => {
resolve(this.stateCtrl.value);
});
}
getValueAsync(val) {
return new Promise(resolve => {
resolve(val);
});
}
getAllPossibleArgs() {
const all_args = args;
const arg_arrays = Object.keys(all_args).map(function(key) {
return all_args[key];
});
// converts array of arrays to one array
const singular_arg_array = [].concat.apply([], arg_arrays);
const args_by_key = singular_arg_array.reduce((acc, curr) => {
acc[curr.key] = curr;
return acc;
}, {});
this.argsByKey = args_by_key;
this.availableArgs = singular_arg_array;
this.argsByCategory = all_args;
this.argsInfo = args_info;
}
setFirstArg(arg_key) {
this.stateCtrl.setValue(arg_key);
}
// chip list functions
add(event) {
const input = event.input;
const arg = event.value;
if (!arg || arg.trim().length === 0) {
return;
}
this.args_array.push(arg);
if (this.modified_args.length > 0) {
this.modified_args += ',,'
}
this.modified_args += arg;
if (input) { input.value = ''; }
}
remove(arg_index) {
this.args_array.splice(arg_index, 1);
this.modified_args = this.args_array.join(',,');
}
generateArgsArray() {
if (this.modified_args.trim().length === 0) {
this.args_array = [];
return;
}
this.args_array = this.modified_args.split(',,');
}
drop(event: CdkDragDrop<any>) {
moveItemInArray(this.args_array, event.previousIndex, event.currentIndex);
this.modified_args = this.args_array.join(',,');
}
}

View File

@@ -0,0 +1,230 @@
const uncategorized = [
{'key': '-h', 'alt': '--help', 'description': 'Print this help text and exit'},
{'key': '--version', 'description': 'Print program version and exit'},
{'key': '-U', 'alt': '--update', 'description': 'Update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed)'},
{'key': '-i', 'alt': '--ignore-errors', 'description': 'Continue on download errors, for example to skip unavailable videos in a playlist'},
{'key': '--abort-on-error', 'description': 'Abort downloading of further videos (in the playlist or the command line) if an error occurs'},
{'key': '--dump-user-agent', 'description': 'Display the current browser identification'},
{'key': '--list-extractors', 'description': 'List all supported extractors'},
{'key': '--extractor-descriptions', 'description': 'Output descriptions of all supported extractors'},
{'key': '--force-generic-extractor', 'description': 'Force extraction to use the generic extractor'},
{'key': '--default-search', 'description': 'Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for youtube-dl "large apple". Use the value "auto" to let youtube-dl guess ("auto_warning" to emit awarning when guessing). "error" just throws an error. The default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching.'},
{'key': '--ignore-config', 'description': 'Do not read configuration files. When given in the global configuration file /etc/youtube-dl.conf: Do not read the user configuration in ~/.config/youtube-dl/config (%APPDATA%/youtube-dl/config.txt on Windows)'},
{'key': '--config-location', 'description': 'Location of the configuration file; either the path to the config or its containing directory.'},
{'key': '--flat-playlist', 'description': 'Do not extract the videos of a playlist, only list them.'},
{'key': '--mark-watched', 'description': 'Mark videos watched (YouTube only)'},
{'key': '--no-mark-watched', 'description': 'Do not mark videos watched (YouTube only)'},
{'key': '--no-color', 'description': 'Do not emit color codes in output'}
];
const network = [
{'key': '--proxy', 'description': 'Use the specified HTTP/HTTPS/SOCKS proxy.To enable SOCKS proxy, specify a proper scheme. For example socks5://127.0.0.1:1080/. Pass in an empty string (--proxy "") for direct connection.'},
{'key': '--socket-timeout', 'description': 'Time to wait before giving up, in seconds'},
{'key': '--source-address', 'description': 'Client-side IP address to bind to'},
{'key': '-4', 'alt': '--force-ipv4', 'description': 'Make all connections via IPv4'},
{'key': '-6', 'alt': '--force-ipv6', 'description': 'Make all connections via IPv6'}
];
const geo_restriction = [
{'key': '--geo-verification-proxy', 'description': 'Use this proxy to verify the IP address for some geo-restricted sites. The default proxy specified by --proxy\', if the option is not present) is used for the actual downloading.'},
{'key': '--geo-bypass', 'description': 'Bypass geographic restriction via faking X-Forwarded-For HTTP header'},
{'key': '--no-geo-bypass', 'description': 'Do not bypass geographic restriction via faking X-Forwarded-For HTTP header'},
{'key': '--geo-bypass-country', 'description': 'Force bypass geographic restriction with explicitly provided two-letter ISO 3166-2 country code'},
{'key': '--geo-bypass-ip-block', 'description': 'Force bypass geographic restriction with explicitly provided IP block in CIDR notation'}
];
const video_selection = [
{'key': '--playlist-start', 'description': 'Playlist video to start at (default is 1)'},
{'key': '--playlist-end', 'description': 'Playlist video to end at (default is last)'},
{'key': '--playlist-items', 'description': 'Playlist video items to download. Specify indices of the videos in the playlist separated by commas like: "--playlist-items 1,2,5,8" if you want to download videos indexed 1, 2, 5, 8 in the playlist. You can specify range: "--playlist-items 1-3,7,10-13", it will download the videos at index 1, 2, 3, 7, 10, 11, 12 and 13.'},
{'key': '--match-title', 'description': 'Download only matching titles (regex orcaseless sub-string)'},
{'key': '--reject-title', 'description': 'Skip download for matching titles (regex orcaseless sub-string)'},
{'key': '--max-downloads', 'description': 'Abort after downloading NUMBER files'},
{'key': '--min-filesize', 'description': 'Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)'},
{'key': '--max-filesize', 'description': 'Do not download any videos larger than SIZE (e.g. 50k or 44.6m)'},
{'key': '--date', 'description': 'Download only videos uploaded in this date'},
{'key': '--datebefore', 'description': 'Download only videos uploaded on or before this date (i.e. inclusive)'},
{'key': '--dateafter', 'description': 'Download only videos uploaded on or after this date (i.e. inclusive)'},
{'key': '--min-views', 'description': 'Do not download any videos with less than COUNT views'},
{'key': '--max-views', 'description': 'Do not download any videos with more than COUNT views'},
{'key': '--match-filter', 'description': 'Generic video filter. Specify any key (seethe "OUTPUT TEMPLATE" for a list of available keys) to match if the key is present, !key to check if the key is not present, key > NUMBER (like "comment_count > 12", also works with >=, <, <=, !=, =) to compare against a number, key = \'LITERAL\' (like "uploader = \'Mike Smith\'", also works with !=) to match against a string literal and & to require multiple matches. Values which are not known are excluded unless you put a question mark (?) after the operator. For example, to only match videos that have been liked more than 100 times and disliked less than 50 times (or the dislike functionality is not available at the given service), but who also have a description, use --match-filter'},
{'key': '--no-playlist', 'description': 'Download only the video, if the URL refers to a video and a playlist.'},
{'key': '--yes-playlist', 'description': 'Download the playlist, if the URL refers to a video and a playlist.'},
{'key': '--age-limit', 'description': 'Download only videos suitable for the given age'},
{'key': '--download-archive', 'description': 'Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it.'},
{'key': '--include-ads', 'description': 'Download advertisements as well (experimental)'}
];
const download = [
{'key': '-r', 'alt': '--limit-rate', 'description': 'Maximum download rate in bytes per second(e.g. 50K or 4.2M)'},
{'key': '-R', 'alt': '--retries', 'description': 'Number of retries (default is 10), or "infinite".'},
{'key': '--fragment-retries', 'description': 'Number of retries for a fragment (default is 10), or "infinite" (DASH, hlsnative and ISM)'},
{'key': '--skip-unavailable-fragments', 'description': 'Skip unavailable fragments (DASH, hlsnative and ISM)'},
{'key': '--abort-on-unavailable-fragment', 'description': 'Abort downloading when some fragment is not available'},
{'key': '--keep-fragments', 'description': 'Keep downloaded fragments on disk after downloading is finished; fragments are erased by default'},
{'key': '--buffer-size', 'description': 'Size of download buffer (e.g. 1024 or 16K) (default is 1024)'},
{'key': '--no-resize-buffer', 'description': 'Do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value of SIZE.'},
{'key': '--http-chunk-size', 'description': 'Size of a chunk for chunk-based HTTP downloading (e.g. 10485760 or 10M) (default is disabled). May be useful for bypassing bandwidth throttling imposed by a webserver (experimental)'},
{'key': '--playlist-reverse', 'description': 'Download playlist videos in reverse order'},
{'key': '--playlist-random', 'description': 'Download playlist videos in random order'},
{'key': '--xattr-set-filesize', 'description': 'Set file xattribute ytdl.filesize with expected file size'},
{'key': '--hls-prefer-native', 'description': 'Use the native HLS downloader instead of ffmpeg'},
{'key': '--hls-prefer-ffmpeg', 'description': 'Use ffmpeg instead of the native HLS downloader'},
{'key': '--hls-use-mpegts', 'description': 'Use the mpegts container for HLS videos, allowing to play the video while downloading (some players may not be able to play it)'},
{'key': '--external-downloader', 'description': 'Use the specified external downloader. Currently supports aria2c,avconv,axel,curl,ffmpeg,httpie,wget'},
{'key': '--external-downloader-args'}
];
const filesystem = [
{'key': '-a', 'alt': '--batch-file', 'description': 'File containing URLs to download (\'-\' for stdin), one URL per line. Lines starting with \'#\', \';\' or \']\' are considered as comments and ignored.'},
{'key': '--id', 'description': 'Use only video ID in file name'},
{'key': '-o', 'alt': '--output', 'description': 'Output filename template, see the "OUTPUT TEMPLATE" for all the info'},
{'key': '--autonumber-start', 'description': 'Specify the start value for %(autonumber)s (default is 1)'},
{'key': '--restrict-filenames', 'description': 'Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames'},
{'key': '-w', 'alt': '--no-overwrites', 'description': 'Do not overwrite files'},
{'key': '-c', 'alt': '--continue', 'description': 'Force resume of partially downloaded files. By default, youtube-dl will resume downloads if possible.'},
{'key': '--no-continue', 'description': 'Do not resume partially downloaded files (restart from beginning)'},
{'key': '--no-part', 'description': 'Do not use .part files - write directlyinto output file'},
{'key': '--no-mtime', 'description': 'Do not use the Last-modified header to set the file modification time'},
{'key': '--write-description', 'description': 'Write video description to a .description file'},
{'key': '--write-info-json', 'description': 'Write video metadata to a .info.json file'},
{'key': '--write-annotations', 'description': 'Write video annotations to a.annotations.xml file'},
{'key': '--load-info-json', 'description': 'JSON file containing the video information (created with the "--write-info-json" option)'},
{'key': '--cookies', 'description': 'File to read cookies from and dump cookie jar in'},
{'key': '--cache-dir', 'description': 'Location in the file system where youtube-dl can store some downloaded information permanently. By default $XDG_CACHE_HOME/youtube-dl or ~/.cache/youtube-dl . At the moment, only YouTube player files (for videos with obfuscated signatures) are cached, but that may change.'},
{'key': '--no-cache-dir', 'description': 'Disable filesystem caching'},
{'key': '--rm-cache-dir', 'description': 'Delete all filesystem cache files'}
];
const thumbnail = [
{'key': '--write-thumbnail', 'description': 'Write thumbnail image to disk'},
{'key': '--write-all-thumbnails', 'description': 'Write all thumbnail image formats to disk'},
{'key': '--list-thumbnails', 'description': 'Simulate and list all available thumbnail formats'}
];
const verbosity = [
{'key': '-q', 'alt': '--quiet', 'description': 'Activate quiet mode'},
{'key': '--no-warnings', 'description': 'Ignore warnings'},
{'key': '-s', 'alt': '--simulate', 'description': 'Do not download the video and do not writeanything to disk'},
{'key': '--skip-download', 'description': 'Do not download the video'},
{'key': '-g', 'alt': '--get-url', 'description': 'Simulate, quiet but print URL'},
{'key': '-e', 'alt': '--get-title', 'description': 'Simulate, quiet but print title'},
{'key': '--get-id', 'description': 'Simulate, quiet but print id'},
{'key': '--get-thumbnail', 'description': 'Simulate, quiet but print thumbnail URL'},
{'key': '--get-description', 'description': 'Simulate, quiet but print video description'},
{'key': '--get-duration', 'description': 'Simulate, quiet but print video length'},
{'key': '--get-filename', 'description': 'Simulate, quiet but print output filename'},
{'key': '--get-format', 'description': 'Simulate, quiet but print output format'},
{'key': '-j', 'alt': '--dump-json', 'description': 'Simulate, quiet but print JSON information. See the "OUTPUT TEMPLATE" for a description of available keys.'},
{'key': '-J', 'alt': '--dump-single-json', 'description': 'Simulate, quiet but print JSON information for each command-line argument. If the URL refers to a playlist, dump the whole playlist information in a single line.'},
{'key': '--print-json', 'description': 'Be quiet and print the video information as JSON (video is still being downloaded).'},
{'key': '--newline', 'description': 'Output progress bar as new lines'},
{'key': '--no-progress', 'description': 'Do not print progress bar'},
{'key': '--console-title', 'description': 'Display progress in console title bar'},
{'key': '-v', 'alt': '--verbose', 'description': 'Print various debugging information'},
{'key': '--dump-pages', 'description': 'Print downloaded pages encoded using base64 to debug problems (very verbose)'},
{'key': '--write-pages', 'description': 'Write downloaded intermediary pages to files in the current directory to debug problems'},
{'key': '--print-traffic', 'description': 'Display sent and read HTTP traffic'},
{'key': '-C', 'alt': '--call-home', 'description': 'Contact the youtube-dl server for debugging'},
{'key': '--no-call-home', 'description': 'Do NOT contact the youtube-dl server for debugging'}
];
const workarounds = [
{'key': '--encoding', 'description': 'Force the specified encoding (experimental)'},
{'key': '--no-check-certificate', 'description': 'Suppress HTTPS certificate validation'},
{'key': '--prefer-insecure', 'description': 'Use an unencrypted connection to retrieve information about the video. (Currently supported only for YouTube)'},
{'key': '--user-agent', 'description': 'Specify a custom user agent'},
{'key': '--referer', 'description': 'Specify a custom referer, use if the video access is restricted to one domain'},
{'key': '--add-header', 'description': 'Specify a custom HTTP header and its value, separated by a colon \':\'. You can use this option multiple times'},
{'key': '--bidi-workaround', 'description': 'Work around terminals that lack bidirectional text support. Requires bidiv or fribidi executable in PATH'},
{'key': '--sleep-interval', 'description': 'Number of seconds to sleep before each download when used alone or a lower boundof a range for randomized sleep before each download (minimum possible number of seconds to sleep) when used along with --max-sleep-interval'},
{'key': '--max-sleep-interval', 'description': 'Upper bound of a range for randomized sleep before each download (maximum possible number of seconds to sleep). Must only beused along with --min-sleep-interval'}
]
const video_format = [
{'key': '-f', 'alt': '--format', 'description': 'Video format code, see the "FORMAT SELECTION" for all the info'},
{'key': '--all-formats', 'description': 'Download all available video formats'},
{'key': '--prefer-free-formats', 'description': 'Prefer free video formats unless a specific one is requested'},
{'key': '-F', 'alt': '--list-formats', 'description': 'List all available formats of requested videos'},
{'key': '--youtube-skip-dash-manifest', 'description': 'Do not download the DASH manifests and related data on YouTube videos'},
{'key': '--merge-output-format', 'description': 'If a merge is required (e.g. bestvideo+bestaudio), output to given container format. One of mkv, mp4, ogg, webm, flv. Ignored if no merge is required'}
];
const subtitle = [
{'key': '--write-sub', 'description': 'Write subtitle file'},
{'key': '--write-auto-sub', 'description': 'Write automatically generated subtitle file (YouTube only)'},
{'key': '--all-subs', 'description': 'Download all the available subtitles of the video'},
{'key': '--list-subs', 'description': 'List all available subtitles for the video'},
{'key': '--sub-format', 'description': 'Subtitle format, accepts formats preference, for example: "srt" or "ass/srt/best"'},
{'key': '--sub-lang', 'description': 'Languages of the subtitles to download (optional) separated by commas, use --list-subs'}
];
const authentication = [
{'key': '-u', 'alt': '--username', 'description': 'Login with this account ID'},
{'key': '-p', 'alt': '--password', 'description': 'Account password. If this option is left out, youtube-dl will ask interactively.'},
{'key': '-2', 'alt': '--twofactor', 'description': 'Two-factor authentication code'},
{'key': '-n', 'alt': '--netrc', 'description': 'Use .netrc authentication data'},
{'key': '--video-password', 'description': 'Video password (vimeo, smotri, youku)'}
];
const adobe_pass = [
{'key': '--ap-mso', 'description': 'Adobe Pass multiple-system operator (TV provider) identifier, use --ap-list-mso'},
{'key': '--ap-username', 'description': 'Multiple-system operator account login'},
{'key': '--ap-password', 'description': 'Multiple-system operator account password. If this option is left out, youtube-dl will ask interactively.'},
{'key': '--ap-list-mso', 'description': 'List all supported multiple-system operators'}
];
const post_processing = [
{'key': '-x', 'alt': '--extract-audio', 'description': 'Convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)'},
{'key': '--audio-format', 'description': 'Specify audio format: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "best" by default; No effect without -x'},
{'key': '--audio-quality', 'description': 'Specify ffmpeg/avconv audio quality, insert a value between 0 (better) and 9 (worse)for VBR or a specific bitrate like 128K (default 5)'},
{'key': '--recode-video', 'description': 'Encode the video to another format if necessary (currently supported:mp4|flv|ogg|webm|mkv|avi)'},
{'key': '--postprocessor-args', 'description': 'Give these arguments to the postprocessor'},
{'key': '-k', 'alt': '--keep-video', 'description': 'Keep the video file on disk after the post-processing; the video is erased by default'},
{'key': '--no-post-overwrites', 'description': 'Do not overwrite post-processed files; the post-processed files are overwritten by default'},
{'key': '--embed-subs', 'description': 'Embed subtitles in the video (only for mp4,webm and mkv videos)'},
{'key': '--embed-thumbnail', 'description': 'Embed thumbnail in the audio as cover art'},
{'key': '--add-metadata', 'description': 'Write metadata to the video file'},
{'key': '--metadata-from-title', 'description': 'Parse additional metadata like song title/artist from the video title. The format syntax is the same as --output'},
{'key': '--xattrs', 'description': 'Write metadata to the video file\'s xattrs (using dublin core and xdg standards)'},
{'key': '--fixup', 'description': 'Automatically correct known faults of the file. One of never (do nothing), warn (only emit a warning), detect_or_warn (the default; fix file if we can, warn otherwise)'},
{'key': '--prefer-avconv', 'description': 'Prefer avconv over ffmpeg for running the postprocessors'},
{'key': '--prefer-ffmpeg', 'description': 'Prefer ffmpeg over avconv for running the postprocessors (default)'},
{'key': '--ffmpeg-location', 'description': 'Location of the ffmpeg/avconv binary; either the path to the binary or its containing directory.'},
{'key': '--exec', 'description': 'Execute a command on the file after downloading, similar to find\'s -exec syntax. Example: --exec'},
{'key': '--convert-subs', 'description': 'Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)'}
];
export const args_info = {
'uncategorized' : {'label': 'Main'},
'network' : {'label': 'Network'},
'geo_restriction': {'label': 'Geo Restriction'},
'video_selection': {'label': 'Video Selection'},
'download' : {'label': 'Download'},
'filesystem' : {'label': 'Filesystem'},
'thumbnail' : {'label': 'Thumbnail'},
'verbosity' : {'label': 'Verbosity'},
'workarounds' : {'label': 'Workarounds'},
'video_format' : {'label': 'Video Format'},
'subtitle' : {'label': 'Subtitle'},
'authentication' : {'label': 'Authentication'},
'adobe_pass' : {'label': 'Adobe Pass'},
'post_processing': {'label': 'Post Processing'},
};
export const args = {
'uncategorized' : uncategorized,
'network' : network,
'geo_restriction': geo_restriction,
'video_selection': video_selection,
'download' : download,
'filesystem' : filesystem,
'thumbnail' : thumbnail,
'verbosity' : verbosity,
'workarounds' : workarounds,
'video_format' : video_format,
'subtitle' : subtitle,
'authentication' : authentication,
'adobe_pass' : adobe_pass,
'post_processing': post_processing
}

View File

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

View File

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

View File

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

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