Compare commits

..

179 Commits

Author SHA1 Message Date
dependabot[bot]
9bf1a16ce4 Bump ubuntu from 22.04 to 23.04
Bumps ubuntu from 22.04 to 23.04.

---
updated-dependencies:
- dependency-name: ubuntu
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-01 11:11:47 +00:00
Tzahi12345
9c0a77cb6e Merge pull request #705 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-07-09 21:25:07 -04:00
Azhar Pusparadhian
75915c41c7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (381 of 381 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/id/
2022-07-09 20:18:52 +02:00
Tzahi12345
fc3c179f6a Reverted #696 and updated node version to avoid 243 error 2022-07-04 20:45:39 -04:00
Tzahi12345
f3572d274c Merge pull request #696 from martadinata666/patch-2
Update entrypoint.sh
2022-07-03 17:26:48 -04:00
Dedy Martadinata S
02447e0285 Update entrypoint.sh
about gosu behaviour, it looking for local "existed" UID, i  found it become blank.
That is our issue, so updating our entrypoint to change existed user "youtube" UID and GID to match compose request, will make gosu correctly find use UID:GID as it existed.
2022-07-03 13:05:53 +07:00
Isaac Abadi
24475386f9 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2022-07-01 00:55:03 -04:00
Isaac Abadi
55268301f6 Removed config env vars set message if no items were set 2022-07-01 00:54:38 -04:00
Isaac Abadi
faa76abbbd Fixed issue where setting resolution in a sub would instead require that resolution to exist (#678 and #330) 2022-07-01 00:51:30 -04:00
Glassed Silver
b827f8f0cc Merge pull request #687 from Tzahi12345/add-permissions-for-tasks-manager
Add permissions for tasks manager
2022-06-30 16:34:08 +02:00
Glassed Silver
b6b61c42d4 Merge pull request #677 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-06-30 16:22:45 +02:00
Glassed Silver
6af1ce4092 Merge pull request #683 from adripo/patch-1
fix: #682 install tzdata
2022-06-30 16:20:40 +02:00
Glassed Silver
303d0015c6 Merge pull request #688 from adripo/patch-2
fix: remove exposed ports for mongo
2022-06-30 16:04:45 +02:00
adripo
56db43da79 fix: remove exposed ports for mongo
exposed ports between services in the same stack is not needed
2022-06-30 13:11:21 +02:00
Isaac Abadi
64b1a9e5c0 Updated mangled translations
Improved automatic translations command
2022-06-30 01:34:52 -04:00
Isaac Abadi
48f0a700ab Paginator is now always visible to avoid case where file type filter permanently disappears 2022-06-30 01:30:06 -04:00
Isaac Abadi
768798c6b3 Fixed issue where one-off playlist downlaods would only include the first video 2022-06-30 01:29:18 -04:00
Isaac Abadi
9d1f93acfb Added translations for manage-role component 2022-06-30 00:44:11 -04:00
Isaac Abadi
077a0d8fdb Added migration to add tasks manager permission for admin role
All routes are now properly protected against logged in users w/o permissions
2022-06-30 00:43:57 -04:00
Tzahi12345
c9359f172e Merge pull request #681 from adripo/node-config-fix
fix: node-config fix environment variable
2022-06-29 23:46:45 -04:00
Tzahi12345
d6dc4756a7 Merge pull request #680 from adripo/remove-container-config-env
fix: remove write_ytdl_config
2022-06-29 23:44:50 -04:00
adripo
9bc9b17294 fix: #682 install tzdata 2022-06-29 23:52:25 +02:00
adripo
80d3580447 fix: node-config fix environment variable 2022-06-29 20:20:27 +02:00
adripo
3f15f3bcaf fix: remove env variable check 2022-06-29 19:44:13 +02:00
Tzahi12345
703848e4e5 Merge pull request #675 from FoxxMD/unknownFormatsFix
Fixed parsing expected file size for videos with no known formats
2022-06-29 11:31:32 -04:00
Sebastian Danielsson
934965720e Translated using Weblate (Swedish)
Currently translated at 31.4% (120 of 381 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/sv/
2022-06-29 17:17:36 +02:00
AbsurdUsername
bb4a882d19 Translated using Weblate (Italian)
Currently translated at 100.0% (381 of 381 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/it/
2022-06-29 17:17:36 +02:00
FoxxMD
74315b8c76 Fixed parsing expected file size for videos with no known formats 2022-06-29 09:37:26 -04:00
Glassed Silver
a9e95c5bb8 Merge pull request #672 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-06-28 07:20:04 +02:00
Sebastian
fe45a889c9 Added translation using Weblate (Swedish) 2022-06-27 22:14:59 +02:00
Isaac Abadi
e726e991cc Partially reverted fecefde3ad 2022-06-27 13:12:53 -04:00
Isaac Abadi
940267651d Hotfix for bug where a sub with missing videos would cause a crash during migration #670 2022-06-27 03:52:05 -04:00
Isaac Abadi
2dc68139f7 Streaming-only subs are now actually paused
DB transfers in any direction now generate backups and associated logs are set to info
2022-06-27 00:08:41 -04:00
Isaac Abadi
301451d021 Fixed issue where tasks would not initialize properly after migration until server restart
streaming-only subscriptions now autopause due to deprecated
2022-06-26 23:47:03 -04:00
Isaac Abadi
a7f8795e7e Fixed missing latest tag on docker-release 2022-06-26 23:10:47 -04:00
Isaac Abadi
162094a9b9 File type filter now only appears with the paginator 2022-06-26 23:06:15 -04:00
Isaac Abadi
e843b4c97f Fixed crash during docker-release action 2022-06-26 22:07:21 -04:00
Isaac Abadi
c784091ad6 Fixed issue where docker-release action couldn't accept inputs (2) 2022-06-26 22:04:42 -04:00
Isaac Abadi
fb404d3cee Fixed issue where docker-release action couldn't accept inputs 2022-06-26 22:02:23 -04:00
Tzahi12345
68c2ee26ff Merge pull request #657 from Tzahi12345/4.3-prep
4.3 Prep
2022-06-26 21:13:51 -04:00
Isaac Abadi
742129bf6a Updated source translation file 2022-06-26 20:58:59 -04:00
Glassed Silver
9a250b5c58 Update unified-file-card.component.html 2022-06-27 02:06:13 +02:00
Isaac Abadi
86cbfea08f UI & logs now use proper fork name rather than just youtube-dl 2022-06-25 17:22:08 -04:00
Isaac Abadi
19a3ffc118 Minor code cleanup 2022-06-24 19:27:46 -04:00
Isaac Abadi
d225e84a03 Fixed issue where errored single videos in playlist/sub downloads in yt-dlp would cause the entire sub check to fail (#493) 2022-06-24 19:26:34 -04:00
Isaac Abadi
a4a0045475 Fixed error where sub with no videos would crash if none existed and redownload fresh uploads was enabled (#480) 2022-06-24 17:33:10 -04:00
Isaac Abadi
573cca0b2f Completed deprecation of streamingOnly mode for subscriptions 2022-06-24 17:29:06 -04:00
Isaac Abadi
fecefde3ad Removed pm2 global install in favor of local node_modules (#662) 2022-06-24 13:14:28 -04:00
Isaac Abadi
d300a8a3c6 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into 4.3-prep 2022-06-24 13:02:26 -04:00
Isaac Abadi
c6b7e7bc4c Added rpi 4 as a "special case" in the readme 2022-06-24 00:13:36 -04:00
Isaac Abadi
0ffd7022d0 Removed nodemon
Fixed several dependency vulnerabilities
2022-06-23 19:49:48 -04:00
Isaac Abadi
a0c36bf1a1 Subscription videos now skip the collect info step during download process, reducing calls to video host 2022-06-23 18:37:54 -04:00
Isaac Abadi
64b4b5a2b4 Fixed #600, where selecting a max resolution for subscriptions was broken 2022-06-23 01:44:55 -04:00
Isaac Abadi
b1448d95e5 Added resolution and audio bitrate to video info dialog 2022-06-23 01:44:19 -04:00
Isaac Abadi
a2d1b154a3 Fixed broken translations for snackbars 2022-06-23 01:22:22 -04:00
Isaac Abadi
2ba1dc6333 Archive refactor to improve reliability and consistency 2022-06-23 01:10:18 -04:00
Isaac Abadi
314a5047d6 Updated translations source file 2022-06-21 22:48:03 -04:00
Isaac Abadi
fb6cf8548d Added spinner during db connection test 2022-06-21 22:45:54 -04:00
Isaac Abadi
adbe7f95d5 Fixed issue where reopening file info dialog after editing metadata would show stale data 2022-06-21 22:45:38 -04:00
Isaac Abadi
55d4f746c3 Fixed issue where mongodb attempted to connect even when using local DB 2022-06-21 22:28:30 -04:00
Isaac Abadi
6d3f5e6c94 Improved UI for download only mode
Updated API model for DatabaseFile
2022-06-21 21:38:58 -04:00
Isaac Abadi
bec158f65d Fixed an issue where if file manager and download only mode were enabled, files would download twice 2022-06-21 21:07:34 -04:00
Isaac Abadi
6fa4296edf Hotfix for #661 startup crash 2022-06-21 20:24:48 -04:00
Isaac Abadi
636f7b16a8 Changed default downloader to yt-dlp 2022-06-21 02:23:35 -04:00
Isaac Abadi
39abc3efcf Utilize yt-dlp filesize approximations if available 2022-06-21 02:21:04 -04:00
Glassed Silver
2edc00c950 Merge pull request #659 from GlassedSilver/master
Remove releases folder from root dir -> fixes #625
2022-06-21 08:11:15 +02:00
Isaac Abadi
6fe0cd5649 Updated pip target 2022-06-21 02:01:36 -04:00
Isaac Abadi
b6de6d08fa Fixed potential command injection vulnerability 2022-06-21 01:58:35 -04:00
Isaac Abadi
cddd280206 Fixed issue where pip was missing in Docker
Temp twitch chat files now get auto removed
2022-06-21 01:51:32 -04:00
Isaac Abadi
9d1624d569 Removed recommendation of nightly, updated language in README, and updated SECURITY.md 2022-06-21 01:42:15 -04:00
Isaac Abadi
da8c23d3ef Updated dockerfile to include tcd installation 2022-06-21 01:40:42 -04:00
Isaac Abadi
901e87a681 Fixed loading spinner in create playlist dialog 2022-06-21 01:23:28 -04:00
Isaac Abadi
7bfb2976fe Fixed twitch chat downloads, tcd is now required and client secret must be supplied 2022-06-21 01:22:58 -04:00
GlassedSilver
295781b1f1 Remove releases folder from root dir -> fixes #625 2022-06-21 00:55:36 +02:00
Isaac Abadi
cbdd1a6253 Improved snackbar translations support 2022-06-20 16:25:55 -04:00
Tzahi12345
c91e51de15 Merge pull request #655 from Tzahi12345/improved-downloads-management
Improved downloads management
2022-06-20 16:07:02 -04:00
Isaac Abadi
4c6c15d3a3 Code cleanup 2022-06-20 16:05:54 -04:00
Isaac Abadi
0951e445ac Removed modify playlsit component 2022-06-20 16:05:37 -04:00
Isaac Abadi
e1cb56e8e9 Unified create and modify playlist components 2022-06-20 16:02:15 -04:00
Glassed Silver
05ee48ffb6 Merge pull request #604 from GlassedSilver/master
Added weekly (Tuesday) Build and Push Docker image + Added second fix-script
2022-06-20 15:08:39 +02:00
Isaac Abadi
7f47fb339b Updated modify playlist size
Fixed issue where playlist order could not be rearranged
2022-06-19 23:46:24 -04:00
Isaac Abadi
690cc38899 Updated playlist file selection to use recent videos component
Playlists are now file type agnostic

Updated translations
2022-06-19 23:09:30 -04:00
GlassedSilver
d912e44484 Moved Docker Weekly build to docker.yml 2022-06-20 01:50:35 +02:00
GlassedSilver
93e3dafb03 Merge branch 'master' of https://github.com/GlassedSilver/YoutubeDL-Material into master 2022-06-20 01:44:59 +02:00
Isaac Abadi
b5ee0d365c Subscription file cards are now replaced with unified file cards
GetAllFiles can now filter by sub_id

Improved API models and added request body docs for GetAllFiles
2022-06-19 01:14:59 -04:00
Isaac Abadi
0dd617b438 Updated translation source file 2022-06-18 20:11:47 -04:00
Isaac Abadi
aca86e0228 Added test for ID3 tagging 2022-06-18 20:09:11 -04:00
Isaac Abadi
895c385d6b Updated version to 4.3 2022-06-18 19:36:20 -04:00
Isaac Abadi
b56c66f705 Refactored retrieval of categories and improved runtime search of files in category
Fixed issue with editing/saving categories

Database queries can now handle nested object paths

Code cleanup
2022-06-17 19:43:32 -04:00
Isaac Abadi
c810d4d878 Code cleanup 2022-06-17 15:35:06 -04:00
Isaac Abadi
9cf8b87c6e Added ability to modify file metadata 2022-06-17 15:34:12 -04:00
Isaac Abadi
53a181e04d Fixed several bugs with categories
Code cleanup
2022-06-16 23:42:47 -04:00
Isaac Abadi
eee8494c70 Updated local font/bootstrap setup to be more Angular compatible 2022-06-14 21:00:28 -04:00
Isaac Abadi
b50baf6fa7 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into improved-downloads-management 2022-06-14 20:40:46 -04:00
Isaac Abadi
f905d3c321 Fixed a few broken tests 2022-06-14 20:33:01 -04:00
Glassed Silver
91f5de326d Merge pull request #649 from fpiesche/patch-4
Fix 403s when pushing to GHCR
2022-06-14 06:55:20 +02:00
Florian Piesche
dcbd8f0346 Fix secret name 2022-06-13 18:32:34 +01:00
Florian Piesche
8a6a578e60 Fix secret name 2022-06-13 18:31:24 +01:00
Florian Piesche
01114d9309 Fix 403 when pushing images to GHCR 2022-06-13 18:29:53 +01:00
Florian Piesche
7f387ce6aa Fix 403s when pushing images to GHCR 2022-06-13 18:28:45 +01:00
Glassed Silver
523d303766 Merge pull request #647 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-06-12 03:09:39 +02:00
ㅤAbsurdUsername
6bd9ddd14c Translated using Weblate (Italian)
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/it/
2022-06-11 13:17:31 +02:00
TyRoyal
f8d4e18fd4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-06-11 13:17:28 +02:00
TyRoyal
56facd320f Translated using Weblate (Chinese (Simplified))
Currently translated at 98.4% (319 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-06-10 04:40:04 +02:00
Glassed Silver
14c9dc482b Merge pull request #628 from firstdorsal/master
add fonts and css local for better privacy
2022-06-09 14:34:09 +02:00
Glassed Silver
1f47f01fd5 Merge pull request #635 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-06-09 14:30:27 +02:00
Glassed Silver
09957843ec Merge pull request #631 from fpiesche/patch-2
Automatically run docker release on releases
2022-06-09 14:27:31 +02:00
Glassed Silver
a6ae5d114e Merge pull request #633 from fpiesche/patch-4
Use docker/metadata-action
2022-06-09 14:26:19 +02:00
Glassed Silver
68c2bc9d3d Merge pull request #632 from fpiesche/patch-3
Add Dependabot configuration
2022-06-09 14:00:06 +02:00
Felipe
f9f35b27bd Translated using Weblate (Portuguese)
Currently translated at 89.8% (291 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt/
2022-06-08 09:16:19 +02:00
Felipe Nogueira
6a63f7ee1a Added translation using Weblate (Portuguese (Brazil)) 2022-06-07 08:52:10 +02:00
Maxime Leroy
11acd56e1e Translated using Weblate (French)
Currently translated at 99.3% (322 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2022-06-03 07:14:29 +02:00
Florian Piesche
12dd9d45b5 Use docker/metadata-action
This simplifies specifying tags and will also generate metadata labels for the image as per [OpenContainers spec](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
2022-06-01 23:40:58 +01:00
Florian Piesche
d0171d719b Use docker/metadata-action
This will generate tags based on specific patterns, as well as add metadata labels to the images as per [the OpenContainers spec](https://github.com/opencontainers/image-spec/blob/main/annotations.md).
2022-06-01 23:25:05 +01:00
Florian Piesche
e298f19534 Fix Github workflows directory 2022-06-01 23:05:28 +01:00
Florian Piesche
6c875ba667 Add Dependabot configuration
With this set up, you'll automatically get PRs from [Dependabot](https://github.com/dependabot) for most of your dependencies - base images for your Dockerfiles, Github Actions in your workflows, and npm packages for the frontend and backend.
2022-06-01 23:03:48 +01:00
Florian Piesche
1b4caf4699 Automatically run docker release on releases
With this change, publishing a new release using the github web UI will automatically trigger the `docker-release.yml` workflow and build and push the image, publishing it under the `latest` tag as well as the repository tag name for the release. No more manually kicking off the release workflow!

I've also added publishing the image to ghcr.io because with Docker Hub pushing harder on subscriptions it might be nice to have a backup in place.
2022-06-01 22:55:56 +01:00
Paul Colin Hennig
ca9b1641d8 add fonts and css local for better privacy 2022-05-29 14:14:50 +02:00
Glassed Silver
b861e54a51 Merge branch 'Tzahi12345:master' into master 2022-05-23 01:34:37 +02:00
Glassed Silver
050c40fc19 Merge pull request #616 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-05-22 19:21:10 +02:00
AHOHNMYC
0945a0bbd1 Translated using Weblate (Russian)
Currently translated at 97.8% (317 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2022-05-22 13:41:56 +02:00
S3aBreeze
9f91fdf221 Translated using Weblate (Russian)
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2022-05-22 13:41:56 +02:00
AHOHNMYC
4b89c58c84 Translated using Weblate (Russian)
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2022-05-22 13:41:56 +02:00
dejan995
d0876516a6 Translated using Weblate (Macedonian)
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/mk/
2022-05-22 13:41:56 +02:00
Maite Guix
e8390e3d9d Translated using Weblate (Catalan)
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2022-05-22 13:41:56 +02:00
Isaac Abadi
306da4ea63 LDAP logins no longer show error resulting from the required internal login attempt 2022-05-21 22:59:39 -04:00
Glassed Silver
5c4c282718 Merge pull request #619 from Tzahi12345/GlassedSilver-REPO-improvement-no-response
Autoclose stale issues lacking requested response
2022-05-22 04:11:50 +02:00
Isaac Abadi
bf64d97b72 Fixed downloads not sorting properly
Confirm dialog can now be a selection list
2022-05-21 21:26:25 -04:00
GlassedSilver
8d8c52e009 002-fix_dupes_per_archive: prep consistency w/ 003 2022-05-15 05:48:06 +02:00
GlassedSilver
37107148eb Fix Scripts: Small improvements to usage instr. 2022-05-15 04:47:16 +02:00
GlassedSilver
29273e2775 fix-scripts: specifically target bash over POSIX 2022-05-14 03:58:46 +02:00
Isaac Abadi
71d5a64272 Ported $ne to local DB and added tests for applyFilterToLocalDB 2022-05-11 23:56:49 -04:00
Isaac Abadi
a2db8ba0fe Further cleanup to API and models 2022-05-11 23:53:10 -04:00
Isaac Abadi
1514952fd1 Cleaned up dependencies, routes, and API models 2022-05-11 22:58:46 -04:00
Glassed Silver
9fc659dc0a Autoclose stale issues lacking requested response
fixes #618
2022-05-11 21:30:47 +02:00
GlassedSilver
24659213c2 Make fix-scripts executable by default 2022-05-09 22:41:51 +02:00
GlassedSilver
2354749c2f 002-fix_dupes_per_archive fix script: small impr. 2022-05-09 07:10:15 +02:00
GlassedSilver
5cd3669634 002-fix_dupes_per_archive: Added helpful advice 2022-05-09 06:33:14 +02:00
GlassedSilver
2328502c0d 002-fix_dupes_per_archive: Add info messsages 2022-05-09 06:16:32 +02:00
GlassedSilver
301d8a6ae3 Adding fix script for dupe lines in archives 2022-05-09 06:04:21 +02:00
Glassed Silver
8529fe152c Merge branch 'Tzahi12345:master' into master 2022-05-08 08:12:52 +02:00
Glassed Silver
8dac9d1806 Merge pull request #609 from martadinata666/docker-nodejs16-upgrade
Docker nodejs16 upgrade
2022-05-08 08:11:02 +02:00
Glassed Silver
0f6742f11b Merge pull request #611 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-05-08 08:05:36 +02:00
Glassed Silver
d6aaca9233 Merge branch 'master' into docker-nodejs16-upgrade 2022-05-07 11:36:27 +02:00
Dedy Martadinata S
7333edf6c8 Update Dockerfile 2022-05-07 12:31:52 +07:00
Dedy Martadinata S
5d9cb19bde Update Dockerfile 2022-05-07 12:21:05 +07:00
Dedy Martadinata S
a21dc85d15 revert script 2022-05-07 12:10:23 +07:00
Dedy Martadinata S
a57a25133c go to 20.04 2022-05-07 10:57:47 +07:00
Dedy Martadinata S
d6f5b87d3f also correctly create user home 2022-05-07 10:41:53 +07:00
Dedy Martadinata S
2678215e08 fetch ffmpeg without script 2022-05-07 10:34:27 +07:00
Heimen Stoffels
95203a47d0 Translated using Weblate (Dutch)
Currently translated at 100.0% (324 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2022-05-06 14:11:54 +02:00
Eric
7f4119febe Translated using Weblate (Chinese (Simplified))
Currently translated at 75.3% (244 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-05-06 14:11:54 +02:00
Tzahi12345
02f758c33d Translated using Weblate (Spanish)
Currently translated at 81.4% (264 of 324 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2022-05-06 14:11:54 +02:00
Glassed Silver
554f7c9787 Merge pull request #610 from Tzahi12345/docker-revert-ubuntu-version
Reverted ubuntu version to 20.04
2022-05-06 04:05:33 +02:00
Isaac Abadi
d37287541f Reverted ubuntu version to 20.04 2022-05-05 21:37:02 -04:00
Dedy Martadinata S
da226df72a move to ubuntu as requested
Use setup node 16, as LTS im afraid it will jump to node 18 few months later.
2022-05-06 08:16:48 +07:00
Dedy Martadinata S
6199157687 Update Dockerfile 2022-05-06 00:02:13 +07:00
Dedy Martadinata S
d2e1b04326 check again 2022-05-05 23:43:13 +07:00
Glassed Silver
feff8b2461 revert pm2 config cluster_mode
at least for now, implementation needs more research
2022-05-05 18:24:06 +02:00
Dedy Martadinata S
4bff50a5f0 copy all backend 2022-05-05 22:02:05 +07:00
Dedy Martadinata S
6ffa9d1ffd Some clean up and restructure 2022-05-05 21:42:52 +07:00
Dedy Martadinata S
5a80b7aafa rename to prevent confusion 2022-05-05 21:39:24 +07:00
Glassed Silver
4d00960fcf Merge pull request #607 from Tzahi12345/fix-603
Startup crash hotfix
2022-05-05 16:06:58 +02:00
Tzahi12345
6e8ca9d843 Fixed bug that caused verifyBinaryExistsLinux to crash the server on startup 2022-05-05 09:27:56 -04:00
GlassedSilver
9ab15dd5dd Added weekly (TUES) Docker Build and Push 2022-05-05 13:24:35 +02:00
Glassed Silver
76e4635338 Merge branch 'Tzahi12345:master' into master 2022-05-05 13:20:27 +02:00
Tzahi12345
83a9d93ce5 Merge pull request #602 from Tzahi12345/heroku-fixes-and-repo-cleanup
Various bug fixes and heroku improvements
2022-05-05 02:47:45 -04:00
Isaac Abadi
2707b09627 Merge branch 'db-bug-fixes' of https://github.com/Tzahi12345/YoutubeDL-Material into heroku-fixes-and-repo-cleanup 2022-05-05 02:36:00 -04:00
Isaac Abadi
8118906b0a Binary path is now confirmed for linux, solves #601 2022-05-05 02:31:28 -04:00
Isaac Abadi
2ad42ebf27 Fixed issue where python couldn't be found 2022-05-05 02:29:31 -04:00
Isaac Abadi
2b3490e52c Updated heroku dockerfile 2022-05-05 01:08:43 -04:00
Isaac Abadi
55fd60acd3 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into heroku-fixes-and-repo-cleanup 2022-05-05 01:00:35 -04:00
Glassed Silver
c97b88614f Merge branch 'Tzahi12345:master' into master 2022-05-05 00:39:16 +02:00
GlassedSilver
8738b13cd1 Usage of PM2 Cluster Mode to increase performance 2022-05-05 00:38:40 +02:00
Isaac Abadi
02e683add9 Cleaned up unused files 2021-09-21 22:35:49 -06:00
Isaac Abadi
d90b2d3687 Added back stack field in app.json 2021-09-21 21:01:53 -06:00
Isaac Abadi
32b68033e8 Removed stack field from app.json 2021-09-21 21:01:01 -06:00
Isaac Abadi
08027a5c0b Added run statement to heroku.yml 2021-09-21 20:49:38 -06:00
Isaac Abadi
25aac19cfb Replaced procfile with heroku.yml, added heroku-only Dockerfile and updated app.json 2021-09-21 20:33:42 -06:00
228 changed files with 15923 additions and 3058 deletions

18
.github/dependabot.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/.github/workflows"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/backend/"
schedule:
interval: "daily"

View File

@@ -0,0 +1,38 @@
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Schedule for five minutes after the hour, every hour
- cron: '5 * * * *'
# By specifying the access of one of the scopes, all of those that are not
# specified are set to 'none'.
permissions:
issues: write
jobs:
noResponse:
runs-on: ubuntu-latest
if: ${{ github.repository == 'Tzahi12345/YoutubeDL-Material' }}
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further. We will re-open this issue if you provide us
with the requested information with a comment under this issue.
Thank you for your understanding and for trying to help make this application
a better one!
# Number of days of inactivity before an issue is closed for lack of response.
daysUntilClose: 21
# Label requiring a response.
responseRequiredLabel: "💬 response-needed"

View File

@@ -6,19 +6,25 @@ on:
tags:
description: 'Docker tags'
required: true
release:
types: [published]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: Set hash
id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json
id: create-json
uses: jsdaniell/create-json@1.1.2
@@ -26,15 +32,49 @@ jobs:
name: "version.json"
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: Set image tag
id: tags
run: |
if [ "${{ github.event.inputs.tags }}" != "" ]; then
echo "::set-output name=tags::${{ github.event.inputs.tags }}"
elif [ ${{ github.event.action }} == "release" ]; then
echo "::set-output name=tags::${{ github.event.release.tag_name }}"
else
echo "Unknown workflow trigger: ${{ github.event.action }}! Cannot determine default tag."
exit 1
fi
- name: Generate Docker image metadata
id: docker-meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
tags: |
type=raw,value=${{ steps.tags.outputs.tags }}
type=raw,value=latest
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build & push images
uses: docker/build-push-action@v2
with:
@@ -42,4 +82,5 @@ jobs:
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true
tags: ${{ github.event.inputs.tags }}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}

View File

@@ -13,6 +13,9 @@ on:
- '**.pem'
- '.dockerignore'
- '.gitignore'
schedule:
- cron: '34 4 * * 2'
workflow_dispatch:
jobs:
build-and-push:
@@ -20,12 +23,15 @@ jobs:
steps:
- name: checkout code
uses: actions/checkout@v2
- name: Set hash
id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json
id: create-json
uses: jsdaniell/create-json@1.1.2
@@ -33,15 +39,42 @@ jobs:
name: "version.json"
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: Generate Docker image metadata
id: docker-meta
uses: docker/metadata-action@v4
# Defaults:
# DOCKERHUB_USERNAME : tzahi12345
# DOCKERHUB_REPO : youtubedl-material
# DOCKERHUB_MASTER_TAG: nightly
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
tags: |
type=raw,${{secrets.DOCKERHUB_MASTER_TAG}}-{{ date 'YYYY-MM-DD' }}
type=raw,${{secrets.DOCKERHUB_MASTER_TAG}}
type=sha,prefix=sha-,format=short
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build & push images
uses: docker/build-push-action@v2
with:
@@ -49,8 +82,5 @@ jobs:
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true
# Defaults:
# DOCKERHUB_USERNAME : tzahi12345
# DOCKERHUB_REPO : youtubedl-material
# DOCKERHUB_MASTER_TAG: nightly
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{secrets.DOCKERHUB_MASTER_TAG}}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}

View File

@@ -1,72 +1,70 @@
FROM ubuntu:22.04 AS ffmpeg
# Fetching our ffmpeg
FROM ubuntu:23.04 AS ffmpeg
ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability
COPY ffmpeg-fetch.sh .
RUN sh ./ffmpeg-fetch.sh
COPY docker-build.sh .
RUN sh ./docker-build.sh
#--------------# Stage 2
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
# Go to 20.04
FROM ubuntu:23.04 AS base
ARG DEBIAN_FRONTEND=noninteractive
ENV UID=1000
ENV GID=1000
ENV USER=youtube
ENV NO_UPDATE_NOTIFIER=true
ENV PM2_HOME=/app/pm2
ENV ALLOW_CONFIG_MUTATIONS=true
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
apt update && \
apt install -y --no-install-recommends curl ca-certificates tzdata && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt install -y --no-install-recommends nodejs && \
npm -g install npm n && \
n 16.14.2 && \
apt clean && \
rm -rf /var/lib/apt/lists/*
FROM ubuntu:22.04 as frontend
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install \
curl \
gnupg \
# Ubuntu 22.04 ships Node.JS 12 by default :)
nodejs \
# needed on 21.10 and before, maybe not on 22.04 YARN: brings along npm, solves dependency conflicts,
# spares us this spaghetti approach: https://stackoverflow.com/a/60547197
npm && \
apt-get install -f && \
npm config set strict-ssl false && \
npm install -g @angular/cli
# Build frontend
FROM base as frontend
RUN npm install -g @angular/cli
WORKDIR /build
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN npm run build
RUN npm install && \
npm run build && \
ls -al /build/backend/public
#--------------# Final Stage
FROM ubuntu:22.04
ENV UID=1000 \
GID=1000 \
USER=youtube \
NO_UPDATE_NOTIFIER=true
ENV DEBIAN_FRONTEND=noninteractive
RUN groupadd -g $GID $USER && useradd --system -g $USER --uid $UID $USER
RUN apt-get update && apt-get -y install \
npm \
python2 \
python3 \
gosu \
atomicparsley && \
apt-get install -f && \
apt-get autoremove --purge && \
apt-get autoremove && \
apt-get clean && \
rm -rf /var/lib/apt
# Install backend deps
FROM base as backend
WORKDIR /app
COPY [ "backend/","/app/" ]
RUN npm config set strict-ssl false && \
npm install --prod && \
ls -al
# Final image
FROM base
RUN npm install -g pm2 && \
apt update && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install tcd
WORKDIR /app
# User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
ENV PM2_HOME=/app/pm2
RUN npm config set strict-ssl false && \
npm install pm2 -g && \
npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
RUN chmod +x /app/fix-scripts/*.sh
# Add some persistence data
#VOLUME ["/app/appdata"]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "pm2-runtime", "pm2.config.js" ]
CMD [ "npm","start" ]

2
Dockerfile.heroku Normal file
View File

@@ -0,0 +1,2 @@
FROM tzahi12345/youtubedl-material:latest
CMD [ "npm", "start" ]

View File

@@ -1 +0,0 @@
web: npm start --prefix backend

View File

@@ -97,6 +97,11 @@ paths:
summary: Get all files
description: Gets all files and playlists stored in the db
operationId: get-getAllFiles
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetAllFilesRequest'
responses:
'200':
description: OK
@@ -129,6 +134,27 @@ paths:
description: User is not authorized to view the file.
security:
- Auth query parameter: []
/api/updateFile:
post:
tags:
- files
summary: Updates file database object
description: Updates a file db object using its uid and a change object.
operationId: post-updateFile
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/enableSharing:
post:
tags:
@@ -841,17 +867,10 @@ paths:
- Auth query parameter: []
tags:
- downloader
/api/clearFinishedDownloads:
/api/clearDownloads:
post:
tags:
- downloader
summary: Clear finished downloads
operationId: post-api-clear-finished-downloads
requestBody:
content:
application/json:
schema:
type: object
summary: Clear multiple downloads
operationId: post-api-clear-downloads
responses:
'200':
description: OK
@@ -859,8 +878,17 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ClearDownloadsRequest'
description: ''
description: "Clears multiple downloads based on a given filter."
security:
- Auth query parameter: []
tags:
- downloader
/api/getTask:
post:
summary: Get info for one task
@@ -1507,6 +1535,8 @@ components:
properties:
success:
type: boolean
error:
type: string
FileType:
type: string
enum:
@@ -1607,6 +1637,15 @@ components:
type: array
items:
$ref: '#/components/schemas/Download'
ClearDownloadsRequest:
type: object
properties:
clear_finished:
type: boolean
clear_paused:
type: boolean
clear_errors:
type: boolean
GetTaskRequest:
type: object
properties:
@@ -1690,6 +1729,41 @@ components:
description: All video playlists
items:
$ref: '#/components/schemas/Playlist'
GetAllFilesRequest:
type: object
properties:
sort:
$ref: '#/components/schemas/Sort'
range:
type: array
items:
type: number
description: Two elements allowed, start index and end index
minItems: 2
maxItems: 2
text_search:
type: string
description: Filter files by title
file_type_filter:
$ref: '#/components/schemas/FileTypeFilter'
sub_id:
type: string
description: Include if you want to filter by subscription
Sort:
type: object
properties:
by:
type: string
description: Property to sort by
order:
type: number
description: 1 for ascending, -1 for descending
FileTypeFilter:
type: string
enum:
- audio_only
- video_only
- both
GetAllFilesResponse:
required:
- files
@@ -1727,6 +1801,18 @@ components:
type: boolean
file:
$ref: '#/components/schemas/DatabaseFile'
UpdateFileRequest:
required:
- uid
- change_obj
type: object
properties:
uid:
type: string
description: Video UID
change_obj:
type: object
description: Object with fields to update as keys and their new values
SharingToggle:
required:
- uid
@@ -1740,7 +1826,6 @@ components:
required:
- name
- url
- streamingOnly
type: object
properties:
name:
@@ -1853,7 +1938,6 @@ components:
- uids
- playlistName
- thumbnailURL
- type
type: object
properties:
playlistName:
@@ -1862,8 +1946,6 @@ components:
type: array
items:
type: string
type:
$ref: '#/components/schemas/FileType'
thumbnailURL:
type: string
CreatePlaylistResponse:
@@ -1893,15 +1975,17 @@ components:
required:
- playlist
- success
- type
type: object
properties:
playlist:
$ref: '#/components/schemas/Playlist'
type:
$ref: '#/components/schemas/FileType'
success:
type: boolean
file_objs:
type: array
description: File objects for every uid in the playlist's uids property, in the same order
items:
$ref: '#/components/schemas/DatabaseFile'
GetPlaylistsRequest:
type: object
properties:
@@ -1926,13 +2010,10 @@ components:
DeletePlaylistRequest:
required:
- playlist_id
- type
type: object
properties:
playlist_id:
type: string
type:
$ref: '#/components/schemas/FileType'
DownloadFileRequest:
type: object
properties:
@@ -2153,7 +2234,6 @@ components:
type: boolean
result:
allOf:
- $ref: '#/components/schemas/file'
- type: object
properties:
formats:
@@ -2311,6 +2391,9 @@ components:
type: string
thumbnailURL:
type: string
description: Backup if thumbnailPath is not defined
thumbnailPath:
type: string
isAudio:
type: boolean
duration:
@@ -2322,6 +2405,7 @@ components:
type: string
size:
type: number
description: In bytes
path:
type: string
upload_date:
@@ -2330,6 +2414,22 @@ components:
type: string
sharingEnabled:
type: boolean
category:
$ref: '#/components/schemas/Category'
view_count:
type: number
local_view_count:
type: number
sub_id:
type: string
registered:
type: number
height:
type: number
description: In pixels, only for videos
abr:
type: number
description: In Kbps
Playlist:
required:
- uids
@@ -2359,6 +2459,8 @@ components:
type: number
user_uid:
type: string
auto:
type: boolean
Download:
required:
- url
@@ -2409,6 +2511,8 @@ components:
type: string
sub_name:
type: string
prefetched_info:
type: object
Task:
required:
- key
@@ -2423,6 +2527,8 @@ components:
properties:
key:
type: string
title:
type: string
last_ran:
type: number
last_confirmed:
@@ -2503,7 +2609,6 @@ components:
- url
- type
- user_uid
- streamingOnly
- isPlaylist
- videos
type: object
@@ -2519,8 +2624,6 @@ components:
user_uid:
type: string
nullable: true
streamingOnly:
type: boolean
isPlaylist:
type: boolean
archive:
@@ -2545,28 +2648,6 @@ components:
type: string
passhash:
type: string
files:
type: object
properties:
audio:
type: array
items:
$ref: '#/components/schemas/file'
video:
type: array
items:
$ref: '#/components/schemas/file'
playlists:
type: object
properties:
audio:
type: array
items:
$ref: '#/components/schemas/file'
video:
type: array
items:
$ref: '#/components/schemas/file'
subscriptions:
type: array
items:

View File

@@ -12,16 +12,6 @@ Now with [Docker](#Docker) support!
<hr>
### USAGE OF THE NIGHTLY BUILDS IS HIGHLY RECOMMENDED.
For much better scaling with large datasets please run your YTDL-M instance with a MongoDB backend rather than the json file-based default.
It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
The (closed) issues as well as the project's Wiki will give you good starting points for your journey!
For MongoDB specifically there is [this little guide](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
<hr>
## Getting Started
Check out the prerequisites, and go to the installation section. Easy as pie!
@@ -58,6 +48,7 @@ sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
### Installing
@@ -91,7 +82,7 @@ Alternatively, you can port forward the port specified in the config (defaults t
### Host-specific instructions
If you're on a Synology NAS, unRAID or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
If you're on a Synology NAS, unRAID, Raspberry Pi 4 or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
### Setup
@@ -102,8 +93,6 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
### Custom UID/GID
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
@@ -114,6 +103,12 @@ environment:
GID: YOUR_GID
```
## MongoDB
For much better scaling with large datasets please run your YoutubeDL-Material instance with MongoDB backend rather than the json file-based default. It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
[Tutorial](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
## API
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)

View File

@@ -2,16 +2,16 @@
## Supported Versions
Currently all work on this project goes into the nightly builds.
4.2's RELEASE build is now quite old and should be considered legacy.
We urge users to use the nightly releases, because the project
constantly sees fixes.
If you would like to see the latest updates, use the `nightly` tag on Docker.
| Version | Supported |
| ------------- | ------------------ |
| 4.2 Nightlies | :white_check_mark: |
| 4.2 Release | :x: |
| < 4.2 | :x: |
If you'd like to stick with more stable releases, use the `latest` tag on Docker or download the [latest release here](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest).
| Version | Supported |
| -------------------- | ------------------ |
| 4.3 Docker Nightlies | :white_check_mark: |
| 4.3 Release | :white_check_mark: |
| 4.2 Release | :x: |
| < 4.2 | :x: |
## Reporting a Vulnerability

View File

@@ -30,7 +30,8 @@
"src/backend"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"src/bootstrap.min.css"
],
"scripts": [],
"vendorChunk": true,
@@ -118,7 +119,8 @@
"src/backend"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"src/bootstrap.min.css"
],
"scripts": []
},
@@ -151,7 +153,8 @@
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
"src/styles.scss"
"src/styles.scss",
"src/bootstrap.min.css"
],
"assets": [
"src/assets",

View File

@@ -2,6 +2,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",
"stack": "container",
"logo": "https://i.imgur.com/GPzvPiU.png",
"keywords": ["youtube-dl", "youtubedl-material", "nodejs"]
}

View File

@@ -68,7 +68,8 @@ db.defaults(
configWriteFlag: false,
downloads: {},
subscriptions: [],
files_to_db_migration_complete: false
files_to_db_migration_complete: false,
tasks_manager_role_migration_complete: false
}).write();
users_db.defaults(
@@ -101,7 +102,6 @@ let backendPort = null;
let useDefaultDownloadingAgent = null;
let customDownloadingAgent = null;
let allowSubscriptions = null;
let archivePath = path.join(__dirname, 'appdata', 'archives');
// other needed values
let url_domain = null;
@@ -148,16 +148,11 @@ if (fs.existsSync('version.json')) {
// don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value();
let writeConfigMode = process.env.write_ytdl_config;
// checks if config exists, if not, a config is auto generated
config_api.configExistsCheck();
if (writeConfigMode) {
setAndLoadConfig();
} else {
loadConfig();
}
setAndLoadConfig();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
@@ -188,13 +183,22 @@ async function checkMigrations() {
if (!new_db_system_migration_complete) {
logger.info('Beginning migration: 4.2->4.3+')
let success = await db_api.importJSONToDB(db.value(), users_db.value());
await tasks_api.setupTasks(); // necessary as tasks were not properly initialized at first
// sets migration to complete
db.set('new_db_system_migration_complete', true).write();
if (success) { logger.info('4.2->4.3+ migration complete!'); }
else { logger.error('Migration failed: 4.2->4.3+'); }
}
const tasks_manager_role_migration_complete = db.get('tasks_manager_role_migration_complete').value();
if (!tasks_manager_role_migration_complete) {
logger.info('Checking if tasks manager role permissions exist for admin user...');
const success = await auth_api.changeRolePermissions('admin', 'tasks_manager', 'yes');
if (success) logger.info('Task manager permissions check complete!');
else logger.error('Failed to auto add tasks manager permissions to admin role!');
db.set('tasks_manager_role_migration_complete', true).write();
}
return true;
}
@@ -249,14 +253,6 @@ async function startServer() {
});
}
async function restartServer(is_update = false) {
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
// the following line restarts the server through nodemon
fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only');
process.exit(1);
}
async function updateServer(tag) {
// no tag provided means update to the latest version
if (!tag) {
@@ -297,7 +293,7 @@ async function updateServer(tag) {
updating: true,
'details': 'Update complete! Restarting server...'
}
restartServer(true);
utils.restartServer(true);
}, err => {
logger.error(err);
updaterStatus = {
@@ -492,8 +488,9 @@ async function setAndLoadConfig() {
}
async function setConfigFromEnv() {
let config_items = getEnvConfigItems();
let success = config_api.setConfigItems(config_items);
const config_items = getEnvConfigItems();
if (!config_items || config_items.length === 0) return true;
const success = config_api.setConfigItems(config_items);
if (success) {
logger.info('Config items set using ENV variables.');
await utils.wait(100);
@@ -508,12 +505,13 @@ async function loadConfig() {
loadConfigValues();
// connect to DB
await db_api.connectToDB();
if (!config_api.getConfigItem('ytdl_use_local_db'))
await db_api.connectToDB();
db_api.database_initialized = true;
db_api.database_initialized_bs.next(true);
// creates archive path if missing
await fs.ensureDir(archivePath);
await fs.ensureDir(utils.getArchiveFolder());
// check migrations
await checkMigrations();
@@ -583,7 +581,11 @@ async function watchSubscriptions() {
if (!subscriptions) return;
const valid_subscriptions = subscriptions.filter(sub => !sub.paused);
// auto pause deprecated streamingOnly mode
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
subscriptions_api.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
const valid_subscriptions = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly);
let subscriptions_amount = valid_subscriptions.length;
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
@@ -676,6 +678,7 @@ async function getUrlInfos(url) {
async function startYoutubeDL() {
// auto update youtube-dl
youtubedl_api.verifyBinaryExistsLinux();
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
}
@@ -764,7 +767,7 @@ app.get('/api/versionInfo', (req, res) => {
app.post('/api/restartServer', optionalJwt, (req, res) => {
// delayed by a little bit so that the client gets a response
setTimeout(() => {restartServer()}, 100);
setTimeout(() => {utils.restartServer()}, 100);
res.send({success: true});
});
@@ -919,11 +922,11 @@ app.post('/api/getFile', optionalJwt, async function (req, res) {
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
// these are returned
let files = null;
let playlists = null;
let sort = req.body.sort;
let range = req.body.range;
let text_search = req.body.text_search;
let file_type_filter = req.body.file_type_filter;
const sort = req.body.sort;
const range = req.body.range;
const text_search = req.body.text_search;
const file_type_filter = req.body.file_type_filter;
const sub_id = req.body.sub_id;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const filter_obj = {user_uid: uuid};
@@ -936,27 +939,42 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
}
}
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
files = await db_api.getRecords('files', filter_obj, false, sort, range, text_search);
let file_count = await db_api.getRecords('files', filter_obj, true);
playlists = await db_api.getRecords('playlists', {user_uid: uuid});
const categories = await categories_api.getCategoriesAsPlaylists(files);
if (categories) {
playlists = playlists.concat(categories);
}
const file_count = await db_api.getRecords('files', filter_obj, true);
files = JSON.parse(JSON.stringify(files));
res.send({
files: files,
file_count: file_count,
playlists: playlists
});
});
app.post('/api/updateFile', optionalJwt, async function (req, res) {
const uid = req.body.uid;
const change_obj = req.body.change_obj;
const file = await db_api.updateRecord('files', {uid: uid}, change_obj);
if (!file) {
res.send({
success: false,
error: 'File could not be found'
});
} else {
res.send({
success: true
});
}
});
app.post('/api/checkConcurrentStream', async (req, res) => {
const uid = req.body.uid;
@@ -1264,7 +1282,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
subscription = JSON.parse(JSON.stringify(subscription));
// get sub videos
if (subscription.name && !subscription.streamingOnly) {
if (subscription.name) {
var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos;
subscription['videos'] = parsed_files;
// loop through files for extra processing
@@ -1274,19 +1292,6 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
}
res.send({
subscription: subscription,
files: parsed_files
});
} else if (subscription.name && subscription.streamingOnly) {
// return list of videos
let parsed_files = [];
if (subscription.videos) {
for (let i = 0; i < subscription.videos.length; i++) {
const video = subscription.videos[i];
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date, video.view_count, video.height, video.abr));
}
}
res.send({
subscription: subscription,
files: parsed_files
@@ -1331,9 +1336,8 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => {
app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let playlistName = req.body.playlistName;
let uids = req.body.uids;
let type = req.body.type;
const new_playlist = await db_api.createPlaylist(playlistName, uids, type, req.isAuthenticated() ? req.user.uid : null);
const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
res.send({
new_playlist: new_playlist,
@@ -1361,7 +1365,6 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
res.send({
playlist: playlist,
file_objs: file_objs,
type: playlist && playlist.type,
success: !!playlist
});
});
@@ -1372,7 +1375,7 @@ app.post('/api/getPlaylists', optionalJwt, async (req, res) => {
let playlists = await db_api.getRecords('playlists', {user_uid: uuid});
if (include_categories) {
const categories = await categories_api.getCategoriesAsPlaylists(files);
const categories = await categories_api.getCategoriesAsPlaylists();
if (categories) {
playlists = playlists.concat(categories);
}
@@ -1676,9 +1679,15 @@ app.post('/api/download', optionalJwt, async (req, res) => {
}
});
app.post('/api/clearFinishedDownloads', optionalJwt, async (req, res) => {
app.post('/api/clearDownloads', optionalJwt, async (req, res) => {
const user_uid = req.isAuthenticated() ? req.user.uid : null;
const success = db_api.removeAllRecords('download_queue', {finished: true, user_uid: user_uid});
const clear_finished = req.body.clear_finished;
const clear_paused = req.body.clear_paused;
const clear_errors = req.body.clear_errors;
let success = true;
if (clear_finished) success &= await db_api.removeAllRecords('download_queue', {finished: true, user_uid: user_uid});
if (clear_paused) success &= await db_api.removeAllRecords('download_queue', {paused: true, user_uid: user_uid});
if (clear_errors) success &= await db_api.removeAllRecords('download_queue', {error: {$ne: null}, user_uid: user_uid});
res.send({success: success});
});
@@ -1802,6 +1811,7 @@ app.post('/api/updateTaskData', optionalJwt, async (req, res) => {
app.post('/api/getDBBackups', optionalJwt, async (req, res) => {
const backup_dir = path.join('appdata', 'db_backup');
fs.ensureDirSync(backup_dir);
const db_backups = [];
const candidate_backups = await utils.recFindByExt(backup_dir, 'bak', null, [], false);

View File

@@ -31,7 +31,8 @@
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false
@@ -63,7 +64,7 @@
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
},
"Advanced": {
"default_downloader": "youtube-dl",
"default_downloader": "yt-dlp",
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,

View File

@@ -171,8 +171,12 @@ exports.registerUser = async function(req, res) {
exports.login = async (username, password) => {
// even if we're using LDAP, we still want users to be able to login using internal credentials
const user = await db_api.getRecord('users', {name: username});
if (!user) { logger.error(`User ${username} not found`); return false }
if (!user) {
if (config_api.getConfigItem('ytdl_auth_method') === 'internal') logger.error(`User ${username} not found`);
return false;
}
if (user.auth_method && user.auth_method !== 'internal') { return false }
return await bcrypt.compare(password, user.passhash) ? user : false;
}
@@ -357,7 +361,6 @@ exports.userHasPermission = async function(user_uid, permission) {
logger.error('Invalid role ' + role);
return false;
}
const role_permissions = (await db_api.getRecords('roles'))['permissions'];
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
@@ -372,7 +375,8 @@ exports.userHasPermission = async function(user_uid, permission) {
}
// no overrides, let's check if the role has the permission
if (role_permissions.includes(permission)) {
const role_has_permission = await exports.roleHasPermissions(role, permission);
if (role_has_permission) {
return true;
} else {
logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
@@ -380,6 +384,16 @@ exports.userHasPermission = async function(user_uid, permission) {
}
}
exports.roleHasPermissions = async function(role, permission) {
const role_obj = await db_api.getRecord('roles', {key: role})
if (!role) {
logger.error(`Role ${role} does not exist!`);
}
const role_permissions = role_obj['permissions'];
if (role_permissions && role_permissions.includes(permission)) return true;
else return false;
}
exports.userPermissions = async function(user_uid) {
let user_permissions = [];
const user_obj = await db_api.getRecord('users', ({uid: user_uid}));

View File

@@ -55,17 +55,18 @@ async function getCategories() {
return categories ? categories : null;
}
async function getCategoriesAsPlaylists(files = null) {
async function getCategoriesAsPlaylists() {
const categories_as_playlists = [];
const available_categories = await getCategories();
if (available_categories && files) {
if (available_categories) {
for (let category of available_categories) {
const files_that_match = utils.addUIDsToCategory(category, files);
const files_that_match = await db_api.getRecords('files', {'category.uid': category['uid']});
if (files_that_match && files_that_match.length > 0) {
category['thumbnailURL'] = files_that_match[0].thumbnailURL;
category['thumbnailPath'] = files_that_match[0].thumbnailPath;
category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
category['id'] = category['uid'];
category['auto'] = true;
categories_as_playlists.push(category);
}
}

View File

@@ -127,7 +127,7 @@ function setConfigItem(key, value) {
success = setConfigFile(config_json);
return success;
};
}
function setConfigItems(items) {
let success = false;
@@ -206,7 +206,8 @@ const DEFAULT_CONFIG = {
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false
@@ -238,7 +239,7 @@ const DEFAULT_CONFIG = {
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
},
"Advanced": {
"default_downloader": "youtube-dl",
"default_downloader": "yt-dlp",
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,

View File

@@ -102,9 +102,13 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API'
},
'ytdl_twitch_api_key': {
'key': 'ytdl_twitch_api_key',
'path': 'YoutubeDLMaterial.API.twitch_API_key'
'ytdl_twitch_client_id': {
'key': 'ytdl_twitch_client_id',
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
},
'ytdl_twitch_client_secret': {
'key': 'ytdl_twitch_client_secret',
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
},
'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat',
@@ -217,7 +221,8 @@ exports.AVAILABLE_PERMISSIONS = [
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
'downloads_manager',
'tasks_manager'
];
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
@@ -301,4 +306,4 @@ const YTDL_ARGS_WITH_VALUES = [
// we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
exports.CURRENT_VERSION = 'v4.2';
exports.CURRENT_VERSION = 'v4.3';

View File

@@ -198,7 +198,7 @@ async function registerFileDBManual(file_object) {
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
await exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object;
}
@@ -357,7 +357,7 @@ exports.addMetadataPropertyToDB = async (property_key) => {
}
}
exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL'];
@@ -366,7 +366,6 @@ exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
uids: uids,
id: uuid(),
thumbnailURL: thumbnailToUse,
type: type,
registered: Date.now(),
randomize_order: false
};
@@ -387,9 +386,9 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal
if (!playlist) {
playlist = await exports.getRecord('categories', {uid: playlist_id});
if (playlist) {
// category found
const files = await exports.getFiles(user_uid);
utils.addUIDsToCategory(playlist, files);
const uids = (await exports.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
playlist['uids'] = uids;
playlist['auto'] = true;
}
}
@@ -495,8 +494,7 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
const archive_path = utils.getArchiveFolder(type, uuid);
// get ID from JSON
@@ -504,14 +502,8 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
let id = null;
if (jsonobj) id = jsonobj.id;
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if (await fs.pathExists(archive_path)) {
const line = id ? await utils.removeIDFromArchive(archive_path, id) : null;
if (blacklistMode && line) await writeToBlacklist(type, line);
} else {
logger.info('Could not find archive file for audio files. Creating...');
await fs.close(await fs.open(archive_path, 'w'));
}
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
await utils.deleteFileFromArchive(uid, type, archive_path, id, blacklistMode);
}
if (jsonExists) await fs.unlink(jsonPath);
@@ -629,7 +621,7 @@ exports.bulkInsertRecordsIntoTable = async (table, docs) => {
exports.getRecord = async (table, filter_obj) => {
// local db override
if (using_local_db) {
return applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value();
return exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value();
}
return await database.collection(table).findOne(filter_obj);
@@ -638,7 +630,7 @@ exports.getRecord = async (table, filter_obj) => {
exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => {
// local db override
if (using_local_db) {
let cursor = filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
let cursor = filter_obj ? exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
if (sort) {
cursor = cursor.sort((a, b) => (a[sort['by']] > b[sort['by']] ? sort['order'] : sort['order']*-1));
}
@@ -664,7 +656,7 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort
exports.updateRecord = async (table, filter_obj, update_obj) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
return true;
}
@@ -677,7 +669,7 @@ exports.updateRecord = async (table, filter_obj, update_obj) => {
exports.updateRecords = async (table, filter_obj, update_obj) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
return true;
}
@@ -722,7 +714,7 @@ exports.bulkUpdateRecords = async (table, key_label, update_obj) => {
exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write();
return true;
}
@@ -733,7 +725,7 @@ exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write();
return true;
}
@@ -746,7 +738,7 @@ exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
exports.removeRecord = async (table, filter_obj) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
return true;
}
@@ -757,7 +749,7 @@ exports.removeRecord = async (table, filter_obj) => {
// exports.removeRecordsByUIDBulk = async (table, uids) => {
// // local db override
// if (using_local_db) {
// applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
// exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
// return true;
// }
@@ -821,7 +813,7 @@ exports.removeAllRecords = async (table = null, filter_obj = null) => {
if (using_local_db) {
for (let i = 0; i < tables_to_remove.length; i++) {
const table_to_remove = tables_to_remove[i];
if (filter_obj) applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
if (filter_obj) exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
else local_db.assign({[table_to_remove]: []}).write();
logger.debug(`Successfully removed records from ${table_to_remove}`);
}
@@ -933,6 +925,7 @@ exports.importJSONToDB = async (db_json, users_json) => {
const createFilesRecords = (files, subscriptions) => {
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i];
if (!subscription['videos']) continue;
subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined}));
files = files.concat(subscriptions[i]['videos']);
}
@@ -993,7 +986,7 @@ exports.backupDB = async () => {
const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`;
const path_to_backups = path.join(backup_dir, backup_file_name);
logger.verbose(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
logger.info(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
const table_to_records = {};
for (let i = 0; i < tables_list.length; i++) {
@@ -1040,10 +1033,11 @@ exports.transferDB = async (local_to_remote) => {
table_to_records[table] = await exports.getRecords(table);
}
logger.info('Backup up DB...');
await exports.backupDB(); // should backup always
using_local_db = !local_to_remote;
if (local_to_remote) {
logger.debug('Backup up DB...');
await exports.backupDB();
const db_connected = await exports.connectToDB(5, true);
if (!db_connected) {
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
@@ -1075,8 +1069,13 @@ exports.transferDB = async (local_to_remote) => {
This function is necessary to emulate mongodb's ability to search for null or missing values.
A filter of null or undefined for a property will find docs that have that property missing, or have it
null or undefined. We want that same functionality for the local DB as well
error: {$ne: null}
^ ^
| |
filter_prop filter_prop_value
*/
const applyFilterLocalDB = (db_path, filter_obj, operation) => {
exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
const filter_props = Object.keys(filter_obj);
const return_val = db_path[operation](record => {
if (!filter_props) return true;
@@ -1085,14 +1084,20 @@ const applyFilterLocalDB = (db_path, filter_obj, operation) => {
const filter_prop = filter_props[i];
const filter_prop_value = filter_obj[filter_prop];
if (filter_prop_value === undefined || filter_prop_value === null) {
filtered &= record[filter_prop] === undefined || record[filter_prop] === null
filtered &= record[filter_prop] === undefined || record[filter_prop] === null;
} else {
if (typeof filter_prop_value === 'object') {
if (filter_prop_value['$regex']) {
if ('$regex' in filter_prop_value) {
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
} else if ('$ne' in filter_prop_value) {
filtered &= filter_prop in record && record[filter_prop] !== filter_prop_value['$ne'];
}
} else {
filtered &= record[filter_prop] === filter_prop_value;
// handle case of nested property check
if (filter_prop.includes('.'))
filtered &= utils.searchObjectByString(record, filter_prop) === filter_prop_value;
else
filtered &= record[filter_prop] === filter_prop_value;
}
}
}
@@ -1100,15 +1105,3 @@ const applyFilterLocalDB = (db_path, filter_obj, operation) => {
});
return return_val;
}
// archive helper functions
async function writeToBlacklist(type, line) {
const archivePath = path.join(__dirname, 'appdata', 'archives');
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
// adds newline to the beginning of the line
line.replace('\n', '');
line.replace('\r', '');
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
}

View File

@@ -18,8 +18,6 @@ const db_api = require('./db');
const mutex = new Mutex();
let should_check_downloads = true;
const archivePath = path.join(__dirname, 'appdata', 'archives');
if (db_api.database_initialized) {
setupDownloads();
} else {
@@ -28,7 +26,7 @@ if (db_api.database_initialized) {
});
}
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => {
return await mutex.runExclusive(async () => {
const download = {
url: url,
@@ -37,6 +35,7 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
user_uid: user_uid,
sub_id: sub_id,
sub_name: sub_name,
prefetched_info: prefetched_info,
options: options,
uid: uuid(),
step_index: 0,
@@ -108,6 +107,7 @@ exports.clearDownload = async (download_uid) => {
}
async function handleDownloadError(download_uid, error_message) {
if (!download_uid) return;
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error_message, finished: true, running: false});
}
@@ -186,7 +186,7 @@ async function collectInfo(download_uid) {
let args = await exports.generateArgs(url, type, options, download['user_uid']);
// get video info prior to download
let info = await getVideoInfoByURL(url, args, download_uid);
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
if (!info) {
// info failed, error presumably already recorded
@@ -203,9 +203,12 @@ async function collectInfo(download_uid) {
options.customOutput = category['custom_output'];
options.noRelativePath = true;
args = await exports.generateArgs(url, type, options, download['user_uid']);
info = await getVideoInfoByURL(url, args, download_uid);
args = utils.filterArgs(args, ['--no-simulate']);
info = await exports.getVideoInfoByURL(url, args, download_uid);
}
download['category'] = category;
// setup info required to calculate download progress
const expected_file_size = utils.getExpectedFileSize(info);
@@ -226,7 +229,8 @@ async function collectInfo(download_uid) {
options: options,
files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title']
title: playlist_title ? playlist_title : info['title'],
prefetched_info: null
});
}
@@ -239,6 +243,7 @@ async function downloadQueuedFile(download_uid) {
return new Promise(async resolve => {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
const url = download['url'];
@@ -246,9 +251,11 @@ async function downloadQueuedFile(download_uid) {
const options = download['options'];
const args = download['args'];
const category = download['category'];
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath;
} else if (download['user_uid']) {
fileFolderPath = path.join(usersFolderPath, download['user_uid'], type);
}
fs.ensureDirSync(fileFolderPath);
@@ -350,7 +357,7 @@ async function downloadQueuedFile(download_uid) {
if (file_objs.length > 1) {
// create playlist
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, download['user_uid']);
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
} else if (file_objs.length === 1) {
container = file_objs[0];
} else {
@@ -370,15 +377,23 @@ async function downloadQueuedFile(download_uid) {
// helper functions
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
const globalArgs = config_api.getConfigItem('ytdl_custom_args');
const useCookies = config_api.getConfigItem('ytdl_use_cookies');
const is_audio = type === 'audio';
let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath;
} else if (user_uid) {
fileFolderPath = path.join(usersFolderPath, user_uid, fileFolderPath);
}
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
@@ -388,6 +403,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
// video-specific args
const selectedHeight = options.selectedHeight;
const maxHeight = options.maxHeight;
const heightParam = selectedHeight || maxHeight;
// audio-specific args
const maxBitrate = options.maxBitrate;
@@ -401,8 +418,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format
qualityPath = null;
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
qualityPath = ['-f', 'bestvideo+bestaudio']
}
if (customArgs) {
@@ -410,8 +425,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
} else {
if (customQualityConfiguration) {
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (heightParam && heightParam !== '' && !is_audio) {
qualityPath = ['-f', `'(mp4)[height${maxHeight ? '<' : ''}=${heightParam}]`];
} else if (is_audio) {
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
}
@@ -493,9 +508,11 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
downloadConfig.push('-r', rate_limit);
}
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (default_downloader === 'yt-dlp') {
downloadConfig.push('--no-clean-infojson');
downloadConfig = utils.filterArgs(downloadConfig, ['--print-json']);
// in yt-dlp -j --no-simulate is preferable
downloadConfig.push('--no-clean-info-json', '-j', '--no-simulate');
}
}
@@ -503,11 +520,11 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
// filter out incompatible args
downloadConfig = filterArgs(downloadConfig, is_audio);
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
if (!simulated) logger.verbose(`${default_downloader} args being used: ${downloadConfig.join(',')}`);
return downloadConfig;
}
async function getVideoInfoByURL(url, args = [], download_uid = null) {
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
@@ -562,8 +579,7 @@ async function getVideoInfoByURL(url, args = [], download_uid = null) {
function filterArgs(args, isAudio) {
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
const args_to_remove = isAudio ? video_only_args : audio_only_args;
return args.filter(x => !args_to_remove.includes(x));
return utils.filterArgs(args, isAudio ? video_only_args : audio_only_args);
}
async function checkDownloadPercent(download_uid) {
@@ -625,6 +641,6 @@ function getArchiveFolder(fileFolderPath, options, user_uid) {
} else if (user_uid) {
return path.join(fileFolderPath, 'archives');
} else {
return path.join(archivePath);
return path.join('appdata', 'archives');
}
}

View File

@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
CMD="pm2-runtime pm2.config.js"
CMD="npm start"
# if the first arg starts with "-" pass it to program
if [ "${1#-}" != "$1" ]; then

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
# Date: 2022-05-03
@@ -6,8 +6,7 @@
# If you want to run this script on a bare-metal installation instead of within Docker
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
# USAGE: within your container's bash shell:
# chmod -R +x ./fix-scripts/
# ./fix-scripts/001-fix_download_permissions.sh
# ./fix-scripts/<name of fix-script>
# User defines / Docker env defaults
PATH_SUBS=/app/subscriptions

View File

@@ -0,0 +1,142 @@
#!/bin/bash
# INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M
# Date: 2022-05-09
# If you want to run this script on a bare-metal installation instead of within Docker
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
# USAGE: within your container's bash shell:
# ./fix-scripts/<name of fix-script>
# User defines (NO TRAILING SLASHES) / Docker env defaults
PATH_SUBSARCHIVE=/app/subscriptions/archives
PATH_ONEOFFARCHIVE=/app/appdata/archives
# Backup paths (substitute with your personal preference if you like)
PATH_SUBSARCHIVEBKP=$PATH_SUBSARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
PATH_ONEOFFARCHIVEBKP=$PATH_ONEOFFARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
# Define Colors for TUI
yellow=$(tput setaf 3)
normal=$(tput sgr0)
tput civis # hide the cursor
clear -x
printf "\n"
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
printf "Welcome to the INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M."
printf "\nThis script will cycle through the archive files in the folders mentioned"
printf "\nbelow and remove within each archive the dupe entries. (compact them)"
printf "\nDuring some older builds of YTDL-M the archives could receive dupe"
printf "\nentries and blow up in size, sometimes causing conflicts with download management."
printf '\n%*s' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
printf "\n"
# check whether dirs exist
i=0
[ -d $PATH_SUBSARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found Subscriptions archive directory at ${PATH_SUBSARCHIVE}"
[ -d $PATH_ONEOFFARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found one-off archive directory at ${PATH_ONEOFFARCHIVE}"
# Ask to proceed or cancel, exit on missing paths
case $i in
0)
printf "\n\n Couldn't find any archive location path! \n\nPlease edit this script to configure!"
tput cnorm
exit 2;;
2)
printf "\n\n Found all archive locations. \n\nProceed? (Y/N)";;
*)
printf "\n\n Only found ${i} out of 2 archive locations! Something about this script's config must be wrong. \n\nProceed anyways? (Y/N)";;
esac
old_stty_cfg=$(stty -g)
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
if echo "$answer" | grep -iq "^y" ;then
printf "\n\nRunning jobs now... (this may take a while)\n"
printf "\nBacking up directories...\n"
chars="⣾⣽⣻⢿⡿⣟⣯⣷"
cp -R $PATH_SUBSARCHIVE $PATH_SUBSARCHIVEBKP &
PID=$!
i=1
echo -n ' '
while [ -d /proc/$PID ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.15
done
[ -d $PATH_SUBSARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_SUBSARCHIVE} to ${PATH_SUBSARCHIVEBKP} ($(du -sh $PATH_SUBSARCHIVEBKP | cut -f1))\n"
cp -R $PATH_ONEOFFARCHIVE $PATH_ONEOFFARCHIVEBKP &
PID2=$!
i=1
echo -n ' '
while [ -d /proc/$PID2 ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.1
done
[ -d $PATH_ONEOFFARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_ONEOFFARCHIVE} to ${PATH_ONEOFFARCHIVEBKP} ($(du -sh $PATH_ONEOFFARCHIVEBKP | cut -f1))\n"
printf "\nCompacting files...\n"
tmpfile=$(mktemp) &&
[ -d $PATH_SUBSARCHIVE ] &&
find $PATH_SUBSARCHIVE -name '*.txt' -print0 | while read -d $'\0' file # Set delimiter to null because we want to catch all possible filenames (WE CANNOT CHANGE IFS HERE) - https://stackoverflow.com/a/15931055
do
cp "$file" "$tmpfile"
{ awk '!x[$0]++' "$tmpfile" > "$file"; } & # https://unix.stackexchange.com/questions/159695/how-does-awk-a0-work
PID3=$!
i=1
echo -n ''
while [ -d /proc/$PID3 ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.1
done
BEFORE=$(wc -l < $tmpfile)
AFTER=$(wc -l < $file)
if [[ "$AFTER" -ne "$BEFORE" ]]; then
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
else
printf "\b No action needed for file: ${file}\n"
fi
done
[ -d $PATH_ONEOFFARCHIVE ] &&
find $PATH_ONEOFFARCHIVE -name '*.txt' -print0 | while read -d $'\0' file
do
cp "$file" "$tmpfile" &
awk '!x[$0]++' "$tmpfile" > "$file" &
PID4=$!
i=1
echo -n ''
while [ -d /proc/$PID4 ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.1
done
BEFORE=$(wc -l < $tmpfile)
AFTER=$(wc -l < $file)
if [ "$BEFORE" -ne "$AFTER" ]; then
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
else
printf "\b No action ran for file: ${file}\n"
fi
done
tput cnorm # show the cursor
rm "$tmpfile"
printf "\n\n✔ Done."
printf "\n Please keep in mind that you may still want to"
printf "\n run corruption checks against your archives!\n\n"
exit
else
tput cnorm
printf "\nOkay, bye.\n\n"
exit
fi

1825
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,20 +5,9 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js",
"start": "pm2-runtime --raw pm2.config.js",
"debug": "set YTDL_MODE=debug && node app.js"
},
"nodemonConfig": {
"ignore": [
"*.js",
"appdata/*",
"public/*"
],
"watch": [
"restart_update.json",
"restart_general.json"
]
},
"repository": {
"type": "git",
"url": ""
@@ -47,11 +36,10 @@
"mocha": "^9.2.2",
"moment": "^2.29.2",
"mongodb": "^3.6.9",
"multer": "^1.4.2",
"multer": "1.4.5-lts.1",
"node-fetch": "^2.6.7",
"node-id3": "^0.1.14",
"node-schedule": "^2.1.0",
"nodemon": "^2.0.7",
"passport": "^0.4.1",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",

View File

@@ -6,4 +6,4 @@ module.exports = {
out_file: "/dev/null",
error_file: "/dev/null"
}]
}
}

View File

@@ -178,7 +178,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
]);
if (jsonExists) {
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
retrievedID = fs.readJSONSync(jsonPath)['id'];
await fs.unlink(jsonPath);
}
@@ -196,12 +196,11 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
return 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 (await fs.pathExists(archive_path)) {
utils.removeIDFromArchive(archive_path, retrievedID);
}
if (useArchive && retrievedID) {
const archive_path = utils.getArchiveFolder(sub.type, user_uid, sub);
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
await utils.deleteFileFromArchive(file_uid, sub.type, archive_path, retrievedID, deleteForever);
}
return true;
}
@@ -242,64 +241,22 @@ async function getVideosForSub(sub, user_uid = null) {
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message);
if (err.stderr.includes('This video is unavailable')) {
if (err.stderr.includes('This video is unavailable') || err.stderr.includes('Private video')) {
logger.info('An error was encountered with at least one video, backup method will be used.')
try {
// TODO: reimplement
// const outputs = err.stdout.split(/\r\n|\r|\n/);
// for (let i = 0; i < outputs.length; i++) {
// const output = JSON.parse(outputs[i]);
// await handleOutputJSON(sub, output, i === 0, multiUserMode)
// if (err.stderr.includes(output['id']) && archive_path) {
// // we found a video that errored! add it to the archive to prevent future errors
// if (sub.archive) {
// archive_dir = sub.archive;
// archive_path = path.join(archive_dir, 'archive.txt')
// fs.appendFileSync(archive_path, output['id']);
// }
// }
// }
const outputs = err.stdout.split(/\r\n|\r|\n/);
const files_to_download = await handleOutputJSON(outputs, sub, user_uid);
resolve(files_to_download);
} catch(e) {
logger.error('Backup method failed. See error below:');
logger.error(e);
}
} else {
logger.error('Subscription check failed!');
}
resolve(false);
} else if (output) {
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
await setFreshUploads(sub, user_uid);
checkVideosForFreshUploads(sub, user_uid);
}
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name);
resolve(true);
return;
}
const output_jsons = [];
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
output_jsons.push(output_json);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
}
const files_to_download = await getFilesToDownload(sub, output_jsons);
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
for (let j = 0; j < files_to_download.length; j++) {
const file_to_download = files_to_download[j];
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name);
}
const files_to_download = await handleOutputJSON(output, sub, user_uid);
resolve(files_to_download);
}
});
@@ -309,6 +266,43 @@ async function getVideosForSub(sub, user_uid = null) {
});
}
async function handleOutputJSON(output, sub, user_uid) {
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
await setFreshUploads(sub, user_uid);
checkVideosForFreshUploads(sub, user_uid);
}
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name);
return [];
}
const output_jsons = [];
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
output_jsons.push(output_json);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
}
const files_to_download = await getFilesToDownload(sub, output_jsons);
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
for (let j = 0; j < files_to_download.length; j++) {
const file_to_download = files_to_download[j];
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, file_to_download);
}
return files_to_download;
}
function generateOptionsForSubscriptionDownload(sub, user_uid) {
let basePath = null;
if (user_uid)
@@ -319,10 +313,10 @@ function generateOptionsForSubscriptionDownload(sub, user_uid) {
let default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
const base_download_options = {
selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
maxHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
customFileFolderPath: getAppendedBasePath(sub, basePath),
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
customArchivePath: path.join(basePath, 'archives', sub.name),
additionalArgs: sub.custom_args
}
@@ -389,11 +383,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
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 && !redownload) {
downloadConfig.push('--dateafter', sub.timerange);
}
@@ -418,9 +407,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (default_downloader === 'yt-dlp') {
downloadConfig.push('--no-clean-infojson');
downloadConfig.push('--no-clean-info-json');
}
downloadConfig = utils.filterArgs(downloadConfig, ['--write-comments']);
return downloadConfig;
}
@@ -467,7 +458,7 @@ async function updateSubscription(sub) {
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
subs.forEach(async sub => {
await updateSubscriptionProperty(sub, assignment_obj, sub.user_uid);
await updateSubscriptionProperty(sub, assignment_obj);
});
}
@@ -479,6 +470,7 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
async function setFreshUploads(sub) {
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
if (!sub_files) return;
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub_files.forEach(async file => {
if (current_date === file['upload_date'].replace(/-/g, '')) {

View File

@@ -148,6 +148,7 @@ exports.updateTaskSchedule = async (task_key, schedule) => {
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
if (TASKS[task_key]['job']) {
TASKS[task_key]['job'].cancel();
TASKS[task_key]['job'] = null;
}
if (schedule) {
TASKS[task_key]['job'] = scheduleJob(task_key, schedule);

View File

@@ -1,6 +1,7 @@
var assert = require('assert');
const assert = require('assert');
const low = require('lowdb')
var winston = require('winston');
const winston = require('winston');
const path = require('path');
process.chdir('./backend')
@@ -39,9 +40,29 @@ const utils = require('../utils');
const subscriptions_api = require('../subscriptions');
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
db_api.initialize(db, users_db);
const sample_video_json = {
id: "Sample Video",
title: "Sample Video",
thumbnailURL: "https://sampleurl.jpg",
isAudio: false,
duration: 177.413,
url: "sampleurl.com",
uploader: "Sample Uploader",
size: 2838445,
path: "users\\admin\\video\\Sample Video.mp4",
upload_date: "2017-07-28",
description: null,
view_count: 230,
abr: 128,
thumbnailPath: null,
user_uid: "admin",
uid: "1ada04ab-2773-4dd4-bbdd-3e2d40761c50",
registered: 1628469039377
}
describe('Database', async function() {
describe('Import', async function() {
@@ -214,7 +235,7 @@ describe('Database', async function() {
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
const uid = uuid();
if (i === NUM_RECORDS_TO_ADD/2) random_uid = uid;
test_records.push({"id":"A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J","title":"A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J","thumbnailURL":"https://i.ytimg.com/vi/tt7gP_IW-1w/maxresdefault.jpg","isAudio":true,"duration":312,"url":"https://www.youtube.com/watch?v=tt7gP_IW-1w","uploader":"asapmobVEVO","size":5060157,"path":"audio\\A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J.mp3","upload_date":"2016-05-11","description":"A$AP Mob ft. Juicy J - \"Yamborghini High\" Get it now on:\niTunes: http://smarturl.it/iYAMH?IQid=yt\nListen on Spotify: http://smarturl.it/sYAMH?IQid=yt\nGoogle Play: http://smarturl.it/gYAMH?IQid=yt\nAmazon: http://smarturl.it/aYAMH?IQid=yt\n\nFollow A$AP Mob:\nhttps://www.facebook.com/asapmobofficial\nhttps://twitter.com/ASAPMOB\nhttp://instagram.com/asapmob \nhttp://www.asapmob.com/\n\n#AsapMob #YamborghiniHigh #Vevo #HipHop #OfficialMusicVideo #JuicyJ","view_count":118689353,"height":null,"abr":160,"uid": uid,"registered":1626672120632});
test_records.push({"id":"RandomTextRandomText","title":"RandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomText","thumbnailURL":"https://i.ytimg.com/vi/randomurl/maxresdefault.jpg","isAudio":true,"duration":312,"url":"https://www.youtube.com/watch?v=randomvideo","uploader":"randomUploader","size":5060157,"path":"audio\\RandomTextRandomText.mp3","upload_date":"2016-05-11","description":"RandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomText","view_count":118689353,"height":null,"abr":160,"uid": uid,"registered":1626672120632});
}
const insert_start = Date.now();
let success = await db_api.bulkInsertRecordsIntoTable('test', test_records);
@@ -235,6 +256,30 @@ describe('Database', async function() {
assert(success);
});
});
describe('Local DB Filters', async function() {
it('Basic', async function() {
const result = db_api.applyFilterLocalDB([{test: 'test'}, {test: 'test1'}], {test: 'test'}, 'find');
assert(result && result['test'] === 'test');
});
it('Regex', async function() {
const filter = {$regex: `\\w+\\d`, $options: 'i'};
const result = db_api.applyFilterLocalDB([{test: 'test'}, {test: 'test1'}], {test: filter}, 'find');
assert(result && result['test'] === 'test1');
});
it('Not equals', async function() {
const filter = {$ne: 'test'};
const result = db_api.applyFilterLocalDB([{test: 'test'}, {test: 'test1'}], {test: filter}, 'find');
assert(result && result['test'] === 'test1');
});
it('Nested', async function() {
const result = db_api.applyFilterLocalDB([{test1: {test2: 'test3'}}, {test4: 'test5'}], {'test1.test2': 'test3'}, 'find');
assert(result && result['test1']['test2'] === 'test3');
});
})
});
describe('Multi User', async function() {
@@ -253,10 +298,12 @@ describe('Multi User', async function() {
assert(user);
});
});
describe('Video player - normal', function() {
const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
describe('Video player - normal', async function() {
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
await db_api.insertRecordIntoTable('files', sample_video_json);
const video_to_test = sample_video_json['uid'];
it('Get video', async function() {
const video_obj = db_api.getVideo(video_to_test, 'admin');
const video_obj = await db_api.getVideo(video_to_test);
assert(video_obj);
});
@@ -341,7 +388,9 @@ describe('Downloader', function() {
});
it('Get file info', async function() {
this.timeout(300000);
const info = await downloader_api.getVideoInfoByURL(url);
assert(!!info);
});
it('Download file', async function() {
@@ -352,6 +401,19 @@ describe('Downloader', function() {
});
it('Tag file', async function() {
const audio_path = './test/sample.mp3';
const sample_json = fs.readJSONSync('./test/sample.info.json');
const tags = {
title: sample_json['title'],
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
TRCK: '27'
}
NodeID3.write(tags, audio_path);
const written_tags = NodeID3.read(audio_path);
assert(written_tags['raw']['TRCK'] === '27');
});
it('Queue file', async function() {
this.timeout(300000);
const returned_download = await downloader_api.createDownload(url, 'video', options);
@@ -360,20 +422,23 @@ describe('Downloader', function() {
});
it('Pause file', async function() {
const returned_download = await downloader_api.createDownload(url, 'video', options);
await downloader_api.pauseDownload(returned_download['uid']);
const updated_download = await db_api.getRecord('download_queue', {uid: returned_download['uid']});
assert(updated_download['paused'] && !updated_download['running']);
});
it('Generate args', async function() {
const args = await downloader_api.generateArgs(url, 'video', options);
console.log(args);
assert(args.length > 0);
});
it('Generate args - subscription', async function() {
subscriptions_api.initialize(db_api, logger);
const sub = await subscriptions_api.getSubscription(sub_id);
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
const args = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
console.log(args);
const args_normal = await downloader_api.generateArgs(url, 'video', options);
const args_sub = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
console.log(JSON.stringify(args_normal) !== JSON.stringify(args_sub));
});
it('Generate kodi NFO file', async function() {
@@ -401,6 +466,20 @@ describe('Downloader', function() {
console.log(updated_args2);
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
});
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1493770675';
it('Download VOD', async function() {
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
this.timeout(300000);
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
assert(fs.existsSync(sample_path));
// cleanup
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
});
});
});
describe('Tasks', function() {
@@ -417,7 +496,7 @@ describe('Tasks', function() {
};
tasks_api.TASKS['dummy_task'] = dummy_task;
await tasks_api.initialize();
await tasks_api.setupTasks();
});
it('Backup db', async function() {
const backups_original = await utils.recFindByExt('appdata', 'bak');
@@ -429,12 +508,13 @@ describe('Tasks', function() {
});
it('Check for missing files', async function() {
this.timeout(300000);
await db_api.removeAllRecords('files', {uid: 'test'});
const test_missing_file = {uid: 'test', path: 'test/missing_file.mp4'};
await db_api.insertRecordIntoTable('files', test_missing_file);
await tasks_api.executeTask('missing_files_check');
const task_obj = await db_api.getRecord('tasks', {key: 'missing_files_check'});
assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
const missing_file_db_record = await db_api.getRecord('files', {uid: 'test'});
assert(!missing_file_db_record, true);
});
it('Check for duplicate files', async function() {
@@ -447,10 +527,13 @@ describe('Tasks', function() {
await db_api.insertRecordIntoTable('files', test_duplicate_file1);
await db_api.insertRecordIntoTable('files', test_duplicate_file2);
await db_api.insertRecordIntoTable('files', test_duplicate_file3);
await tasks_api.executeTask('duplicate_files_check');
await tasks_api.executeRun('duplicate_files_check');
const task_obj = await db_api.getRecord('tasks', {key: 'duplicate_files_check'});
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
await tasks_api.executeTask('duplicate_files_check');
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
assert(duplicated_record_count == 1, true);
});
@@ -475,22 +558,72 @@ describe('Tasks', function() {
});
it('Schedule and cancel task', async function() {
const today_4_hours = new Date();
today_4_hours.setHours(today_4_hours.getHours() + 4);
await tasks_api.updateTaskSchedule('dummy_task', today_4_hours);
assert(!!tasks_api.TASKS['dummy_task']['job'], true);
this.timeout(5000);
const today_one_year = new Date();
today_one_year.setFullYear(today_one_year.getFullYear() + 1);
const schedule_obj = {
type: 'timestamp',
data: { timestamp: today_one_year.getTime() }
}
await tasks_api.updateTaskSchedule('dummy_task', schedule_obj);
const dummy_task = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(!!tasks_api.TASKS['dummy_task']['job']);
assert(!!dummy_task['schedule']);
await tasks_api.updateTaskSchedule('dummy_task', null);
assert(!!tasks_api.TASKS['dummy_task']['job'], false);
const dummy_task_updated = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(!tasks_api.TASKS['dummy_task']['job']);
assert(!dummy_task_updated['schedule']);
});
it('Schedule and run task', async function() {
this.timeout(5000);
const today_1_second = new Date();
today_1_second.setSeconds(today_1_second.getSeconds() + 1);
await tasks_api.updateTaskSchedule('dummy_task', today_1_second);
assert(!!tasks_api.TASKS['dummy_task']['job'], true);
const schedule_obj = {
type: 'timestamp',
data: { timestamp: today_1_second.getTime() }
}
await tasks_api.updateTaskSchedule('dummy_task', schedule_obj);
assert(!!tasks_api.TASKS['dummy_task']['job']);
await utils.wait(2000);
const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(dummy_task_obj['data'], true);
assert(dummy_task_obj['data']);
});
});
describe('Archive', async function() {
const archive_path = path.join('test', 'archives');
fs.ensureDirSync(archive_path);
const archive_file_path = path.join(archive_path, 'archive_video.txt');
const blacklist_file_path = path.join(archive_path, 'blacklist_video.txt');
beforeEach(async function() {
if (fs.existsSync(archive_file_path)) fs.unlinkSync(archive_file_path);
fs.writeFileSync(archive_file_path, 'youtube testing1\nyoutube testing2\nyoutube testing3\n');
if (fs.existsSync(blacklist_file_path)) fs.unlinkSync(blacklist_file_path);
fs.writeFileSync(blacklist_file_path, '');
});
it('Delete from archive', async function() {
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', false);
const new_archive = fs.readFileSync(archive_file_path);
assert(!new_archive.includes('testing2'));
});
it('Delete from archive - blacklist', async function() {
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', true);
const new_archive = fs.readFileSync(archive_file_path);
const new_blacklist = fs.readFileSync(blacklist_file_path);
assert(!new_archive.includes('testing2'));
assert(new_blacklist.includes('testing2'));
});
});
describe('Utils', async function() {
it('Strip properties', async function() {
const test_obj = {test1: 'test1', test2: 'test2', test3: 'test3'};
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
});
});

View File

@@ -1,90 +1,64 @@
var moment = require('moment');
var Axios = require('axios');
var fs = require('fs-extra')
var path = require('path');
const config_api = require('./config');
const logger = require('./logger');
async function getCommentsForVOD(clientID, vodId) {
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
batch,
cursor;
const moment = require('moment');
const fs = require('fs-extra')
const path = require('path');
let comments = null;
try {
do {
batch = (await Axios.get(url, {
headers: {
'Client-ID': clientID,
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
}
})).data;
const str = batch.comments.map(c => {
let {
created_at: msgCreated,
content_offset_seconds: timestamp,
commenter: {
name,
_id,
created_at: acctCreated
},
message: {
body: msg,
user_color: user_color
}
} = c;
const timestamp_str = moment.duration(timestamp, 'seconds')
.toISOString()
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
(_, ...ms) => {
const seg = v => v ? v.padStart(2, '0') : '00';
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
});
acctCreated = moment(acctCreated).utc();
msgCreated = moment(msgCreated).utc();
if (!comments) comments = [];
comments.push({
timestamp: timestamp,
timestamp_str: timestamp_str,
name: name,
message: msg,
user_color: user_color
});
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
// return line;
}).join('\n');
cursor = batch._next;
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
await new Promise(res => setTimeout(res, 300));
} while (cursor);
} catch (err) {
console.error(err);
async function getCommentsForVOD(clientID, clientSecret, vodId) {
const { promisify } = require('util');
const child_process = require('child_process');
const exec = promisify(child_process.exec);
// Reject invalid params to prevent command injection attack
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
return null;
}
return comments;
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
if (result['stderr']) {
logger.error(`Failed to download twitch comments for ${vodId}`);
logger.error(result['stderr']);
return null;
}
const temp_chat_path = path.join('appdata', `${vodId}.json`);
const raw_json = fs.readJSONSync(temp_chat_path);
const new_json = raw_json.comments.map(comment_obj => {
return {
timestamp: comment_obj.content_offset_seconds,
timestamp_str: convertTimestamp(comment_obj.content_offset_seconds),
name: comment_obj.commenter.name,
message: comment_obj.message.body,
user_color: comment_obj.message.user_color
}
});
fs.unlinkSync(temp_chat_path);
return new_json;
}
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
let file_path = null;
if (user_uid) {
if (sub) {
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
} else {
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`);
}
} else {
if (sub) {
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
} else {
file_path = path.join(type, id + '.twitch_chat.json');
const typeFolder = config_api.getConfigItem(`ytdl_${type}_folder_path`);
file_path = path.join(typeFolder, `${id}.twitch_chat.json`);
}
}
@@ -96,23 +70,28 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
return chat_file;
}
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
const chat = await getCommentsForVOD(twitch_api_key, vodId);
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
// save file if needed params are included
let file_path = null;
if (user_uid) {
if (customFileFolderPath) {
file_path = path.join(customFileFolderPath, `${id}.twitch_chat.json`)
} else if (user_uid) {
if (sub) {
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
} else {
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`);
}
} else {
if (sub) {
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`);
} else {
file_path = path.join(type, id + '.twitch_chat.json');
file_path = path.join(type, `${id}.twitch_chat.json`);
}
}
@@ -121,6 +100,14 @@ async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
return chat;
}
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
.toISOString()
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
(_, ...ms) => {
const seg = v => v ? v.padStart(2, '0') : '00';
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
});
module.exports = {
getCommentsForVOD: getCommentsForVOD,
getTwitchChatByFileID: getTwitchChatByFileID,

View File

@@ -172,11 +172,13 @@ function getExpectedFileSize(input_info_jsons) {
const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0;
formats.forEach(format_id => {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
individual_expected_filesize += available_format.filesize;
}
});
if (info_json.formats !== undefined) {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && (available_format.filesize || available_format.filesize_approx)) {
individual_expected_filesize += (available_format.filesize ? available_format.filesize : available_format.filesize_approx);
}
});
}
});
expected_filesize += individual_expected_filesize;
});
@@ -210,7 +212,7 @@ function deleteJSONFile(file_path, type) {
const ext = type === 'audio' ? '.mp3' : '.mp4';
const file_path_no_extension = removeFileExtension(file_path);
let json_path = file_path_no_extension + '.info.json';
let alternate_json_path = file_path_no_extension + ext + '.info.json';
@@ -218,8 +220,11 @@ function deleteJSONFile(file_path, type) {
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
async function removeIDFromArchive(archive_path, id) {
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
// archive helper functions
async function removeIDFromArchive(archive_path, type, id) {
const archive_file = path.join(archive_path, `archive_${type}.txt`);
const data = await fs.readFile(archive_file, {encoding: 'utf-8'});
if (!data) {
logger.error('Archive could not be found.');
return;
@@ -236,12 +241,34 @@ async function removeIDFromArchive(archive_path, id) {
}
}
if (lastIndex === -1) return null;
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
// UPDATE FILE WITH NEW DATA
const updatedData = dataArray.join('\n');
await fs.writeFile(archive_path, updatedData);
if (line) return line;
await fs.writeFile(archive_file, updatedData);
if (line) return Array.isArray(line) && line.length === 1 ? line[0] : line;
}
async function writeToBlacklist(archive_folder, type, line) {
let blacklistPath = path.join(archive_folder, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
// adds newline to the beginning of the line
line.replace('\n', '');
line.replace('\r', '');
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
}
async function deleteFileFromArchive(uid, type, archive_path, id, blacklistMode) {
const archive_file = path.join(archive_path, `archive_${type}.txt`);
if (await fs.pathExists(archive_path)) {
const line = id ? await removeIDFromArchive(archive_path, type, id) : null;
if (blacklistMode && line) await writeToBlacklist(archive_path, type, line);
} else {
logger.info(`Could not find archive file for file ${uid}. Creating...`);
await fs.close(await fs.open(archive_file, 'w'));
}
}
function durationStringToNumber(dur_str) {
@@ -306,9 +333,9 @@ function createEdgeNGrams(str) {
if (str && str.length > 3) {
const minGram = 3
const maxGram = str.length
return str.split(" ").reduce((ngrams, token) => {
if (token.length > minGram) {
if (token.length > minGram) {
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
ngrams = [...ngrams, token.substr(0, i)]
}
@@ -318,7 +345,7 @@ function createEdgeNGrams(str) {
return ngrams
}, []).join(" ")
}
return str
}
@@ -415,6 +442,14 @@ async function fetchFile(url, path, file_label) {
});
}
async function restartServer(is_update = false) {
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
// the following line restarts the server through pm2
fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only');
process.exit(1);
}
// adds or replaces args according to the following rules:
// - if it already exists and has value, then replace both arg and value
// - if already exists and doesn't have value, ignore
@@ -426,7 +461,7 @@ function injectArgs(original_args, new_args) {
for (let i = 0; i < new_args.length; i++) {
const new_arg = new_args[i];
if (!new_arg.startsWith('-') && !new_arg.startsWith('--') && i > 0 && original_args.includes(new_args[i - 1])) continue;
if (CONSTS.YTDL_ARGS_WITH_VALUES.has(new_arg)) {
if (original_args.includes(new_arg)) {
const original_index = original_args.indexOf(new_arg);
@@ -448,6 +483,60 @@ function injectArgs(original_args, new_args) {
return updated_args;
}
function filterArgs(args, args_to_remove) {
return args.filter(x => !args_to_remove.includes(x));
}
const searchObjectByString = 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 stripPropertiesFromObject(obj, properties, whitelist = false) {
if (!whitelist) {
const new_obj = JSON.parse(JSON.stringify(obj));
for (let field of properties) {
delete new_obj[field];
}
return new_obj;
}
const new_obj = {};
for (let field of properties) {
new_obj[field] = obj[field];
}
return new_obj;
}
function getArchiveFolder(type, user_uid = null, sub = null) {
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
const subsFolderPath = config_api.getConfigItem('ytdl_subscriptions_base_path');
if (user_uid) {
if (sub) {
return path.join(usersFolderPath, user_uid, 'subscriptions', 'archives', sub.name);
} else {
return path.join(usersFolderPath, user_uid, type, 'archives');
}
} else {
if (sub) {
return path.join(subsFolderPath, 'archives', sub.name);
} else {
return path.join('appdata', 'archives');
}
}
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
@@ -477,11 +566,12 @@ module.exports = {
fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
removeIDFromArchive: removeIDFromArchive,
writeToBlacklist: writeToBlacklist,
deleteFileFromArchive: deleteFileFromArchive,
getDownloadedFilesByType: getDownloadedFilesByType,
createContainerZipFile: createContainerZipFile,
durationStringToNumber: durationStringToNumber,
getMatchingCategoryFiles: getMatchingCategoryFiles,
addUIDsToCategory: addUIDsToCategory,
getCurrentDownloader: getCurrentDownloader,
recFindByExt: recFindByExt,
removeFileExtension: removeFileExtension,
@@ -491,6 +581,11 @@ module.exports = {
wait: wait,
checkExistsWithTimeout: checkExistsWithTimeout,
fetchFile: fetchFile,
restartServer: restartServer,
injectArgs: injectArgs,
filterArgs: filterArgs,
searchObjectByString: searchObjectByString,
stripPropertiesFromObject: stripPropertiesFromObject,
getArchiveFolder: getArchiveFolder,
File: File
}

View File

@@ -6,6 +6,8 @@ const utils = require('./utils');
const CONSTS = require('./consts');
const config_api = require('./config.js');
const OUTDATED_VERSION = "2020.00.00";
const is_windows = process.platform === 'win32';
const download_sources = {
@@ -31,7 +33,7 @@ exports.checkForYoutubeDLUpdate = async () => {
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
if (!current_app_details_exists) {
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2020.00.00", "downloader": default_downloader});
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version": OUTDATED_VERSION, "downloader": default_downloader});
}
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
let current_version = current_app_details['version'];
@@ -86,6 +88,18 @@ exports.updateYoutubeDL = async (latest_update_version) => {
await download_sources[default_downloader]['func'](latest_update_version);
}
exports.verifyBinaryExistsLinux = () => {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
if (!is_windows && details_json && (!details_json['path'] || details_json['path'].includes('.exe'))) {
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
details_json['exec'] = 'youtube-dl';
details_json['version'] = OUTDATED_VERSION;
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
utils.restartServer();
}
}
async function downloadLatestYoutubeDLBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';

View File

@@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "4.2"
appVersion: "4.3"

View File

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

@@ -2,7 +2,6 @@ version: "2"
services:
ytdl_material:
environment:
ALLOW_CONFIG_MUTATIONS: 'true'
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
ytdl_use_local_db: 'false'
write_ytdl_config: 'true'
@@ -17,14 +16,12 @@ services:
- ./users:/app/users
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:nightly
image: tzahi12345/youtubedl-material:latest
ytdl-mongo-db:
image: mongo
ports:
- "27017:27017"
logging:
driver: "none"
container_name: mongo-db
restart: always
volumes:
- ./db/:/data/db
- ./db/:/data/db

View File

@@ -40,4 +40,4 @@ echo "(4/5) PROVISION - Provide ffmpeg and ffprobe from ffmpeg obtain layer"
cp /tmp/ffmpeg/*/ffmpeg /usr/local/bin/ffmpeg
cp /tmp/ffmpeg/*/ffprobe /usr/local/bin/ffprobe
echo "(5/5) CLEANUP - Remove temporary downloads from ffmpeg obtain layer"
rm -rf /tmp/ffmpeg ffmpeg.txz
rm -rf /tmp/ffmpeg ffmpeg.txz

3
heroku.yml Normal file
View File

@@ -0,0 +1,3 @@
build:
docker:
web: Dockerfile.heroku

135
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.2.0",
"version": "4.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3295,65 +3295,12 @@
"safer-buffer": "~2.1.0"
}
},
"asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dev": true,
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"dev": true
}
}
},
"assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
"integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
"dev": true,
"requires": {
"object-assign": "^4.1.1",
"util": "0.10.3"
},
"dependencies": {
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
"dev": true
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
"inherits": "2.0.1"
}
}
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
},
"assign-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
"dev": true
},
"ast-types-flow": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
@@ -3367,9 +3314,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
@@ -3728,7 +3675,7 @@
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true
},
"buffer-from": {
@@ -3838,12 +3785,6 @@
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true
},
"normalize-url": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true
}
}
},
@@ -3998,7 +3939,7 @@
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
"integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==",
"dev": true,
"requires": {
"mimic-response": "^1.0.0"
@@ -4719,7 +4660,7 @@
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==",
"dev": true,
"requires": {
"mimic-response": "^1.0.0"
@@ -5009,7 +4950,7 @@
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==",
"dev": true
},
"ecc-jsbn": {
@@ -5029,20 +4970,20 @@
"dev": true
},
"electron": {
"version": "13.6.6",
"resolved": "https://registry.npmjs.org/electron/-/electron-13.6.6.tgz",
"integrity": "sha512-TP2Bl1nTxaH1yRmlYiF7imzvKE/NASE0cl6wOYA3AaP/UrBGc4L3NwJfn5Z55o+1t4TH8vCRxENufESyb32HhA==",
"version": "19.0.6",
"resolved": "https://registry.npmjs.org/electron/-/electron-19.0.6.tgz",
"integrity": "sha512-S9Yud32nKhB0iWC0lGl2JXz4FQnCiLCnP5Vehm1/CqyeICcQGmgQaZl2HYpCJ2pesKIsYL9nsgmku/10cxm/gg==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
"@types/node": "^14.6.2",
"@electron/get": "^1.14.1",
"@types/node": "^16.11.26",
"extract-zip": "^1.0.3"
},
"dependencies": {
"@types/node": {
"version": "14.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
"integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==",
"version": "16.11.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz",
"integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==",
"dev": true
}
}
@@ -6009,7 +5950,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
}
}
@@ -6077,7 +6018,7 @@
"fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"requires": {
"pend": "~1.2.0"
@@ -6465,9 +6406,9 @@
},
"dependencies": {
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true,
"optional": true,
"requires": {
@@ -6495,9 +6436,9 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"globalthis": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz",
"integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
"integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
"dev": true,
"optional": true,
"requires": {
@@ -7508,7 +7449,7 @@
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
"integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true
},
"json-parse-better-errors": {
@@ -7564,7 +7505,7 @@
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
@@ -8614,6 +8555,12 @@
"integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
"dev": true
},
"normalize-url": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true
},
"npm-bundled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
@@ -8637,7 +8584,7 @@
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
"dev": true,
"optional": true
}
@@ -9423,7 +9370,7 @@
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true
},
"performance-now": {
@@ -9870,7 +9817,7 @@
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==",
"dev": true
},
"pretty-bytes": {
@@ -9918,7 +9865,7 @@
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true,
"optional": true
},
@@ -10629,7 +10576,7 @@
"responselike": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
"integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
"integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==",
"dev": true,
"requires": {
"lowercase-keys": "^1.0.0"
@@ -10879,7 +10826,7 @@
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"optional": true
},
@@ -11990,7 +11937,7 @@
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"dev": true
},
"typescript": {
@@ -12080,7 +12027,7 @@
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
"integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==",
"dev": true,
"requires": {
"prepend-http": "^2.0.0"
@@ -12629,7 +12576,7 @@
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"requires": {
"buffer-crc32": "~0.2.3",

View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.2.0",
"version": "4.3.0",
"license": "MIT",
"scripts": {
"ng": "ng",
@@ -13,7 +13,7 @@
"e2e": "ng e2e",
"electron": "ng build --base-href ./ && electron .",
"generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true",
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n"
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
},
"engines": {
"node": "12.3.1",
@@ -66,7 +66,7 @@
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"codelyzer": "^6.0.0",
"electron": "^13.6.6",
"electron": "^19.0.6",
"eslint": "^7.32.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",

View File

@@ -4,6 +4,7 @@
export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest';
export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest';
export type { binary } from './models/binary';
export type { body_19 } from './models/body_19';
export type { body_20 } from './models/body_20';
export type { Category } from './models/Category';
@@ -12,6 +13,7 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
export type { ConcurrentStream } from './models/ConcurrentStream';
export type { Config } from './models/Config';
export type { ConfigResponse } from './models/ConfigResponse';
@@ -23,6 +25,7 @@ export type { CropFileSettings } from './models/CropFileSettings';
export type { DatabaseFile } from './models/DatabaseFile';
export { DBBackup } from './models/DBBackup';
export type { DBInfoResponse } from './models/DBInfoResponse';
export type { DeleteAllFilesResponse } from './models/DeleteAllFilesResponse';
export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest';
export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request';
export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest';
@@ -36,13 +39,14 @@ export type { DownloadResponse } from './models/DownloadResponse';
export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchChatByVODIDRequest';
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
export type { File } from './models/File';
export { FileType } from './models/FileType';
export { FileTypeFilter } from './models/FileTypeFilter';
export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
export type { GetAllFilesRequest } from './models/GetAllFilesRequest';
export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
@@ -80,6 +84,7 @@ export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
export { Schedule } from './models/Schedule';
export type { SetConfigRequest } from './models/SetConfigRequest';
export type { SharingToggle } from './models/SharingToggle';
export type { Sort } from './models/Sort';
export type { SubscribeRequest } from './models/SubscribeRequest';
export type { SubscribeResponse } from './models/SubscribeResponse';
export type { Subscription } from './models/Subscription';
@@ -98,6 +103,7 @@ export type { UpdateCategoriesRequest } from './models/UpdateCategoriesRequest';
export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest';
export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest';
export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse';
export type { UpdateFileRequest } from './models/UpdateFileRequest';
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
export type { UpdaterStatus } from './models/UpdaterStatus';
export type { UpdateServerRequest } from './models/UpdateServerRequest';

View File

@@ -2,8 +2,7 @@
/* tslint:disable */
/* eslint-disable */
export interface AddFileToPlaylistRequest {
export type AddFileToPlaylistRequest = {
file_uid: string;
playlist_id: string;
}
};

View File

@@ -2,10 +2,10 @@
/* tslint:disable */
/* eslint-disable */
import { UserPermission } from './UserPermission';
import { YesNo } from './YesNo';
import type { UserPermission } from './UserPermission';
import type { YesNo } from './YesNo';
export interface BaseChangePermissionsRequest {
export type BaseChangePermissionsRequest = {
permission: UserPermission;
new_value: YesNo;
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { CategoryRule } from './CategoryRule';
import type { CategoryRule } from './CategoryRule';
export interface Category {
export type Category = {
name?: string;
uid?: string;
rules?: Array<CategoryRule>;
@@ -12,4 +12,4 @@ export interface Category {
* Overrides file output for downloaded files in category
*/
custom_output?: string;
}
};

View File

@@ -2,11 +2,10 @@
/* tslint:disable */
/* eslint-disable */
export interface CategoryRule {
export type CategoryRule = {
preceding_operator?: CategoryRule.preceding_operator;
comparator?: CategoryRule.comparator;
}
};
export namespace CategoryRule {

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
import type { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
export interface ChangeRolePermissionsRequest extends BaseChangePermissionsRequest {
role: string;
}
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
role: string;
});

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
import type { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
export interface ChangeUserPermissionsRequest extends BaseChangePermissionsRequest {
user_uid: string;
}
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
user_uid: string;
});

View File

@@ -2,10 +2,9 @@
/* tslint:disable */
/* eslint-disable */
export interface CheckConcurrentStreamRequest {
export type CheckConcurrentStreamRequest = {
/**
* UID of the concurrent stream
*/
uid: string;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { ConcurrentStream } from './ConcurrentStream';
import type { ConcurrentStream } from './ConcurrentStream';
export interface CheckConcurrentStreamResponse {
export type CheckConcurrentStreamResponse = {
stream: ConcurrentStream;
}
};

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClearDownloadsRequest = {
clear_finished?: boolean;
clear_paused?: boolean;
clear_errors?: boolean;
};

View File

@@ -2,9 +2,8 @@
/* tslint:disable */
/* eslint-disable */
export interface ConcurrentStream {
export type ConcurrentStream = {
playback_timestamp?: number;
unix_timestamp?: number;
playing?: boolean;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface Config {
export type Config = {
YoutubeDLMaterial: any;
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { Config } from './Config';
import type { Config } from './Config';
export interface ConfigResponse {
export type ConfigResponse = {
config_file: Config;
success: boolean;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface CreateCategoryRequest {
export type CreateCategoryRequest = {
name: string;
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { Category } from './Category';
import type { Category } from './Category';
export interface CreateCategoryResponse {
export type CreateCategoryResponse = {
new_category?: Category;
success?: boolean;
}
};

View File

@@ -2,11 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface CreatePlaylistRequest {
export type CreatePlaylistRequest = {
playlistName: string;
uids: Array<string>;
type: FileType;
thumbnailURL: string;
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { Playlist } from './Playlist';
import type { Playlist } from './Playlist';
export interface CreatePlaylistResponse {
export type CreatePlaylistResponse = {
new_playlist: Playlist;
success: boolean;
}
};

View File

@@ -2,8 +2,7 @@
/* tslint:disable */
/* eslint-disable */
export interface CropFileSettings {
export type CropFileSettings = {
cropFileStart: number;
cropFileEnd: number;
}
};

View File

@@ -2,13 +2,12 @@
/* tslint:disable */
/* eslint-disable */
export interface DBBackup {
export type DBBackup = {
name: string;
timestamp: number;
size: number;
source: DBBackup.source;
}
};
export namespace DBBackup {

View File

@@ -2,17 +2,17 @@
/* tslint:disable */
/* eslint-disable */
import { TableInfo } from './TableInfo';
import type { TableInfo } from './TableInfo';
export interface DBInfoResponse {
export type DBInfoResponse = {
using_local_db?: boolean;
stats_by_table?: {
files?: TableInfo,
playlists?: TableInfo,
categories?: TableInfo,
subscriptions?: TableInfo,
users?: TableInfo,
roles?: TableInfo,
download_queue?: TableInfo,
files?: TableInfo;
playlists?: TableInfo;
categories?: TableInfo;
subscriptions?: TableInfo;
users?: TableInfo;
roles?: TableInfo;
download_queue?: TableInfo;
};
}
};

View File

@@ -2,11 +2,16 @@
/* tslint:disable */
/* eslint-disable */
import type { Category } from './Category';
export interface DatabaseFile {
export type DatabaseFile = {
id: string;
title: string;
/**
* Backup if thumbnailPath is not defined
*/
thumbnailURL: string;
thumbnailPath?: string;
isAudio: boolean;
/**
* In seconds
@@ -14,9 +19,25 @@ export interface DatabaseFile {
duration: number;
url: string;
uploader: string;
/**
* In bytes
*/
size: number;
path: string;
upload_date: string;
uid: string;
sharingEnabled?: boolean;
}
category?: Category;
view_count?: number;
local_view_count?: number;
sub_id?: string;
registered?: number;
/**
* In pixels, only for videos
*/
height?: number;
/**
* In Kbps
*/
abr?: number;
};

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteAllFilesResponse = {
/**
* Number of files found matching search parameters
*/
file_count?: number;
/**
* Number of files removed
*/
delete_count?: number;
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface DeleteCategoryRequest {
export type DeleteCategoryRequest = {
category_uid: string;
}
};

View File

@@ -2,8 +2,7 @@
/* tslint:disable */
/* eslint-disable */
export interface DeleteMp3Mp4Request {
export type DeleteMp3Mp4Request = {
uid: string;
blacklistMode?: boolean;
}
};

View File

@@ -2,9 +2,6 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface DeletePlaylistRequest {
export type DeletePlaylistRequest = {
playlist_id: string;
type: FileType;
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { SubscriptionRequestData } from './SubscriptionRequestData';
import type { SubscriptionRequestData } from './SubscriptionRequestData';
export interface DeleteSubscriptionFileRequest {
export type DeleteSubscriptionFileRequest = {
file: string;
file_uid?: string;
sub: SubscriptionRequestData;
@@ -12,4 +12,4 @@ export interface DeleteSubscriptionFileRequest {
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
*/
deleteForever?: boolean;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface DeleteUserRequest {
export type DeleteUserRequest = {
uid: string;
}
};

View File

@@ -1,7 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Dictionary<T> = {
[key: string]: T;
}

View File

@@ -2,8 +2,7 @@
/* tslint:disable */
/* eslint-disable */
export interface Download {
export type Download = {
uid: string;
ui_uid?: string;
running: boolean;
@@ -23,4 +22,5 @@ export interface Download {
user_uid?: string;
sub_id?: string;
sub_name?: string;
}
prefetched_info?: any;
};

View File

@@ -2,9 +2,8 @@
/* tslint:disable */
/* eslint-disable */
export interface DownloadArchiveRequest {
export type DownloadArchiveRequest = {
sub: {
archive_dir: string,
archive_dir: string;
};
}
};

View File

@@ -2,13 +2,13 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import type { FileType } from './FileType';
export interface DownloadFileRequest {
export type DownloadFileRequest = {
uid?: string;
uuid?: string;
sub_id?: string;
playlist_id?: string;
url?: string;
type?: FileType;
}
};

View File

@@ -2,10 +2,10 @@
/* tslint:disable */
/* eslint-disable */
import { CropFileSettings } from './CropFileSettings';
import { FileType } from './FileType';
import type { CropFileSettings } from './CropFileSettings';
import type { FileType } from './FileType';
export interface DownloadRequest {
export type DownloadRequest = {
url: string;
/**
* Video format code. Overrides other quality options.
@@ -41,4 +41,4 @@ export interface DownloadRequest {
maxBitrate?: string;
type?: FileType;
cropFileSettings?: CropFileSettings;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { Download } from './Download';
import type { Download } from './Download';
export interface DownloadResponse {
export type DownloadResponse = {
download?: Download;
}
};

View File

@@ -2,10 +2,10 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import { Subscription } from './Subscription';
import type { FileType } from './FileType';
import type { Subscription } from './Subscription';
export interface DownloadTwitchChatByVODIDRequest {
export type DownloadTwitchChatByVODIDRequest = {
/**
* File ID
*/
@@ -20,4 +20,4 @@ export interface DownloadTwitchChatByVODIDRequest {
*/
uuid?: string;
sub?: Subscription;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { TwitchChatMessage } from './TwitchChatMessage';
import type { TwitchChatMessage } from './TwitchChatMessage';
export interface DownloadTwitchChatByVODIDResponse {
export type DownloadTwitchChatByVODIDResponse = {
chat: Array<TwitchChatMessage>;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface DownloadVideosForSubscriptionRequest {
export type DownloadVideosForSubscriptionRequest = {
subID: string;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export enum FileType {
AUDIO = 'audio',
VIDEO = 'video',

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export enum FileTypeFilter {
AUDIO_ONLY = 'audio_only',
VIDEO_ONLY = 'video_only',
BOTH = 'both',
}

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface GenerateArgsResponse {
export type GenerateArgsResponse = {
args?: Array<string>;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface GenerateNewApiKeyResponse {
export type GenerateNewApiKeyResponse = {
new_api_key: string;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { Category } from './Category';
import type { Category } from './Category';
export interface GetAllCategoriesResponse {
export type GetAllCategoriesResponse = {
categories: Array<Category>;
}
};

View File

@@ -2,10 +2,9 @@
/* tslint:disable */
/* eslint-disable */
export interface GetAllDownloadsRequest {
export type GetAllDownloadsRequest = {
/**
* Filters downloads with the array
*/
uids?: Array<string> | null;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { Download } from './Download';
import type { Download } from './Download';
export interface GetAllDownloadsResponse {
export type GetAllDownloadsResponse = {
downloads?: Array<Download>;
}
};

View File

@@ -0,0 +1,20 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { FileTypeFilter } from './FileTypeFilter';
import type { Sort } from './Sort';
export type GetAllFilesRequest = {
sort?: Sort;
range?: Array<number>;
/**
* Filter files by title
*/
text_search?: string;
file_type_filter?: FileTypeFilter;
/**
* Include if you want to filter by subscription
*/
sub_id?: string;
};

View File

@@ -2,13 +2,13 @@
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import { Playlist } from './Playlist';
import type { DatabaseFile } from './DatabaseFile';
import type { Playlist } from './Playlist';
export interface GetAllFilesResponse {
export type GetAllFilesResponse = {
files: Array<DatabaseFile>;
/**
* All video playlists
*/
playlists: Array<Playlist>;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { Subscription } from './Subscription';
import type { Subscription } from './Subscription';
export interface GetAllSubscriptionsResponse {
export type GetAllSubscriptionsResponse = {
subscriptions: Array<Subscription>;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { Task } from './Task';
import type { Task } from './Task';
export interface GetAllTasksResponse {
export type GetAllTasksResponse = {
tasks?: Array<Task>;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { DBBackup } from './DBBackup';
import type { DBBackup } from './DBBackup';
export interface GetDBBackupsResponse {
export type GetDBBackupsResponse = {
tasks?: Array<DBBackup>;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface GetDownloadRequest {
export type GetDownloadRequest = {
download_uid: string;
}
};

View File

@@ -2,8 +2,8 @@
/* tslint:disable */
/* eslint-disable */
import { Download } from './Download';
import type { Download } from './Download';
export interface GetDownloadResponse {
export type GetDownloadResponse = {
download?: Download;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface GetFileFormatsRequest {
export type GetFileFormatsRequest = {
url?: string;
}
};

View File

@@ -2,11 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { File } from './File';
export interface GetFileFormatsResponse {
export type GetFileFormatsResponse = {
success: boolean;
result: {
formats?: Array<any>,
formats?: Array<any>;
};
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import type { FileType } from './FileType';
export interface GetFileRequest {
export type GetFileRequest = {
/**
* Video UID
*/
@@ -14,4 +14,4 @@ export interface GetFileRequest {
* User UID
*/
uuid?: string;
}
};

View File

@@ -2,9 +2,9 @@
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import type { DatabaseFile } from './DatabaseFile';
export interface GetFileResponse {
export type GetFileResponse = {
success: boolean;
file?: DatabaseFile;
}
};

View File

@@ -2,10 +2,10 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import { Subscription } from './Subscription';
import type { FileType } from './FileType';
import type { Subscription } from './Subscription';
export interface GetFullTwitchChatRequest {
export type GetFullTwitchChatRequest = {
/**
* File ID
*/
@@ -16,4 +16,4 @@ export interface GetFullTwitchChatRequest {
*/
uuid?: string;
sub?: Subscription;
}
};

View File

@@ -2,8 +2,7 @@
/* tslint:disable */
/* eslint-disable */
export interface GetFullTwitchChatResponse {
export type GetFullTwitchChatResponse = {
success: boolean;
error?: string;
}
};

View File

@@ -2,7 +2,6 @@
/* tslint:disable */
/* eslint-disable */
export interface GetLogsRequest {
export type GetLogsRequest = {
lines?: number;
}
};

View File

@@ -2,11 +2,10 @@
/* tslint:disable */
/* eslint-disable */
export interface GetLogsResponse {
export type GetLogsResponse = {
/**
* Number of lines to retrieve from the bottom
*/
logs?: string;
success?: boolean;
}
};

View File

@@ -2,13 +2,13 @@
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import { Playlist } from './Playlist';
import type { DatabaseFile } from './DatabaseFile';
import type { Playlist } from './Playlist';
export interface GetMp3sResponse {
export type GetMp3sResponse = {
mp3s: Array<DatabaseFile>;
/**
* All audio playlists
*/
playlists: Array<Playlist>;
}
};

View File

@@ -2,13 +2,13 @@
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import { Playlist } from './Playlist';
import type { DatabaseFile } from './DatabaseFile';
import type { Playlist } from './Playlist';
export interface GetMp4sResponse {
export type GetMp4sResponse = {
mp4s: Array<DatabaseFile>;
/**
* All video playlists
*/
playlists: Array<Playlist>;
}
};

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