Compare commits

...

409 Commits
v2.1 ... v4.0

Author SHA1 Message Date
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
Tzahi12345
a8d2e1d890 Merge pull request #12 from Tzahi12345/serve-nodejs
Serve frontend app through nodejs
2020-03-01 00:50:20 -05:00
Isaac Grynsztein
0511996b26 fixed margins on advanced mode UI and temporarily disabled youtube auth until youtube-dl fixes it
advanced mode inputs now get saved in cookies

fixed bug in UI where delete button was missing by making it more mobile-friendly
2020-03-01 00:48:22 -05:00
Isaac Grynsztein
f29a29bf2f fixed bug that prevented custom args from working 2020-03-01 00:46:42 -05:00
Isaac Grynsztein
24d4107311 Clarified old config in README 2020-02-29 04:31:23 -05:00
Isaac Grynsztein
a46f9c37c6 fixed bug where old config item was fetched 2020-02-29 04:31:06 -05:00
Isaac Grynsztein
2b1c68bad0 update docker-compose and dockerfile 2020-02-29 04:30:56 -05:00
Isaac Grynsztein
e2d23404ce removed unused variable 2020-02-29 04:30:34 -05:00
Isaac Grynsztein
db208ed55e Updated README to reflect changes in the config 2020-02-29 03:30:21 -05:00
Isaac Grynsztein
b87a9f1e2f fixed bug where playlist titles included their relative path 2020-02-29 03:08:02 -05:00
Tzahi12345
a1ac1e450d Merge pull request #11 from Tzahi12345/auth-params
Add the ability to use youtube authentication
2020-02-28 22:12:29 -05:00
Tzahi12345
6764ea6c3b Merge pull request #10 from Tzahi12345/url_params
Add URL params home page
2020-02-28 22:07:53 -05:00
Isaac Grynsztein
8a52020186 resized favicon 2020-02-28 22:05:16 -05:00
Isaac Grynsztein
695b836852 added url params on home page to auto download content
created chrome extension to facilitate this feature
2020-02-28 21:21:17 -05:00
Isaac Grynsztein
71d7c30032 updated backend to support youtube auth
frontend now support youtube auth as well
2020-02-28 20:09:59 -05:00
Isaac Grynsztein
5ca4f036c7 fixed bug where if multi mode was enabled, click on file card URLs didn't work 2020-02-28 00:32:33 -05:00
Isaac Grynsztein
1ffe61f01f removed path-base and updated docker-compose.yml & README 2020-02-28 00:20:08 -05:00
Isaac Grynsztein
5e331b9ffa config settings now just have url and port
fixed bug where multi download mode would not allow file card link clicking
2020-02-28 00:14:46 -05:00
Isaac Grynsztein
09bdae90e2 refactored code so node.js serves the angular app, and all the backend routes are prepended with /api/
nodejs now compressed requests
2020-02-27 22:52:50 -05:00
Isaac Grynsztein
37cc8f4fe1 fixed error in docker compose 2020-02-27 04:15:19 -05:00
Isaac Grynsztein
925e083b8d added new config settings to README 2020-02-27 04:12:27 -05:00
Isaac Grynsztein
6dc42278c4 updated docker-compose to new youtubedl-material version number 2020-02-27 03:57:41 -05:00
Isaac Grynsztein
12c227badb temporarily disabled advanced mode until further testing 2020-02-27 03:47:11 -05:00
Isaac Grynsztein
181a9f842c fixed bug where downloading files failed if the name had to be encoded 2020-02-27 03:46:57 -05:00
Isaac Grynsztein
b79d801c0f Added support for custom arguments and custom output patch 2020-02-27 03:27:57 -05:00
Isaac Grynsztein
fc3691336d added allow multi download mode setting frontend implementation 2020-02-27 01:10:23 -05:00
Isaac Grynsztein
bcd879ebc8 added multiple download support
lazy loaded images now reload after a new download
2020-02-27 01:06:32 -05:00
Isaac Grynsztein
b646db4828 Added the ability to cancel downloads
Audio only checkbox now disabled when downloading

Laid the groundwork for multiple simulataneous downloads
2020-02-26 19:04:02 -05:00
Isaac Grynsztein
426d52e359 fixed tabindex ordering of file cards (delete came before url) 2020-02-26 19:03:13 -05:00
Isaac Grynsztein
17199dd9c0 Updated readme 2020-02-26 18:11:37 -05:00
Isaac Grynsztein
c680c2827b reworded docker steps 2020-02-26 03:18:19 -05:00
Isaac Grynsztein
2dbf8d31f7 Updated docker info in readme 2020-02-26 03:17:29 -05:00
Isaac Grynsztein
a3753e557c Updated docker instructions 2020-02-26 03:16:42 -05:00
Isaac Grynsztein
ec80abdc8e Updated readme.md to include docker steps 2020-02-26 03:07:22 -05:00
Isaac Grynsztein
1ef7d24c22 downgraded required docker-compose.yml to 2 2020-02-26 01:35:21 -05:00
Isaac Grynsztein
4b6f6996ae fixed bug in docker during building 2020-02-26 01:28:46 -05:00
Isaac Grynsztein
c930ee94c5 added docker support
reworked backend to allow for containerization. config items can now be overwritten by environment vars

fixed bug during building

updated youtube-dl version in backend
2020-02-26 00:34:13 -05:00
Isaac Grynsztein
e88edbef5a Updated README to fix missing step 2020-02-24 16:22:54 -05:00
Isaac Grynsztein
ac13ed3359 updated package.json to remove bug during building 2020-02-24 16:18:09 -05:00
Isaac Grynsztein
faae0d44e6 make it better 2020-02-24 07:47:36 -05:00
Isaac Grynsztein
7d8ec04ad6 make it better 2020-02-24 07:39:00 -05:00
Isaac Grynsztein
8629e6ae9e make it better 2020-02-24 07:28:11 -05:00
Isaac Grynsztein
6e311d46a6 make it better 2020-02-24 06:41:13 -05:00
Isaac Grynsztein
006e983c14 make it better 2020-02-24 06:13:56 -05:00
Isaac Grynsztein
5db3e06a81 make it better 2020-02-24 06:05:24 -05:00
Isaac Grynsztein
2ced7b7f91 researching heroku support 2020-02-24 05:58:25 -05:00
Isaac Grynsztein
042baa418b updated .gitignore 2020-02-24 04:12:45 -05:00
Isaac Grynsztein
deb928da12 sorting and updating now only possible on favorited (saved) playlists
fixed compilation bug in app.module
2020-02-24 04:11:22 -05:00
Isaac Grynsztein
a7f5cc01d3 update youtube-dl binary 2020-02-24 03:51:27 -05:00
Isaac Grynsztein
414b6a26d9 backend playlist updating endpoint implemented
tomp3/tomp4 errors are now logged
2020-02-24 03:51:11 -05:00
Isaac Grynsztein
c069672e62 ngmodule drag and drop import commit 2020-02-24 03:50:29 -05:00
Isaac Grynsztein
167d9dafa2 added title to create playlist dialog 2020-02-24 03:50:10 -05:00
Isaac Grynsztein
9302084f60 playlists can now be rearranged and updated 2020-02-24 03:49:43 -05:00
Isaac Grynsztein
ac0199f596 iOS is now checked by the cdk platform component 2020-02-24 03:49:01 -05:00
Isaac Grynsztein
8e8ab7ac6c added min-height to app component 2020-02-23 22:30:09 -05:00
Isaac Grynsztein
f06c9ba44a fixed bug where non-themed white space that appeared when file manager was expanded 2020-02-23 22:29:42 -05:00
Isaac Grynsztein
6e593472d9 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-02-23 03:21:13 -05:00
Isaac Grynsztein
0bddbda36d updated favicon 2020-02-23 03:20:24 -05:00
Isaac Grynsztein
23feb05fab downloading agent is now the default of youtube-dl by default instead of aria2c. testing showed it performed better over multipled trials
added a setting to use aria2c optionally

added debug timing to getURLInfos
2020-02-23 03:20:07 -05:00
Isaac Grynsztein
a0eff4d96d images on file cards now load when the accordion is hovered over to increase responsiveness. images are loading maybe a second before clicking so hopefully they're done by the time the expansion finishes
added the ability to create playlists in the gui through a new dialog

reloading mp3s/mp4s doesn't cause an image refresh anymore when the list is unchanged

fixed loading spinner of available formats so it now only shows when it is loading the current url

file card images now don't show when errored or thumbnailurl doesn't exist
2020-02-23 03:18:26 -05:00
Isaac Grynsztein
b87b49d77b removed outline for video player 2020-02-23 03:13:09 -05:00
Tzahi12345
c05026aa15 Update README.md 2020-02-21 01:33:31 -05:00
Tzahi12345
883df63d2f Update README.md 2020-02-20 21:22:53 -05:00
Tzahi12345
da1d49b541 Update README.md 2020-02-20 21:22:25 -05:00
Isaac Grynsztein
393ed5a210 added skeleton code for future electron.js support
added font swap to google font call

simplified polyfills

updated backend package.json info
2020-02-20 18:35:09 -05:00
Isaac Grynsztein
54492b109a thumbnails now lazy load. when it is loading, a content loading gradient is shown in front of it
made file cards look better on mobile devices
2020-02-20 15:45:40 -05:00
Isaac Grynsztein
7eac88a31f removed debug logging 2020-02-20 15:44:44 -05:00
Isaac Grynsztein
8fec9639eb fixed bug where if no theme was selected, errors would fire 2020-02-20 14:30:05 -05:00
Isaac Grynsztein
a15e1f98fa fixed compilation error and cleaned up code in app component 2020-02-20 14:29:29 -05:00
Isaac Grynsztein
6604484765 updated styles.css to styles.scss in angular.json 2020-02-20 10:45:59 -05:00
Isaac Grynsztein
c58f8a4058 added theming support with 3 themes (only 2 selectable for now)
switched from css to scss default style system

cleaned up unused code in app component

upated youtube search results styling

downloading video from home screen now shows local progress bar under that video
2020-02-20 10:45:37 -05:00
Isaac Grynsztein
8545016f1d "audio only" checkbox is now remembered after page loads
removed videogular icons as it caused compilation errors
2020-02-19 02:45:05 -05:00
Isaac Grynsztein
9b1e84821e moved theme to internal file 2020-02-19 02:43:58 -05:00
Isaac Grynsztein
6505fad7bc added save button to player component and updated download button 2020-02-19 02:29:36 -05:00
Isaac Grynsztein
d245904c0d added the ability to save playlists
added local db system (lowdb)

playlists are now downloaded as a zip from the streaming menu
2020-02-19 02:29:10 -05:00
Isaac Grynsztein
0095ea1271 fixed bug where search results showed old results when search bar was empty 2020-02-18 18:00:39 -05:00
Isaac Grynsztein
b41d10f514 Added download button to player component 2020-02-18 17:29:34 -05:00
Isaac Grynsztein
8e3d6a0af6 Player compilation error fixed
removed debug statements in player component
2020-02-17 17:42:50 -05:00
Isaac Grynsztein
1e4995c5ce Fixed catch statements not having arguments on backend
Fixed backend location url not working when not in root dir on web server
2020-02-17 17:42:21 -05:00
Isaac Grynsztein
710e3613a8 removed debug statements 2020-02-17 00:40:23 -05:00
Isaac Grynsztein
28331c1037 Updated debug-only default.json to reflect the new options added 2020-02-17 00:37:41 -05:00
Isaac Grynsztein
202c0718b7 Updated debug launch.json to include frontend debugging 2020-02-17 00:37:06 -05:00
Isaac Grynsztein
a985963661 Adding updated bootstrap to index.html 2020-02-17 00:36:33 -05:00
Isaac Grynsztein
f673b325fd Added custom quality options to PostsService and the ability to do a URL info grab from the server
Video and audio streams now save the stream object in a "descriptors" variable which will give the server the ability to close them when the file needs to be deleted.
- without this, windows systems don't play nice with nodejs function fs.unlinkSync. A weird, but necessary workaround

deleting files is now done asynchronously, and success is now determined by whether they exist afterwards or not

Added backend function to get info for URLs

Modified tomp3 and tomp4 endpoint to support custom quality settings.
2020-02-17 00:36:15 -05:00
Isaac Grynsztein
5f4a7a1e69 Added support for custom quality settings for video and audio files.
Available formats are downloaded when a valid YT url is detected. These formats are then parsed and a best audio format is selected based on the results

After downloading a file with no file manager, file is now deleted. After file deletion, mp3/mp4 reload occurs

Updated view on main component to be more responsive, using bootstrap grid

Updated progress bar UI-wise to be more in line with the rest of the page
2020-02-17 00:32:31 -05:00
Isaac Grynsztein
d54b2a73c8 Updated view of player to be more responsive. Window width is now recorded for eventual use for responsiveness optimization 2020-02-17 00:28:49 -05:00
Isaac Grynsztein
8e7bb4ba3b added custom player
added routing with two routes: home and player

moved most of app component to main component. app component currently just manages the top toolbar
2020-02-15 02:13:21 -05:00
Isaac Grynsztein
d595de5786 added functions to get info on a downloaded (or downloading) file
bug fixed where videos with quotations would not properly stream
2020-02-15 02:11:21 -05:00
Isaac Grynsztein
31394fa98c updated mobile view for file cards to be more responsive
streamed audio/video now include the extension in the download

cleaned up unused code in app component
2020-02-14 00:17:51 -05:00
Isaac Grynsztein
af595d3df8 Added debug mode to server and relevant debug configurations
simplified youtubedl download process to speed up the download

queryurl not printed any longer by youtube search service
2020-02-14 00:15:55 -05:00
Isaac Grynsztein
81377a2b38 added youtube search functionality in frontend 2020-02-13 06:43:49 -05:00
Isaac Grynsztein
35bdd1deeb fixed file name paths on backend. backend also now tells frontend when the url provided is a playlist
frontend now does not get the file status and simply waits for the server to respond with the file

added methods to download audio/video files to simplify downloadHelperMp3/Mp4
2020-02-13 05:10:27 -05:00
Isaac Grynsztein
a1ec53edb9 preparing config for youtube search feature 2020-02-13 05:08:14 -05:00
Isaac Grynsztein
aa130d3fc9 updated youtube-dl version for nodejs 2020-02-13 03:16:34 -05:00
Isaac Grynsztein
0f0bf3a401 updated youtube-dl.exe binary for windows 2020-02-13 03:13:20 -05:00
Isaac Grynsztein
73b9c61080 renamed variable in backend
deleteaudiofile/deletevideofile functions now made for reusability

downloaded videos now use the title as the file name. this requires longer download times as 2 calls are created

created a deletefile http call in backend, however it is currently not being used
2020-02-13 03:13:10 -05:00
Isaac Grynsztein
501806909a fixed bug where going back to the page after entering a stream didn't allow downloading of new files
in download only mode, files are now auto deleted when saved
2020-02-13 03:10:52 -05:00
Isaac Grynsztein
77dd96b3b9 added download_only_mode to encryption configuration
reloading of mp3s/mp4s only happens if file manager is enabled
2020-02-11 16:36:59 -05:00
196 changed files with 38383 additions and 782 deletions

21
.gitignore vendored
View File

@@ -44,5 +44,22 @@ Thumbs.db
node_modules/*
backend/node_modules/*
YoutubeDL-Material/node_modules/*
backend/video/*
backend/audio/*
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/*

21
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach",
"port": 9229
},
{
"type": "chrome",
"request": "launch",
"name": "Launch chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
}
]
}

1
Procfile Normal file
View File

@@ -0,0 +1 @@
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,14 @@
# 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 5](https://angular.io/) for the frontend, and [Nodejs](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!
## Getting Started
@@ -8,43 +16,103 @@ 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/m3xozES.png)
![frontpage](https://i.imgur.com/w8iofbb.png)
With optional file management enabled (default):
![frontpage_with_files](https://i.imgur.com/z9vME2O.png)
![frontpage_with_files](https://i.imgur.com/FTATqBM.png)
Dark mode:
![dark_mode](https://i.imgur.com/r5ZtBqd.png)
### Prerequisites
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: ffmpeg, nodejs, python. If you don't, run this command:
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
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 ffmpeg nodejs python
sudo apt-get install nodejs youtube-dl
```
Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
### Installing
First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
Drag all the files in `youtubedl-material` to a location accessible to a web server. It works best if it's the root (usually right inside `public_html`. Once that's done, navigate to `backend` 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.
Port forward `17442` if you're going to access YoutubeDL-Material from out of your network. This is an important step. Make sure the configuration reflects this appropriately.
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
Once the configuration is done, type `sudo nodejs app.js`. This will run the backend server. On your browser, navigate to your installation folder. Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
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 `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.
## Deployment
### Configuration
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
| Config item | Description | Default |
| ------------- | ------------- | ------------- |
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
| port | Desired port for YoutubeDL-Material | "17442" |
| use-encryption | true if you intend to use SSL encryption (https) | false |
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
| path-audio | Path to audio folder for saved mp3s | "audio/" |
| path-video | Path to video folder for saved mp4s | "video/" |
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
| file_manager_enabled | true if you want to use the file manager | true |
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
| download_only_mode | true if you want files to directly download to the client with no media player | false |
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
| allow_advanced_download | true if you want to use the Advanced download options | false |
## Build it yourself
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
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 a web server, and drag the `backend` directory into the same folder. This folder should have `index.html` in it as well. If it does **not**, you're in the wrong directory.
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 `youtubedl-material/backend` folder, and type `sudo 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 `17442` and point it to the server's IP address. Make sure the port is also allowed through the 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/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",
@@ -24,7 +31,87 @@
"src/backend"
],
"styles": [
"src/styles.css"
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"es": {
"localize": ["es"]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "youtube-dl-material:build"
},
"configurations": {
"production": {
"browserTarget": "youtube-dl-material:build:production"
},
"es": {
"browserTarget": "youtube-dl-material:build:es"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "youtube-dl-material:build"
}
},
"serve-electron": {
"builder": "@angular-guru/electron-builder:dev-server",
"options": {
"browserTarget": "youtube-dl-material:build"
},
"configurations": {
"production": {
"browserTarget": "youtube-dl-material:build:production"
}
}
},
"electron": {
"builder": "@angular-guru/electron-builder:build",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "main.js",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico",
"src/backend/audio",
"src/backend/video",
"src/backend"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
@@ -48,23 +135,6 @@
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "youtube-dl-material:build"
},
"configurations": {
"production": {
"browserTarget": "youtube-dl-material:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "youtube-dl-material:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
@@ -74,7 +144,7 @@
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
"src/styles.css"
"src/styles.scss"
],
"assets": [
"src/assets",
@@ -125,7 +195,7 @@
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "css"
"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,55 @@
{
"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,
"logger_level": "info"
}
}
}

View File

@@ -0,0 +1,55 @@
{
"YoutubeDLMaterial": {
"Host": {
"url": "https://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": true,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
},
"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,
"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;
}
};

216
backend/config.js Normal file
View File

@@ -0,0 +1,216 @@
const fs = require('fs');
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
const debugMode = process.env.YTDL_MODE === 'debug';
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);
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);
}
}
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
Object.byString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}
function getParentPath(path) {
let elements = path.split('.');
elements.splice(elements.length - 1, 1);
return elements.join('.');
}
function getElementNameInConfig(path) {
let elements = path.split('.');
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 parsed_data = JSON.parse(raw_data);
return parsed_data;
} catch(e) {
logger.error('Failed to get config file');
return null;
}
}
function setConfigFile(config) {
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
return true;
} catch(e) {
return false;
}
}
function getConfigItem(key) {
let config_json = getConfigFile();
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);
};
function setConfigItem(key, value) {
let success = false;
let config_json = getConfigFile();
let path = CONFIG_ITEMS[key]['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;
};
function setConfigItems(items) {
let success = false;
let config_json = getConfigFile();
for (let i = 0; i < items.length; i++) {
let key = items[i].key;
let value = items[i].value;
// if boolean strings, set to booleans again
if (value === 'false' || value === 'true') {
value = (value === 'true');
}
let item_path = CONFIG_ITEMS[key]['path'];
let item_parent_path = getParentPath(item_path);
let item_element_name = getElementNameInConfig(item_path);
let item_parent_object = Object.byString(config_json, item_parent_path);
item_parent_object[item_element_name] = value;
}
success = setConfigFile(config_json);
return success;
}
module.exports = {
getConfigItem: getConfigItem,
setConfigItem: setConfigItem,
setConfigItems: setConfigItems,
getConfigFile: getConfigFile,
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,
"logger_level": "info"
}
}
}

View File

@@ -1,23 +0,0 @@
{
"YoutubeDLMaterial": {
"Host": {
"frontendurl": "http://example.com",
"backendurl": "http://example.com: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-base": "http://example.com:17442/",
"path-audio": "audio/",
"path-video": "video/"
},
"Extra": {
"title_top": "Youtube Downloader",
"download_only_mode": false,
"file_manager_enabled": true
}
}
}

View File

@@ -1,22 +0,0 @@
{
"YoutubeDLMaterial": {
"Host": {
"frontendurl": "https://example.com",
"backendurl": "https://example.com:17442/"
},
"Encryption": {
"use-encryption": true,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-base": "https://example.com:17442/",
"path-audio": "audio/",
"path-video": "video/"
},
"Extra": {
"title_top": "Youtube Downloader",
"file_manager_enabled": true
}
}
}

170
backend/consts.js Normal file
View File

@@ -0,0 +1,170 @@
let CONFIG_ITEMS = {
// Host
'ytdl_url': {
'key': 'ytdl_url',
'path': 'YoutubeDLMaterial.Host.url'
},
'ytdl_port': {
'key': 'ytdl_port',
'path': 'YoutubeDLMaterial.Host.port'
},
// Encryption
'ytdl_use_encryption': {
'key': 'ytdl_use_encryption',
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
},
'ytdl_cert_file_path': {
'key': 'ytdl_cert_file_path',
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
},
'ytdl_key_file_path': {
'key': 'ytdl_key_file_path',
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
},
// Downloader
'ytdl_audio_folder_path': {
'key': 'ytdl_audio_folder_path',
'path': 'YoutubeDLMaterial.Downloader.path-audio'
},
'ytdl_video_folder_path': {
'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': {
'key': 'ytdl_title_top',
'path': 'YoutubeDLMaterial.Extra.title_top'
},
'ytdl_file_manager_enabled': {
'key': 'ytdl_file_manager_enabled',
'path': 'YoutubeDLMaterial.Extra.file_manager_enabled'
},
'ytdl_allow_quality_select': {
'key': 'ytdl_allow_quality_select',
'path': 'YoutubeDLMaterial.Extra.allow_quality_select'
},
'ytdl_download_only_mode': {
'key': 'ytdl_download_only_mode',
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
},
'ytdl_allow_multi_download_mode': {
'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'
},
'ytdl_youtube_api_key': {
'key': 'ytdl_youtube_api_key',
'path': 'YoutubeDLMaterial.API.youtube_API_key'
},
// Themes
'ytdl_default_theme': {
'key': 'ytdl_default_theme',
'path': 'YoutubeDLMaterial.Themes.default_theme'
},
'ytdl_allow_theme_change': {
'key': 'ytdl_allow_theme_change',
'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',
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
},
'ytdl_custom_downloading_agent': {
'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_logger_level': {
'key': 'ytdl_logger_level',
'path': 'YoutubeDLMaterial.Advanced.logger_level'
}
};
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,14 @@
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
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:latest

2963
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,58 @@
{
"name": "backend",
"version": "1.0.0",
"description": "backend for hda",
"description": "backend for YoutubeDL-Material",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon -q app.js"
},
"nodemonConfig": {
"ignore": [
"*.js",
"appdata/*",
"public/*"
],
"watch": [
"restart.json"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/Tzahi12345/hda-backend.git"
"url": ""
},
"author": "Isaac Grynsztein",
"license": "MIT",
"bugs": {
"url": "https://github.com/Tzahi12345/hda-backend/issues"
"url": ""
},
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
"homepage": "",
"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",
"youtube-dl": "^2.0.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",
"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,791 @@
@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-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.f02e22ff7a15b4b69194.js" type="module"></script><script src="runtime-es5.f02e22ff7a15b4b69194.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.38c23f6efbbfa0797d6d.js" type="module"></script><script src="main-es5.38c23f6efbbfa0797d6d.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:"bd496f58ce4d813e549d"}[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:"bd496f58ce4d813e549d"}[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

391
backend/subscriptions.js Normal file
View File

@@ -0,0 +1,391 @@
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 = 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-annotations', '--write-thumbnail', '--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);
}
// get videos
logger.verbose('Subscribe: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (err) {
logger.error(err.stderr);
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;
}
if (sub.streamingOnly) {
if (i === 0) {
sub_db.assign({videos: []}).write();
}
// remove unnecessary info
output_json.formats = null;
// add to db
sub_db.get('videos').push(output_json).write();
}
// TODO: Potentially store downloaded files in db?
}
resolve(true);
}
});
});
}
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.

28
chrome-extension.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMX9Wk5SM5cIfY
6ReKX3ybY1rsbNbOzG8ceN7yyeXB0mor8pVsX1MOna2HewOyBuaaYNJRO4tJBxic
7a8zQErfgHL/i/QrVvVCpfJ7xKvq6zij5NYoqd/FBUwawqjeH5/voIcAp9z5Vmsr
kL0sxJUKy6b4IWNp3noU7Nvq2RwxnXQbKDhz8FrX6oQAnDC6gsG5a2OSPsaE4oqw
6nmonORJypmpP5hqyHY8ffXBT2lAxjHT7OnYbaCBe2TQP8+rH6rDBOhjVNtUJ089
ocTQL6LtQEPkcF4yKJmtcOwHl8OPGZs5l9i8xb4j9RuSPkm2lbzZX8sOsdGGoqJZ
q68nYhsHAgMBAAECggEAXmtKEzfPObq88B/kAcgSk+FngMHZzcmR7bgD3GwdSxnQ
dkRI9zvk7eQ35tcUwntAr4Lat6/ILjFqlBmVLxrdXHuF5Xz9jcZLYgKzz61xdYM9
dC6FKF0u5eGIIvbauGAo7jaeGFX1F3Zu5b4lP9kEOGwU1B7sxF0FzsQM5+dtCJgv
We/hWQeF+9gtoVnkCSS/Mq2p0UomXXHW0Bz4+HuHlTR9aiYbviYnotABiLUhZyzt
v5yUaktb9qniBfdLpRlq8cp06xYlTEA9gJpa4Pnok8OWUsbAiW6EiXUSaZ/cchVa
AnO8WWYvVOnnt6WHI3+QdFTnqVjE5TBX4N/7bVhHGQKBgQD0dtbFqp7vZK/jVYvE
z0WPdySOg2ZDmoSfk5ZlR1+Y9zWToHv0qu8zqoOjL8Ubxrh9fGlOow+cCVdkEuaa
jWC2AWetuRvW0Z5A3XMXr0/N/1fgOkTqtp3WNrUPjVJahEg3lN+90opgFoT8swSi
s1oxW0oLcVIlrjhGBXAPCfsAuQKBgQDWBLRhHsRAvGcK5wGuVnxVApTIyBOermsW
3bJt+7+UI+4sYrBAwkWdQG93IG0cQtn48TEPBgmR2fjRF5IFT9M4/u+QOeeByT7I
we7nVtHgSY5ByC9N0mjWbcmSg8fktz/LonjldNC4kWdOFb75fxGf8kOGS5rUaMA4
zHucfB6ZvwKBgQCPHJrysMXGY21MaqIeHzEboaX3ABl37hdBzAa5V6UxSVdGCydF
vmO2HVZey/JaJmWOoKyNaowSzq0oWqBBTg6VvhDR9JHFmoVId9uOvAS+FYN+Mt5x
gWK5KuGoLxVNBC+6yh6JY526TrSfsrU+Aj0Es+qO9FIg2PL8muZVB4S3kQKBgH/5
CDMaxpc/EQ5/2413wZjDllwI51J3USm3Hz6Mzp2ybnSz/lh60k2Zfg1polTH1Lb6
4i7tmUNRZ2sAARyUAuWN64n+VeRRhe1dqZFDZPQMh7fmEAMk0fOGaoXlrt2ghdEq
Mchi9Xun1nHmpu9hgBR4NNBU3RwuFuLfwvprbZDZAoGAWa62QJChE86xQGP1MrL2
SbIzw3cfeP5xdQ3MKldJiy5IkbMR7Z13WZ7FwvPTy0g/onLHD1rqlm1kUMsGRHpD
5vH06PNpKXQ6x8BYaRGtE6P39jLycO/X+WK/lYTrWo1bR+mGCebDh4B5XrwT3gI6
x4Gvz134pZCTyQCf5JCwbQs=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,20 @@
// background.js
// Called when the user clicks on the browser action.
chrome.browserAction.onClicked.addListener(function(tab) {
// get the frontend_url
chrome.storage.sync.get({
frontend_url: 'http://localhost',
audio_only: false
}, function(items) {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var activeTab = tabs[0];
var url = activeTab.url;
if (url.includes('youtube.com')) {
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
chrome.tabs.create({ url: new_url });
}
});
});
});

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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

@@ -0,0 +1,26 @@
{
"manifest_version": 2,
"name": "YoutubeDL-Material",
"version": "0.3",
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "favicon.png"
},
"permissions": [
"tabs",
"storage"
],
"options_ui": {
"page": "options.html",
"open_in_tab": false
},
"browser_specific_settings": {
"gecko": {
"id": "IsaacMGrynsztein@gmail.com",
"strict_min_version": "57.0"
}
}
}

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<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 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>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
// Saves options to chrome.storage
function save_options() {
var frontend_url = document.getElementById('frontend_url').value;
var audio_only = document.getElementById('audio_only').checked;
chrome.storage.sync.set({
frontend_url: frontend_url,
audio_only: audio_only
}, function() {
// Update status to let user know options were saved.
$('#collapseExample').collapse('show');
setTimeout(function() {
$('#collapseExample').collapse('hide');
}, 2000);
});
}
// Restores select box and checkbox state using the preferences
// stored in chrome.storage.
function restore_options() {
chrome.storage.sync.get({
frontend_url: 'http://localhost',
audio_only: false
}, function(items) {
document.getElementById('frontend_url').value = items.frontend_url;
document.getElementById('audio_only').checked = items.audio_only;
});
}
document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('save').addEventListener('click',
save_options);

6
db.json Normal file
View File

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

0
installer.py Normal file
View File

41
main.js Normal file
View File

@@ -0,0 +1,41 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');
let win;
function createWindow() {
win = new BrowserWindow({ width: 800, height: 600 });
// load the dist folder from Angular
win.loadURL(
url.format({
pathname: path.join(__dirname, `/dist/index.html`),
protocol: 'file:',
slashes: true
})
);
// The following is optional and will open the DevTools:
// win.webContents.openDevTools()
win.on('closed', () => {
win = null;
});
}
app.on('ready', createWindow);
// on macOS, closing the window doesn't quit the app
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// initialize the app's main window
app.on('activate', () => {
if (win === null) {
createWindow();
}
});

16240
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,59 @@
{
"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"
"e2e": "ng e2e",
"electron": "ng build --base-href ./ && electron ."
},
"engines": {
"node": "12.3.1",
"npm": "6.10.3"
},
"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",
"ng4-configure": "^0.1.7",
"filesize": "^6.1.0",
"ng-lazyload-image": "^7.0.1",
"ngx-videogular": "^9.0.1",
"fingerprintjs2": "^2.1.0",
"rxjs": "^6.5.3",
"rxjs-compat": "^6.0.0-rc.0",
"tslib": "^1.10.0",
"zone.js": "~0.9.1"
"typescript": "~3.7.5",
"web-animations-js": "^2.3.2",
"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",
"karma": "~1.7.0",
@@ -51,7 +64,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.

15
src/_palette.scss Normal file
View File

@@ -0,0 +1,15 @@
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
/* HSL */
$color1: hsla(351%, 56%, 80%, 1);
$softblue: hsla(205%, 100%, 86%, 1);
$color3: hsla(174%, 100%, 83%, 1);
$color4: hsla(133%, 93%, 78%, 1);
$color5: hsla(165%, 20%, 59%, 1);
/* RGB */
$color1: rgba(232, 174, 183, 1);
$softblue: rgba(184, 225, 255, 1);
$color3: rgba(169, 255, 247, 1);
$color4: rgba(148, 251, 171, 1);
$color5: rgba(130, 171, 161, 1);

View File

@@ -0,0 +1,25 @@
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, 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({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -1,56 +1,28 @@
.demo-card {
margin: 16px;
}
.demo-basic {
padding: 0;
}
.demo-basic .mat-card-content {
padding: 16px;
}
mat-toolbar.top {
height: 60px;
width: 100%;
text-align: center;
}
/*::ng-deep .mat-form-field-placeholder{
transform: scale(.75) translateY(20px) !important;
}*/
.big {
max-width: 800px;
margin: 0 auto;
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.example-full-width {
.flex-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
}
mat-form-field.mat-form-field {
font-size: 24px;
}
.spinner {
position: absolute;
display: inline-block;
margin-left: -28px;
margin-top: -10px;
.flex-column {
display: flex;
flex-direction: column;
flex-basis: 100%;
flex: 1;
}
.make-room-for-spinner {
padding-right: 40px;
.theme-slide-toggle {
top: 2px;
left: 10px;
position: relative;
pointer-events: none;
}
.equal-sizes {
padding-right: 20px;
.sidenav-container {
z-index: -1 !important;
}
.top-toolbar {
height: 64px;
}

View File

@@ -1,105 +1,53 @@
<mat-toolbar color="primary" class="top">
<table width="100%" height="100%">
<td class="topbar" style="text-align: left; left:0px; font-size: 15px">
</td>
<td class="topbar" style="text-align: center">
<div style="margin-top: 14px">{{topBarTitle}}</div>
</td>
<td class="topbar" style="text-align: right">
</td>
</table>
</mat-toolbar>
<br/>
<div class="big demo-basic">
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
<mat-card-title>
Youtube Downloader
</mat-card-title>
<mat-card-content>
<div style="position: relative;">
<form class="example-form">
<mat-form-field class="example-full-width">
<input matInput [(ngModel)]="url" placeholder="URL" type="url" name="url" [formControl]="urlForm" required>
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
</mat-form-field>
</form>
<br/>
<mat-checkbox [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
<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>
</mat-card-content>
<mat-card-actions>
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-stroked-button
color="primary">Download</button>
</mat-card-actions>
</mat-card>
</div>
<br/>
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
<div [ngClass]="(determinateProgress && percentDownloaded === 100)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="determinateProgress;else indeterminateprogress">
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
</mat-toolbar>
</div>
<div *ngIf="determinateProgress && percentDownloaded === 100" class="spinner">
<mat-spinner [diameter]="25"></mat-spinner>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav #sidenav>
<mat-nav-list>
<a mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="allowSubscriptions && (!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="enableDownloadsManager && (!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>
<ng-template #indeterminateprogress>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
<br/>
</div>
<ng-template #nofile>
</ng-template>
<div style="margin: 20px" *ngIf="fileManagerEnabled">
<mat-accordion>
<mat-expansion-panel class="big">
<mat-expansion-panel-header>
<mat-panel-title>
Audio
</mat-panel-title>
<mat-panel-description>
Your audio files are here
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="mp3s.length > 0;else nomp3s">
<mat-grid-list cols="4" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
<app-file-card (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="true"></app-file-card>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-expansion-panel>
<mat-expansion-panel class="big">
<mat-expansion-panel-header>
<mat-panel-title>
Video
</mat-panel-title>
<mat-panel-description>
Your video files are here
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="mp4s.length > 0;else nomp4s">
<mat-grid-list cols="4" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="false"></app-file-card>
</mat-grid-tile>
</mat-grid-list>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template #nomp3s>
</ng-template>
<ng-template #nomp4s>
</ng-template>
</div>

View File

@@ -1,14 +1,30 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
import {PostsService} from './posts.services';
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';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/observable/fromEvent'
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/debounceTime'
import 'rxjs/add/operator/do'
import 'rxjs/add/operator/switch'
import { YoutubeSearchService, Result } from './youtube-search.service';
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',
@@ -16,212 +32,185 @@ import 'rxjs/add/operator/toPromise';
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
iOS = false;
determinateProgress = false;
downloadingfile = false;
audioOnly: boolean;
urlError = false;
path = '';
url = '';
exists = '';
@HostBinding('class') componentCssClass;
THEMES_CONFIG = THEMES_CONFIG;
// config items
topBarTitle = 'Youtube Downloader';
percentDownloaded: number;
fileManagerEnabled = false;
downloadOnlyMode = false;
defaultTheme = null;
allowThemeChange = null;
allowSubscriptions = false;
enableDownloadsManager = false;
// defaults to true to prevent attack
settingsPinRequired = true;
mp3s: any[] = [];
mp4s: any[] = [];
@ViewChild('sidenav') sidenav: MatSidenav;
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
navigator: string = null;
urlForm = new FormControl('', [Validators.required]);
constructor(public postsService: PostsService, public snackBar: MatSnackBar, private dialog: MatDialog,
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
constructor(private postsService: PostsService, public snackBar: MatSnackBar) {
this.audioOnly = false;
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();
}
}
});
this.postsService.config_reloaded.subscribe(changed => {
if (changed) {
this.loadConfig();
}
});
}
toggleSidenav() {
this.sidenav.toggle();
}
loadConfig() {
// loading config
this.postsService.loadNavItems().subscribe(result => { // loads settings
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
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'];
this.postsService.path = backendUrl;
this.postsService.startPath = backendUrl;
this.postsService.startPathSSL = backendUrl;
if (this.fileManagerEnabled) {
this.getMp3s();
this.getMp4s();
}
}, error => {
console.log(error);
});
}
// file manager stuff
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
const mp3s = result['mp3s'];
this.mp3s = mp3s;
}, error => {
console.log(error);
});
}
getMp4s() {
this.postsService.getMp4s().subscribe(result => {
const mp4s = result['mp4s'];
this.mp4s = mp4s;
},
error => {
console.log(error);
});
}
public goToFile(name, isAudio) {
if (isAudio) {
this.downloadHelperMp3(name, true);
} else {
this.downloadHelperMp4(name, true);
// sets theme to config default if it doesn't exist
if (!localStorage.getItem('theme')) {
this.setTheme(themingExists ? this.defaultTheme : 'default');
}
}
public removeFromMp3(name: string) {
for (let i = 0; i < this.mp3s.length; i++) {
if (this.mp3s[i].id === name) {
this.mp3s.splice(i, 1);
}
}
}
// theme stuff
public removeFromMp4(name: string) {
// console.log(name);
// console.log(this.mp4s);
for (let i = 0; i < this.mp4s.length; i++) {
if (this.mp4s[i].id === name) {
this.mp4s.splice(i, 1);
}
}
}
// app initialization.
ngOnInit() {
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
}
// download helpers
downloadHelperMp3(name: string, forceView = false) {
this.postsService.getFileStatusMp3(name).subscribe(fileExists => {
const exists = fileExists;
this.exists = exists[0];
if (exists[0] === 'failed') {
const percent = exists[2];
// console.log(percent);
if (percent > 0.30) {
this.determinateProgress = true;
this.percentDownloaded = percent * 100;
}
setTimeout(() => this.downloadHelperMp3(name), 500);
} else {
// if download only mode, just download the file. no redirect
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
const blob: Blob = res;
saveAs(blob, name + '.mp3');
this.downloadingfile = false;
});
} else {
window.location.href = this.exists;
}
// reloads mp3s
this.getMp3s();
}
});
}
downloadHelperMp4(name: string, forceView = false) {
this.postsService.getFileStatusMp4(name).subscribe(fileExists => {
const exists = fileExists;
this.exists = exists[0];
if (exists[0] === 'failed') {
const percent = exists[2];
if (percent > 0.30) {
this.determinateProgress = true;
this.percentDownloaded = percent * 100;
}
setTimeout(() => this.downloadHelperMp4(name), 500);
} else {
// if download only mode, just download the file. no redirect
if (forceView === false && this.downloadOnlyMode) {
this.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
const blob: Blob = res;
saveAs(blob, name + '.mp4');
this.downloadingfile = false;
});
} else {
window.location.href = this.exists;
}
// reloads mp4s
this.getMp4s();
}
});
}
// download click handler
downloadClicked() {
if (this.ValidURL(this.url)) {
this.urlError = false;
this.path = '';
if (this.audioOnly) {
this.downloadingfile = true;
this.postsService.makeMP3(this.url).subscribe(posts => {
this.path = posts['audiopathEncoded'];
if (this.path !== '-1') {
this.downloadHelperMp3(this.path);
setTheme(theme) {
// theme is registered, so set it to the stored cookie variable
let old_theme = null;
if (this.THEMES_CONFIG[theme]) {
if (localStorage.getItem('theme')) {
old_theme = localStorage.getItem('theme');
if (!this.THEMES_CONFIG[old_theme]) {
console.log('bad theme found, setting to default');
if (this.defaultTheme === null) {
// means it hasn't loaded yet
console.error('No default theme detected');
} else {
localStorage.setItem('theme', this.defaultTheme);
old_theme = localStorage.getItem('theme'); // updates old_theme
}
}
}, error => { // can't access server
this.downloadingfile = false;
this.openSnackBar('Download failed!', 'OK.');
});
} else {
this.downloadingfile = true;
this.postsService.makeMP4(this.url).subscribe(posts => {
this.path = posts['videopathEncoded'];
if (this.path !== '-1') {
this.downloadHelperMp4(this.path);
}
}, error => { // can't access server
this.downloadingfile = false;
this.openSnackBar('Download failed!', 'OK.');
});
}
}
localStorage.setItem('theme', theme);
this.elementRef.nativeElement.ownerDocument.body.style.backgroundColor = this.THEMES_CONFIG[theme]['background_color'];
} else {
this.urlError = true;
console.error('Invalid theme: ' + theme);
return;
}
}
// checks if url is a valid URL
ValidURL(str) {
// tslint:disable-next-line: max-line-length
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
const re = new RegExp(strRegex);
return re.test(str);
}
this.postsService.setTheme(theme);
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
}
onSetTheme(theme, old_theme) {
if (old_theme) {
document.body.classList.remove(old_theme);
this.overlayContainer.getContainerElement().classList.remove(old_theme);
}
this.overlayContainer.getContainerElement().classList.add(theme);
this.componentCssClass = theme;
}
flipTheme() {
if (this.postsService.theme.key === 'default') {
this.setTheme('dark');
} else if (this.postsService.theme.key === 'dark') {
this.setTheme('default');
}
}
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() {
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'
});
}
// snackbar helper
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -1,26 +1,114 @@
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,
MatGridList,
MatProgressSpinnerModule} from '@angular/material';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { HttpModule } from '@angular/http';
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 { HttpClientModule, HttpClient } from '@angular/common/http';
import { PostsService } from 'app/posts.services';
import {APP_BASE_HREF} from '@angular/common';
import { FileCardComponent } from './file-card/file-card.component';
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, VgControlsModule, VgOverlayPlayModule, VgBufferingModule} from 'ngx-videogular';
import { InputDialogComponent } from './input-dialog/input-dialog.component';
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
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 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';
registerLocaleData(es, 'es');
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
}
@NgModule({
declarations: [
AppComponent,
FileCardComponent
FileCardComponent,
MainComponent,
PlayerComponent,
InputDialogComponent,
CreatePlaylistComponent,
DownloadItemComponent,
SubscriptionsComponent,
SubscribeDialogComponent,
SubscriptionComponent,
SubscriptionFileCardComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent,
AboutDialogComponent,
VideoInfoDialogComponent,
ArgModifierDialogComponent,
HighlightPipe,
UpdaterComponent,
UpdateProgressDialogComponent,
ShareMediaDialogComponent,
LoginComponent,
DownloadsComponent,
UserProfileDialogComponent,
SetDefaultAdminDialogComponent,
ModifyUsersComponent,
AddUserDialogComponent,
ManageUserComponent,
ManageRoleComponent
],
imports: [
CommonModule,
BrowserModule,
BrowserAnimationsModule,
MatNativeDateModule,
@@ -29,7 +117,6 @@ import {RouterModule} from '@angular/router';
MatInputModule,
MatSelectModule,
ReactiveFormsModule,
HttpModule,
HttpClientModule,
MatToolbarModule,
MatCardModule,
@@ -43,9 +130,44 @@ import {RouterModule} from '@angular/router';
MatExpansionModule,
MatProgressBarModule,
MatProgressSpinnerModule,
RouterModule
MatButtonToggleModule,
MatRippleModule,
MatMenuModule,
MatDialogModule,
MatSlideToggleModule,
MatMenuModule,
MatAutocompleteModule,
MatTabsModule,
MatTooltipModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
MatChipsModule,
DragDropModule,
ClipboardModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
VgBufferingModule,
LazyLoadImageModule.forRoot({ isVisible }),
RouterModule,
AppRoutingModule,
],
entryComponents: [
InputDialogComponent,
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

@@ -0,0 +1,20 @@
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
<form>
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div>
<mat-form-field color="accent">
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</form>
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button>

View File

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

View File

@@ -0,0 +1,58 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl } from '@angular/forms';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-create-playlist',
templateUrl: './create-playlist.component.html',
styleUrls: ['./create-playlist.component.scss']
})
export class CreatePlaylistComponent implements OnInit {
// really "createPlaylistDialogComponent"
filesToSelectFrom = null;
type = null;
filesSelect = new FormControl();
name = '';
create_in_progress = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
private postsService: PostsService,
public dialogRef: MatDialogRef<CreatePlaylistComponent>) { }
ngOnInit() {
if (this.data) {
this.filesToSelectFrom = this.data.filesToSelectFrom;
this.type = this.data.type;
}
}
createPlaylist() {
const thumbnailURL = this.getThumbnailURL();
this.create_in_progress = true;
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
this.create_in_progress = false;
if (res['success']) {
this.dialogRef.close(true);
} else {
this.dialogRef.close(false);
}
});
}
getThumbnailURL() {
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
const file = this.filesToSelectFrom[i];
if (file.id === this.filesSelect.value[0]) {
// different services store the thumbnail in different places
if (file.thumbnailURL) { return file.thumbnailURL };
if (file.thumbnail) { return file.thumbnail };
}
}
return null;
}
}

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>

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