Compare commits

...

344 Commits

Author SHA1 Message Date
Isaac Abadi
664b635439 Updated English source translation file 2022-05-05 00:56:46 -04:00
Isaac Abadi
692d6eeaac Added edit button for playlist subscriptions 2022-05-04 22:02:43 -04:00
Isaac Abadi
9515d5a1b0 Fixed issue where additional args wouldn't properly inject 2022-05-04 22:01:51 -04:00
Isaac Abadi
24df238ff9 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into db-bug-fixes 2022-05-04 19:08:06 -04:00
Glassed Silver
f5e6815200 Merge pull request #597 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-05-05 00:03:36 +02:00
Glassed Silver
0e5efd003e Merge pull request #598 from GlassedSilver/master
Fixing Ubuntu-introduced issues and further improvements
2022-05-04 23:53:59 +02:00
GlassedSilver
3e7ef766de Best to just put fix-scripts in /backend 👍 2022-05-04 23:40:42 +02:00
GlassedSilver
17fdd0d788 update usage instr. for fix-script in comment 2022-05-04 20:57:13 +02:00
GlassedSilver
ce3e645129 for now: user has to DIY chmod +x fix-scripts 2022-05-04 20:54:21 +02:00
GlassedSilver
acca2d0de2 syntax devil struck again 2022-05-04 20:19:52 +02:00
GlassedSilver
31b4fcb9fc We're now using gosu to switch our user down :) 2022-05-04 19:58:00 +02:00
GlassedSilver
336d7a09bd set fix-scripts folder permissions more reliably 2022-05-04 18:31:28 +02:00
GlassedSilver
7c31a10498 ux/guidance improvements 2022-05-04 17:23:04 +02:00
GlassedSilver
a94b8f376e permission needs to be set with octal notation 2022-05-04 17:22:21 +02:00
GlassedSilver
84d33b0f82 fix missing execution permission for fix scripts 2022-05-04 17:21:06 +02:00
GlassedSilver
3abf8ecfc3 Merge branch 'master' of https://github.com/GlassedSilver/YoutubeDL-Material into master 2022-05-04 16:55:24 +02:00
GlassedSilver
5b919b2b18 Fix scripts folder: copy content AND parent folder 2022-05-04 16:55:22 +02:00
Glassed Silver
e290dc0a25 Fixing permissions of ffmpeg and ffprobe
Since we didn't specify UID and GID in copy command before, they were run as root causing permissions conflicts
The ffmpeg stage doesn't need the env variables henceforth
2022-05-04 15:11:35 +02:00
GlassedSilver
a54f07e93a remove white spaces from script & add usage instr. 2022-05-04 12:19:05 +02:00
GlassedSilver
8336d886e9 fix-scripts need to be owned and run by root 2022-05-04 12:16:58 +02:00
GlassedSilver
6a56b5b065 add fix-scripts to docker image 2022-05-04 11:59:45 +02:00
GlassedSilver
7aca8ab060 entrypoint updated for su 2022-05-04 10:18:06 +02:00
GlassedSilver
8cc5c4f733 no need for suexec anymore apparently 2022-05-04 10:16:49 +02:00
Heimen Stoffels
c5eacbb70c Translated using Weblate (Dutch)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2022-05-03 12:11:25 +02:00
Eric
7268242691 Translated using Weblate (Chinese (Simplified))
Currently translated at 83.7% (253 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-05-03 12:11:25 +02:00
GlassedSilver
d0f5518d31 suexec needs to be installed 2022-05-03 09:44:43 +02:00
GlassedSilver
713a97f75a reintegrate suexec 2022-05-03 08:44:55 +02:00
GlassedSilver
0bdcfa3244 Merge branch 'master' of https://github.com/GlassedSilver/YoutubeDL-Material into master 2022-05-03 08:27:49 +02:00
GlassedSilver
849c1927d3 Add fix script for interactive permission fixing. 2022-05-03 08:27:42 +02:00
GlassedSilver
06ca9cbe76 build excludes: now matches ANY *.md :) 2022-05-03 08:26:37 +02:00
GlassedSilver
8e4a3119ab 🚀 bye unnecessary build cleanups (not last stage) 2022-05-03 08:25:38 +02:00
Isaac Abadi
ec1ccf6888 Fixed bug that incorrectly told the UI that DB transfer failed 2022-05-03 00:35:02 -04:00
Isaac Abadi
c33e8010b3 Additional args now replace existing ones intelligently 2022-05-03 00:34:36 -04:00
Glassed Silver
57fd991d5c Merge pull request #595 from GlassedSilver/master
Permissions fixes
2022-05-02 17:27:32 +02:00
GlassedSilver
44c1a34c67 Permissions fix for ffmpeg executable 2022-05-02 13:33:20 +02:00
GlassedSilver
9f740020af possible fix 2022-05-02 13:14:57 +02:00
GlassedSilver
4d4bc76549 Use Ubuntu 22.04, use nodejs from ubuntu repo 2022-05-02 12:59:34 +02:00
GlassedSilver
93ce498e94 switch to ubuntu 21.10 as we wait for nodesource 2022-05-02 08:20:48 +02:00
Glassed Silver
e5b256b8df Merge pull request #592 from Tzahi12345/4.3-bug-fixes
Various bug fixes
2022-05-02 08:01:23 +02:00
Tzahi12345
05ea5a816f Merge pull request #591 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2022-05-02 01:47:10 -04:00
Isaac Abadi
b3342d89c1 Fixed #563 where empty languages existed in language select 2022-05-02 01:46:25 -04:00
Isaac Abadi
7bfb441a00 Fixed bug where files couldn't be deleted with archive mode enabled fixes #487 2022-05-02 01:44:43 -04:00
Tzahi12345
01fd2fb990 Deleted translation using Weblate (Hindi) 2022-05-02 07:24:10 +02:00
Tzahi12345
1bb4d9dbf6 Deleted translation using Weblate (Basa (Cameroon)) 2022-05-02 07:22:08 +02:00
Glassed Silver
5e23932146 Merge pull request #589 from Tzahi12345/better-docker-pr-tests
Better docker PR tests
2022-05-02 05:42:49 +02:00
Glassed Silver
d6d3495c5b Merge pull request #590 from GlassedSilver/master
Adding ignore parameters to docker build-and-push
2022-05-02 05:27:16 +02:00
Isaac Abadi
a36761e96a Fixed frontend build error 2022-05-01 23:24:15 -04:00
Isaac Abadi
88c16d7195 All setIntervals on the frontend are now properly destroyed 2022-05-01 23:23:19 -04:00
Isaac Abadi
8a323f028d Fixed bug where subscription avatars were missing 2022-05-01 23:15:09 -04:00
Isaac Abadi
a68726e7cb Removed deprecated armhf.Dockerfile 2022-05-01 23:13:11 -04:00
Isaac Abadi
dab0b7a8b6 Updated Angular version in readme 2022-05-01 23:13:00 -04:00
Isaac Abadi
9f9054ed9d Removed secrets from docker-pr.yml 2022-05-01 22:59:55 -04:00
GlassedSilver
77e8cbc6b5 Adding ignore parameters to docker build-and-push 2022-05-02 04:21:12 +02:00
Isaac Abadi
4c06c430eb Converted docker-compose build to docker build for docker-pr GH action 2022-05-01 21:21:39 -04:00
Isaac Abadi
2981f843c3 Added docker build PR check 2022-05-01 21:11:21 -04:00
Isaac Abadi
3a48ff2d50 docker build and push action now uses secrets for DockerHub username, repo, and tag 2022-05-01 21:11:01 -04:00
Glassed Silver
ac2c3dc8a1 Merge pull request #588 from GlassedSilver/master
removing strict SSL from npm config
2022-05-02 03:00:39 +02:00
GlassedSilver
0abe252d1e we need to find a different build check solution 2022-05-02 02:59:25 +02:00
GlassedSilver
f5f00e1732 fix name 2022-05-02 02:12:10 +02:00
GlassedSilver
c309e41a91 Merge branch 'master' of https://github.com/GlassedSilver/YoutubeDL-Material into master 2022-05-02 02:09:44 +02:00
GlassedSilver
754d837059 adding docker-pr-check.yml 2022-05-02 02:09:37 +02:00
Glassed Silver
d5626f1dae Dockerfile: wget not needed 2022-05-01 23:29:51 +02:00
GlassedSilver
9c0733453a removing strict SSL from npm config 2022-05-01 23:00:01 +02:00
Glassed Silver
2a41028253 Update Dockerfile 2022-05-01 20:42:45 +02:00
Glassed Silver
67b2e480f8 Merge pull request #586 from dejan995/master
Clean up docker image
2022-05-01 19:38:39 +02:00
dejan.petrov@dapmn.com
2cdc1cee98 Fix for #585
Added the DEBIAN_FRONTEND=noninteractive variable to all stages. This should stop the build from failing.
Also added --no-install-recommends to install only the requested packages.
This might break stuff, but I'm not sure though.
2022-05-01 18:14:27 +02:00
dejan.petrov@dapmn.com
bd1ed2b705 Clean up docker image
Added some commands to clean up the image after apt-get does its thing.
It should shave off a couple of megabytes, nothing to big though.
2022-05-01 18:02:46 +02:00
Glassed Silver
33ca0f0817 Merge pull request #584 from GlassedSilver/master
wow that was a bunch of work, but...
2022-05-01 12:30:49 +02:00
GlassedSilver
d5ab0d7b96 I'm getting sleepy, why am I still pushing through 2022-05-01 11:54:19 +02:00
GlassedSilver
777aebe508 apparently we still need npm in the last stretch.. 2022-05-01 11:52:35 +02:00
GlassedSilver
efaecaa059 use yarn in apt installs instead of npm 2022-05-01 11:48:12 +02:00
GlassedSilver
39ddefab5c fix dependencies needed for our apt packages 2022-05-01 11:37:39 +02:00
GlassedSilver
60f2ab449f yea 2022-05-01 11:31:53 +02:00
GlassedSilver
958f80e200 the good? I learn a lot about Docker building 2022-05-01 11:28:34 +02:00
GlassedSilver
7aa5c1bf7f syyyyntax 2022-05-01 11:21:45 +02:00
GlassedSilver
3bcbe0d3e7 fix dependency node-gyp (>= 3.6.2~) needed 2022-05-01 11:04:59 +02:00
GlassedSilver
80fcecdaea it's a learning experience 2022-05-01 10:57:21 +02:00
GlassedSilver
0329cd9718 don't think we need to install curl twice lol 2022-05-01 10:51:20 +02:00
GlassedSilver
493e876a97 syntax fixes are fun 2022-05-01 10:48:27 +02:00
Glassed Silver
574edd74ab Merge pull request #583 from GlassedSilver/master
I did warn you I will test docker builds this way
2022-05-01 10:41:10 +02:00
GlassedSilver
fe91484f24 I did warn you I will test docker builds this way 2022-05-01 10:40:19 +02:00
Glassed Silver
dda6e40a42 Merge pull request #582 from GlassedSilver/master
fix docker-build.sh for ubuntu, what a ride
2022-05-01 10:16:03 +02:00
GlassedSilver
c0fb838931 fix docker-build.sh for ubuntu, what a ride 2022-05-01 10:11:32 +02:00
Glassed Silver
28924cc7a0 Merge pull request #581 from GlassedSilver/master
fix pipefail MIA in ubuntu without specifying bash
2022-05-01 09:36:27 +02:00
GlassedSilver
2527051eab fix pipefail MIA in ubuntu without specifying bash 2022-05-01 09:35:04 +02:00
Glassed Silver
fcf7d14f46 Merge pull request #580 from GlassedSilver/master
Fix for #480 - existing DLs still getting queued
2022-05-01 09:21:48 +02:00
GlassedSilver
0a8aba54d2 Fix for #480 - existing DLs still getting queued 2022-05-01 09:17:23 +02:00
Glassed Silver
2c6485acb2 Merge pull request #577 from GlassedSilver/master
Dockerfile uses Ubuntu 20.04, fix obtain ffmpeg
2022-05-01 09:14:13 +02:00
GlassedSilver
aea4f52267 revert postbuild.mjs file-extension change 2022-05-01 07:12:00 +02:00
GlassedSilver
5ac5fca482 adapt postbuild.mjs to postbuild.js 2022-05-01 06:37:12 +02:00
GlassedSilver
7874f1b71a curl is in fact missing in focal, my bad 2022-05-01 06:29:54 +02:00
GlassedSilver
960c545f37 Dockerfile uses Ubuntu 20.04, fix obtain ffmpeg 2022-05-01 05:14:31 +02:00
Isaac Abadi
5e3eb68b03 Fixed issue where setting sub downloads as 'fresh' was not working properly (#567) 2022-04-30 00:58:12 -04:00
Glassed Silver
4dd3b97515 Merge pull request #566 from GlassedSilver/master
Fixing DNS issues caused by outdated musl version
2022-04-26 06:39:26 +02:00
Glassed Silver
701066eec1 Merge pull request #562 from Tzahi12345/GlassedSilver-add-security-policy
Added Security Policy
2022-04-26 06:39:16 +02:00
GlassedSilver
7f61ccb5f5 Use fixed version of musl to fix DNS errors 2022-04-26 04:46:05 +02:00
Glassed Silver
4f227ca442 Delete extensions.json 2022-04-26 04:28:47 +02:00
Glassed Silver
666bd2057d Merge branch 'Tzahi12345:master' into master 2022-04-26 04:25:50 +02:00
Isaac Abadi
37c858f950 Revert "Updated ffmpeg link in docker-build.sh to use release builds"
This reverts commit 768ec59f30.
2022-04-24 06:16:43 -04:00
Isaac Abadi
ebb7f6a2b0 Revert "Fixed mangled ffmpeg link"
This reverts commit 48e46db071.
2022-04-24 06:16:02 -04:00
Isaac Abadi
48e46db071 Fixed mangled ffmpeg link 2022-04-24 05:51:04 -04:00
Isaac Abadi
768ec59f30 Updated ffmpeg link in docker-build.sh to use release builds 2022-04-24 05:49:09 -04:00
Glassed Silver
aa8f602856 Added Security Policy 2022-04-24 11:12:22 +02:00
Isaac Abadi
d5c1361e64 Fixed issue where roles were not properly initialized 2022-04-24 05:02:02 -04:00
Isaac Abadi
901a96aada Fixed issue where use_local_db may be out of sync when first starting youtubedl-material 2022-04-24 05:01:45 -04:00
Isaac Abadi
21507ee36d Updated methodology of calculating download progress to rely on fs.readdir instead of glob 2022-04-24 04:15:38 -04:00
Isaac Abadi
0585943d67 Fixed bug where task time was not properly set with values of 0
Fixed issue where time field was not properly populated in the schedule dialog
2022-04-24 04:10:27 -04:00
Isaac Abadi
0bc2193f25 Updated downloadFile API request 2022-04-23 21:41:39 -04:00
Isaac Abadi
f3398fce1a Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2022-04-23 21:20:51 -04:00
Glassed Silver
60e8973f52 Merge branch 'Tzahi12345:master' into master 2022-04-24 00:40:58 +02:00
Isaac Abadi
d94857b0a5 Rolled back passport update 2022-04-23 18:14:44 -04:00
Glassed Silver
5fda56d7af Merge pull request #560 from Tzahi12345/revert-556-dependabot/npm_and_yarn/async-2.6.4
Revert "Bump async from 2.6.3 to 2.6.4"
2022-04-23 23:53:29 +02:00
Glassed Silver
abb80b33f3 Revert "Bump async from 2.6.3 to 2.6.4" 2022-04-23 23:53:15 +02:00
Glassed Silver
9977340161 Merge pull request #558 from Tzahi12345/angular-13-update
Angular/dependencies updates
2022-04-23 08:44:27 +02:00
Glassed Silver
8ded160775 Merge branch 'master' into angular-13-update 2022-04-23 08:43:01 +02:00
Glassed Silver
2ee64c7a65 Merge pull request #515 from depuits/master
Added deleteAllFiles api endpoint
2022-04-23 08:27:54 +02:00
Glassed Silver
2ec7efa1ac Merge pull request #555 from Tzahi12345/dependabot/npm_and_yarn/backend/async-3.2.2
Bump async from 3.2.0 to 3.2.2 in /backend
2022-04-23 08:06:54 +02:00
Glassed Silver
4d51384ce6 Merge pull request #556 from Tzahi12345/dependabot/npm_and_yarn/async-2.6.4
Bump async from 2.6.3 to 2.6.4
2022-04-23 08:06:30 +02:00
Isaac Abadi
aa616af118 Fixed issue where navigating from one sub to another didn't cause the new one to load
Fixed subscription downloading as zip

Minor code cleanuip
2022-04-22 23:09:06 -04:00
Isaac Abadi
feebe3e2ba Fixed accidental reversion of styles.scss to much older version 2022-04-22 22:49:05 -04:00
Isaac Abadi
02e90fe818 Fixed issue where icons would not render properly 2022-04-22 22:43:25 -04:00
Isaac Abadi
a4cfafe63c Updated frontend and backend dependencies, fixed some security issues 2022-04-22 22:34:29 -04:00
Isaac Abadi
63e2e6dd3c Fixed build warnings 2022-04-22 22:14:21 -04:00
Isaac Abadi
5a44229e24 Fixed styles.scss 2022-04-22 22:07:50 -04:00
Isaac Abadi
5025b235b7 Updated angular material to v13 2022-04-22 19:03:55 -04:00
Isaac Abadi
5d540fc52a Updated angular to v13 2022-04-22 18:55:11 -04:00
Isaac Abadi
55dfc17d62 Updated ngx-file-drop to support angular v13 2022-04-22 17:51:20 -04:00
Isaac Abadi
2459403b22 Updated angular material 2022-04-22 17:46:45 -04:00
Isaac Abadi
ed5f910c33 Updated angular to v12 2022-04-22 17:40:53 -04:00
Isaac Abadi
468e7153e4 Updated ngx-videogular 2022-04-22 17:28:15 -04:00
dependabot[bot]
1bd713fe17 Bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-22 19:47:54 +00:00
dependabot[bot]
3df377a260 Bump async from 3.2.0 to 3.2.2 in /backend
Bumps [async](https://github.com/caolan/async) from 3.2.0 to 3.2.2.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/master/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v3.2.0...v3.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-22 19:47:28 +00:00
Glassed Silver
8314ab8fce Merge pull request #554 from Tzahi12345/tasks-and-maintenence-page
Tasks and maintenence page
2022-04-22 21:46:56 +02:00
Isaac Abadi
d32df84e3a Fixed build error 2022-04-22 15:25:26 -04:00
Isaac Abadi
2c3813f302 Removed httpclient import from player component 2022-04-22 00:40:34 -04:00
Isaac Abadi
df687263c5 Fixed bug that prevented files from being downloaded from the server. Reverted some changes from #528 as they are not needed 2022-04-22 00:35:58 -04:00
Isaac Abadi
7a4d91cea0 Removed import of unregistered files on startup as it's a task now 2022-04-21 22:08:47 -04:00
Isaac Abadi
b53d9c9710 Added ability to reset tasks
Refactored youtube-dl updating and added youtube-dl update task
2022-04-21 22:04:45 -04:00
Isaac Abadi
d2d125743e Fixed issue where restoring a DB backup would cause backup_local_db task to be stuck running
Slightly updated tasks UI
2022-04-21 19:56:09 -04:00
Isaac Abadi
a288163644 Added ability to backup remote DB
Added ability to restore DB
2022-04-21 19:29:50 -04:00
GlassedSilver
c008171850 add color picker to WS recs 2022-04-21 22:33:01 +02:00
Isaac Abadi
091f81bb38 Added UI for managing tasks
Added ability to schedule tasks based on timestamp

Fixed mismatched types between frontend and openapi yaml

Simplified imports for several backend components
2022-04-21 03:01:49 -04:00
Isaac Abadi
5b4d4d5f81 Added scheduler for tasks 2022-04-19 22:29:41 -04:00
Glassed Silver
0f4f5293de Merge pull request #551 from GlassedSilver/master
Code Cleanup in some places, fix for conversion errors, possibly better webm support, added dependency checks in docker compose file
2022-04-18 10:56:37 +02:00
GlassedSilver
16943847fc fix ffmpeg download with variable 2022-04-18 08:57:21 +02:00
GlassedSilver
f79b254040 using more recent ffmpeg + code cleanup 2022-04-18 07:29:28 +02:00
GlassedSilver
e7989e41f9 Merge branch 'master' of https://github.com/GlassedSilver/YoutubeDL-Material into master 2022-04-18 05:54:09 +02:00
GlassedSilver
a4d421d398 add downloader script for JVS's ffmpeg master blds 2022-04-18 05:54:01 +02:00
Isaac Abadi
2b1771d30d Began work on tasks 2022-04-17 23:37:47 -04:00
Glassed Silver
d6ed82134b Merge pull request #550 from GlassedSilver/master
revert ffmpeg changees
2022-04-18 01:57:31 +02:00
GlassedSilver
74f5a9983d revert ffmpeg changees 2022-04-18 01:56:16 +02:00
Glassed Silver
07a259f128 Merge pull request #549 from GlassedSilver/master
try to fix ffmpeg install from edge
2022-04-17 23:48:12 +02:00
GlassedSilver
de79efafa6 try to fix ffmpeg install from edge 2022-04-17 23:46:08 +02:00
Glassed Silver
ea214ca953 Merge pull request #548 from GlassedSilver/master
Switch to alpine edge community repo for ffmpeg (fixed syntax)
2022-04-17 21:40:29 +02:00
GlassedSilver
f11baf6d4b fix missing \ in DOCKEFILE 2022-04-17 21:38:01 +02:00
Glassed Silver
7d0d665798 Merge pull request #547 from GlassedSilver/master
Switch to alpine edge community repo for ffmpeg
2022-04-17 21:35:02 +02:00
GlassedSilver
9e35e0fe4d Switch to alpine edge community repo for ffmpeg 2022-04-17 21:15:52 +02:00
Glassed Silver
4a148d5148 Merge pull request #537 from Tzahi12345/dependabot/npm_and_yarn/backend/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /backend
2022-04-09 11:51:42 +02:00
Glassed Silver
4fd60c8a5d Merge pull request #539 from Tzahi12345/dependabot/npm_and_yarn/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6
2022-04-09 11:51:34 +02:00
Glassed Silver
d8cee00e7a Merge pull request #538 from Tzahi12345/dependabot/npm_and_yarn/backend/ansi-regex-3.0.1
Bump ansi-regex from 3.0.0 to 3.0.1 in /backend
2022-04-09 11:50:58 +02:00
dependabot[bot]
478d0c8fad Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 09:39:19 +00:00
dependabot[bot]
d15709008c Bump ansi-regex from 3.0.0 to 3.0.1 in /backend
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 09:38:59 +00:00
dependabot[bot]
80aba6b4a7 Bump minimist from 1.2.5 to 1.2.6 in /backend
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 09:38:56 +00:00
Glassed Silver
c4cf981e57 Merge pull request #536 from Tzahi12345/dependabot/npm_and_yarn/backend/moment-2.29.2
Bump moment from 2.29.1 to 2.29.2 in /backend
2022-04-09 11:38:29 +02:00
dependabot[bot]
7d3079f042 Bump moment from 2.29.1 to 2.29.2 in /backend
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 07:19:13 +00:00
Glassed Silver
ffa1737df3 Merge pull request #535 from GlassedSilver/Readme-Update
Made Global custom args hint clearer. (scope: EN)
2022-04-08 19:18:57 +02:00
GlassedSilver
f1a7986e7a Made Global custom args hint clearer. (scope: EN) 2022-04-08 19:17:14 +02:00
Glassed Silver
8456accda0 Adding link to MongoDB transfer guide in README 2022-03-25 13:26:57 +01:00
Glassed Silver
d4ef7066df Update README to include legal disclaimer
and one small change in the notice about best practices. (because I'm forgetful ok)
2022-03-25 11:17:37 +01:00
Glassed Silver
b76a7f2e43 Update README to highlight usage of nightlies...
... and best practices with large datasets.
2022-03-25 11:09:54 +01:00
Glassed Silver
98e77f65f9 Merge pull request #343 from controlol/patch-1
solve path problem subscriptions
2022-03-25 10:48:36 +01:00
Glassed Silver
ee5d6dfba8 Merge branch 'master' into patch-1 2022-03-25 10:41:31 +01:00
Glassed Silver
3538132d24 Merge pull request #533 from Tzahi12345/dependabot/npm_and_yarn/follow-redirects-1.14.9
Bump follow-redirects from 1.13.0 to 1.14.9
2022-03-23 15:31:41 +01:00
dependabot[bot]
b6399eb876 Bump follow-redirects from 1.13.0 to 1.14.9
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.13.0 to 1.14.9.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.13.0...v1.14.9)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-23 13:21:08 +00:00
Glassed Silver
bcab211ed7 Merge pull request #479 from u0a1009/Added-Shortcut
Added Shortcut description
2022-03-23 14:06:29 +01:00
Glassed Silver
a753b6db95 Merge pull request #483 from GlassedSilver/Readme-Update
Git Bug Report template; better user guidance
2022-03-23 14:05:36 +01:00
Glassed Silver
26eb687ece Merge pull request #528 from chepe263/master
Add download video button on player component.
2022-03-23 14:03:19 +01:00
Glassed Silver
327e1efc95 Merge pull request #531 from Tzahi12345/dependabot/npm_and_yarn/url-parse-1.5.10
Bump url-parse from 1.5.1 to 1.5.10
2022-03-23 14:02:50 +01:00
Glassed Silver
3a4eb8afdb Merge pull request #532 from Tzahi12345/dependabot/npm_and_yarn/electron-13.6.6
Bump electron from 9.4.0 to 13.6.6
2022-03-23 14:01:51 +01:00
Glassed Silver
93d35dd97c Merge pull request #530 from Tzahi12345/dependabot/npm_and_yarn/backend/node-fetch-2.6.7
Bump node-fetch from 2.6.1 to 2.6.7 in /backend
2022-03-23 12:23:23 +01:00
dependabot[bot]
343a9bf70b Bump electron from 9.4.0 to 13.6.6
Bumps [electron](https://github.com/electron/electron) from 9.4.0 to 13.6.6.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v9.4.0...v13.6.6)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-23 09:32:46 +00:00
dependabot[bot]
699b3f5316 Bump url-parse from 1.5.1 to 1.5.10
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-23 09:30:37 +00:00
dependabot[bot]
910ae90882 Bump node-fetch from 2.6.1 to 2.6.7 in /backend
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-23 09:30:07 +00:00
Glassed Silver
605042fdf8 Merge pull request #478 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Weblate
2022-03-23 10:10:12 +01:00
Glassed Silver
118bed551a Merge pull request #517 from Tzahi12345/dependabot/npm_and_yarn/backend/ajv-6.12.6
Bump ajv from 6.12.0 to 6.12.6 in /backend
2022-03-23 10:09:54 +01:00
Glassed Silver
70fc4150d4 Merge pull request #518 from Tzahi12345/dependabot/npm_and_yarn/backend/follow-redirects-1.14.8
Bump follow-redirects from 1.14.4 to 1.14.8 in /backend
2022-03-23 10:09:39 +01:00
Glassed Silver
68e1388178 Merge pull request #524 from Tzahi12345/dependabot/npm_and_yarn/karma-6.3.16
Bump karma from 5.0.9 to 6.3.16
2022-03-23 10:09:19 +01:00
Glassed Silver
aeaa653b27 Merge pull request #529 from EgorBakanov/master
Fixed file type dropdown margin
2022-03-23 10:06:02 +01:00
Egor Bakanov
033d0d0658 Fixed file type dropdown margin 2022-03-22 13:16:13 +07:00
Guillermo Chavez
1980893d9c Add download video button on player component. 2022-03-20 23:14:56 -06:00
dependabot[bot]
a7c36898fa Bump karma from 5.0.9 to 6.3.16
Bumps [karma](https://github.com/karma-runner/karma) from 5.0.9 to 6.3.16.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v5.0.9...v6.3.16)

---
updated-dependencies:
- dependency-name: karma
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 02:19:07 +00:00
Maite Guix
9cb3b71b0f Translated using Weblate (Catalan)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2022-02-28 18:57:37 +01:00
Maxime Leroy
3dc03b3fa0 Translated using Weblate (French)
Currently translated at 99.6% (301 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2022-02-26 07:57:20 +01:00
S3aBreeze
2c49b6e260 Translated using Weblate (Russian)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2022-02-18 15:54:31 +01:00
Heimen Stoffels
1faabda5f0 Translated using Weblate (Dutch)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2022-02-15 11:55:51 +01:00
Allan Nordhøy
82321f28cd Translated using Weblate (Norwegian Bokmål)
Currently translated at 66.5% (201 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nb_NO/
2022-02-14 09:55:26 +01:00
Maite Guix
c80670d0a3 Translated using Weblate (Catalan)
Currently translated at 99.6% (301 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2022-02-14 09:55:25 +01:00
Allan Nordhøy
084367cb50 Translated using Weblate (English)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/en/
2022-02-14 09:55:24 +01:00
dependabot[bot]
72af057a0e Bump follow-redirects from 1.14.4 to 1.14.8 in /backend
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.4 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.4...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-13 21:18:23 +00:00
dependabot[bot]
8d88a14a11 Bump ajv from 6.12.0 to 6.12.6 in /backend
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.0 to 6.12.6.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.0...v6.12.6)

---
updated-dependencies:
- dependency-name: ajv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-13 10:19:47 +00:00
Joeri Colman
b46b9ea386 Added deleteAllFiles api endpoint 2022-02-08 10:07:37 +01:00
Dawson
f305deadc7 Added translation using Weblate (Hindi) 2022-02-04 10:15:59 +01:00
Kachelkaiser
850a3ba12f Translated using Weblate (German)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2022-01-30 16:52:35 +01:00
Maite Guix
0fb4593dc3 Translated using Weblate (Catalan)
Currently translated at 99.6% (301 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2022-01-25 03:31:55 +01:00
Vitor V
cf6546dd02 Translated using Weblate (Portuguese)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt/
2022-01-17 19:53:51 +01:00
Jagadeesh Vijay Varma
dd7354bd77 Translated using Weblate (Telugu)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/te/
2021-12-27 08:50:41 +01:00
Jagadeesh Vijay Varma
4a0000af5f Added translation using Weblate (Telugu) 2021-12-25 08:44:10 +01:00
Nikita Epifanov
71eaf70b2e Translated using Weblate (Russian)
Currently translated at 99.6% (301 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2021-12-17 14:51:32 +01:00
Diamond
548cb654d5 Translated using Weblate (Russian)
Currently translated at 97.0% (293 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2021-12-16 06:51:02 +01:00
Biepa
0747c28d8a Translated using Weblate (German)
Currently translated at 88.0% (266 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2021-12-16 06:51:00 +01:00
Maxime Leroy
4e2b7c4a56 Translated using Weblate (French)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2021-11-16 07:51:14 +01:00
Bitpaint
ef2309d2f3 Translated using Weblate (French)
Currently translated at 89.7% (271 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2021-11-02 03:37:23 +01:00
GlassedSilver
cae88433b6 Git Bug Report template; better user guidance 2021-10-12 07:25:20 +02:00
Minhyuk Lee
b922a904d0 Update README.md 2021-10-07 16:02:04 +09:00
min
86fc02f9e4 Translated using Weblate (Korean)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2021-10-07 04:04:04 +02:00
Tzahi12345
88cc8d0e81 Merge pull request #226 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Weblate
2021-10-01 02:03:14 -06:00
Isaac Abadi
829b8af942 Fixed mangled merge where getSubscriptions became getAllSubscriptions 2021-10-01 01:54:44 -06:00
Isaac Abadi
be94bc81c8 Removed prepare statement in package.json 2021-10-01 01:38:23 -06:00
Isaac Abadi
b9dabcf2f4 Updated dev default.json 2021-09-30 22:48:17 -06:00
Isaac Abadi
609f749d6c Updated API docs to fix errors 2021-09-30 22:38:57 -06:00
Tzahi12345
cc75e94408 Merge pull request #218 from NotWoods/api-generator
Generate types from OpenAPI
2021-09-30 22:29:03 -06:00
Isaac Abadi
bff40d97bb Invalid locales do not prevent languages from being selected anymore
Fixed video previews on mouse over when in multi-user mode
2021-09-30 22:27:44 -06:00
Isaac Abadi
c5db1d30e2 Added missing routes to API
Changed getDBInfo from post to get request
2021-09-30 22:18:55 -06:00
Isaac Abadi
94006ef794 Updated API
Removed unused component
2021-09-30 19:37:21 -06:00
Isaac Abadi
45be270b6f Dockerfile forces PM2_HOME to be in /app directory 2021-09-30 08:55:38 -06:00
Isaac Abadi
b2d8c4ef55 Disabled PM2 logging to $HOME/.pm2 2021-09-30 08:42:20 -06:00
Isaac Abadi
a7f1f1eb8e Fixed issue where language file generation occured after supported_locales.json was created 2021-09-29 23:29:26 -06:00
Isaac Abadi
3937700eff Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into api-generator 2021-09-29 02:38:42 -06:00
Isaac Abadi
f0c3837ee5 Translation JSONs are now generated at build time, removing the necessity to manually run xliff-to-json
- added postbuild.mjs to facilitate this
- all ng build --prod's have been replaced with npm run build
2021-09-29 00:36:56 -06:00
Isaac Abadi
c5f7cd1874 Converted input on the home page to textarea, maintaining same style but allowing an arbitrary number of urls to be entered 2021-09-28 21:36:36 -06:00
Tzahi12345
69767a82a9 Merge pull request #468 from Tzahi12345/forever-to-pm2
Use PM2 instead of ForeverJS
2021-09-28 20:34:05 -06:00
Isaac Abadi
84fa425a99 Fixed issue where selecting video quality would
Main component cleanup

Removed deprecated file card component
2021-09-28 20:27:01 -06:00
Isaac Abadi
84187b9474 Fixed issue where selecting video quality would
Main component cleanup

Removed deprecated file card component
2021-09-28 20:14:57 -06:00
Hosted Weblate
3fc83e636b Merge remote-tracking branch 'origin/master' 2021-09-29 03:45:10 +02:00
dejan995
9b88150555 Translated using Weblate (Macedonian)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/mk/
2021-09-28 22:15:33 +02:00
MeblIkea
90120e821d Translated using Weblate (French)
Currently translated at 88.4% (267 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2021-09-28 22:15:30 +02:00
min
90dd39b9eb Translated using Weblate (Korean)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2021-09-28 13:49:01 +02:00
dejan995
5fee3fd281 Added translation using Weblate (Macedonian) 2021-09-28 13:48:58 +02:00
Isaac Abadi
dbeeb32d48 Updated Dockerfile and entrypoint to use pm2 instead of forever 2021-09-27 18:11:38 -06:00
min
17e8861c40 Translated using Weblate (Korean)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2021-09-27 06:37:55 +02:00
Isaac Abadi
46087f622e Switched forever.js to pm2
Updated winston
2021-09-26 02:40:05 -06:00
Isaac Abadi
5dd48035fb Improved archive management for subscription downloads
Downloads that fail due to existing in the archive now appears as an error in the manager

Fixed issue where redownloading sub videos wouldn't occur if it was not cleared from the download manager
2021-09-25 22:33:22 -06:00
min
40cd4ead1b Translated using Weblate (Korean)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2021-09-25 19:37:00 +02:00
Heimen Stoffels
d60af699dc Translated using Weblate (Dutch)
Currently translated at 100.0% (302 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2021-09-25 19:36:58 +02:00
Tzahi12345
8981657084 Translated using Weblate (Spanish)
Currently translated at 84.1% (254 of 302 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2021-09-25 19:36:57 +02:00
Hosted Weblate
b5b9e84950 Merge remote-tracking branch 'origin/master' 2021-09-23 05:31:50 +02:00
Isaac Abadi
db53a12635 Added Korean translations and updated source translations file 2021-09-22 21:29:15 -06:00
Tzahi12345
8cd21bf433 Merge pull request #322 from Tzahi12345/dependabot/npm_and_yarn/electron-9.4.0
Bump electron from 8.2.0 to 9.4.0
2021-09-22 20:24:53 -06:00
Tzahi12345
c33acfb3de Merge pull request #447 from Tzahi12345/dependabot/npm_and_yarn/backend/axios-0.21.2
Bump axios from 0.21.1 to 0.21.2 in /backend
2021-09-22 20:24:43 -06:00
Isaac Abadi
562eaa1b9b Added support for generate NFO files for Kodi
Minor UI updates to settings
2021-09-22 19:27:25 -06:00
Isaac Abadi
ec7f04552f Fixed mangled Subject initialization in main component 2021-09-21 23:56:04 -06:00
Isaac Abadi
75fc09ed99 Improved arg simulation -- now uses same method as the actual download
Added checkbox for advanced custom args to either replace all args or append
2021-09-21 23:51:07 -06:00
Isaac Abadi
8aa354ac24 Fixed issue where navigating to a sub's video would play all videos from the subscription 2021-09-21 20:05:09 -06:00
Isaac Abadi
58a0dc4afe Version and commit info is now generated during autobuilds and can be viewed in the about dialog
Prepared removal of JSON translations from repo to move towards XLIFF-only
2021-09-21 20:05:09 -06:00
Glassed Silver
0e37d83740 Merge pull request #455 from GlassedSilver/Readme-Update
Readme update
2021-09-20 00:29:23 +02:00
Isaac Abadi
27faff054e Recent videos component now remembers sort order between page reloads 2021-09-19 14:56:32 -04:00
Isaac Abadi
a71d9f5c7e Added tests for arg generation and laid some plumbing for better arg simulation in the UI 2021-09-19 14:44:02 -04:00
Isaac Abadi
759637c1cf Fixed issue where per-subscription custom args were not being applied 2021-09-19 14:29:12 -04:00
Isaac Abadi
33f23c3ca9 Fixed issue where youtube-dl autoupdates broke if checkExistsWithTimeout failed the first time 2021-09-19 14:24:18 -04:00
GlassedSilver
176c99f813 Reference host-specific instructions 2021-09-18 17:41:25 +02:00
GlassedSilver
f7e0b3e86b [DRAFT!] Bump alpine: pinned '3.12' → 'latest' 2021-09-18 17:22:11 +02:00
GlassedSilver
bd2443b1e9 docker-compose.yml: Use YoutubeDL-Material nightly 2021-09-18 16:59:49 +02:00
min
d545926821 Translated using Weblate (Korean)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2021-09-18 08:38:46 +02:00
min
70cc611dfe Added translation using Weblate (Korean) 2021-09-18 08:38:46 +02:00
Allan Nordhøy
244e394924 Translated using Weblate (Norwegian Bokmål)
Currently translated at 56.7% (147 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nb_NO/
2021-09-18 08:38:46 +02:00
Reza Almanda
60030ac525 Translated using Weblate (Indonesian)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/id/
2021-09-18 08:38:46 +02:00
Allan Nordhøy
3651a021ce Added translation using Weblate (Norwegian Bokmål) 2021-09-18 08:38:46 +02:00
Kaantaja
1cdae9f26f Translated using Weblate (Finnish)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fi/
2021-09-18 08:38:46 +02:00
Kaantaja
f4854e10ad Added translation using Weblate (Finnish) 2021-09-18 08:38:46 +02:00
mamingwang
8f5361bd1a Added translation using Weblate (Basa (Cameroon)) 2021-09-18 08:38:46 +02:00
UnlimitedCookies
7be4ad4d41 Translated using Weblate (German)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2021-09-18 08:38:46 +02:00
Adolfo Jayme Barrientos
ea5756293d Translated using Weblate (Spanish)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2021-09-18 08:38:46 +02:00
Nikita Epifanov
d53c6d88ef Translated using Weblate (Russian)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2021-09-18 08:38:46 +02:00
Adolfo Jayme Barrientos
62fe940b2f Translated using Weblate (Catalan)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2021-09-18 08:38:46 +02:00
Adolfo Jayme Barrientos
09beaa6c39 Translated using Weblate (Spanish)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2021-09-18 08:38:46 +02:00
Heimen Stoffels
71ed7c45ac Translated using Weblate (Dutch)
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2021-09-18 08:38:46 +02:00
Eric
a9244e28a7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (259 of 259 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2021-09-18 08:38:46 +02:00
Isaac Abadi
f689609941 Fixed missing rxjs import 2021-09-17 01:15:08 -04:00
dependabot[bot]
1e9eec1b55 Bump electron from 8.2.0 to 9.4.0
Bumps [electron](https://github.com/electron/electron) from 8.2.0 to 9.4.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v8.2.0...v9.4.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-16 19:36:01 +00:00
dependabot[bot]
677af3712b Bump axios from 0.21.1 to 0.21.2 in /backend
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-16 19:35:35 +00:00
Tzahi12345
5fd9d93007 Merge pull request #420 from Tzahi12345/download-manager
Download manager
2021-09-16 15:34:41 -04:00
Isaac Abadi
7ee34d21eb Disables download cancelling for now 2021-09-16 15:28:25 -04:00
Isaac Abadi
66c184a2e6 Fixes issue with broken builds 2021-09-16 15:25:19 -04:00
Isaac Abadi
13f6f698b7 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into download-manager 2021-09-16 15:22:24 -04:00
Isaac Abadi
b08325c1e3 Added ability to filter for only audio and only video files in the home page 2021-09-15 10:47:46 -06:00
Isaac Abadi
070d3fed57 Improved error handling for downloads 2021-09-15 10:04:25 -06:00
Isaac Abadi
775a1766d8 Added max concurrent downloads setting
Fixed issue where navigating to a subscription video would make the player behave like a playlist for the whole sub
2021-09-15 09:44:31 -06:00
Isaac Abadi
dbefb66021 Fixed issue where errored downloads would result in an infinite loop of error messages in the home page
Added dialog to view error from an errored out download
2021-09-13 23:19:59 -06:00
Isaac Abadi
3241d6aaaf Added download manager to home page if autoplay is disabled
Fixed bug where the UI attempted to generate a preview URL for placeholder file cards

Fixed bug where file renaming was always attempted even when not necessary
2021-09-13 22:42:37 -06:00
Tzahi12345
d014c6facb Merge pull request #443 from KuroSetsuna29/fix-ldap-login
Fixed issue preventing LDAP to create new account
2021-09-12 12:57:51 -06:00
KuroSetsuna29
b25ab70732 Fixed issue preventing LDAP to create new account 2021-09-11 02:48:53 -04:00
Isaac Abadi
f9b8e78655 Fixed bug where auto builds would create a users file instead of a directory 2021-09-06 16:18:52 -06:00
Isaac Abadi
acad7cc057 Minor code cleanup 2021-09-06 16:15:52 -06:00
Isaac Abadi
c3d91e89a8 Get downloads now supports filtering by uids 2021-09-06 16:12:51 -06:00
Isaac Abadi
97c5102eb9 Limited video previews to video files only 2021-08-25 23:54:56 -06:00
Isaac Abadi
865185d277 Added ability to pause and resume all downloads
Removed backend dependency on queue library
2021-08-25 23:45:56 -06:00
Isaac Abadi
a36794fd4f Improved video preview behavior 2021-08-25 23:44:22 -06:00
Isaac Abadi
6639305771 Added video previews when hovering over a file card 2021-08-25 23:36:31 -06:00
Isaac Abadi
cca76dd248 Code cleanup 2021-08-24 22:05:02 -06:00
Isaac Abadi
d899f88164 Added button to edit a subscription from the subscriptions page 2021-08-24 21:34:10 -06:00
Isaac Abadi
09b3c752d9 Removed downlload delay setting for subscriptions
Subscription downloads already queued are now not requeued on the next check

Headers in download queue table are now sortable

Added button to clear all finished downloads in the downloads manager
2021-08-24 21:33:43 -06:00
Isaac Abadi
71bb91b6e6 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into download-manager 2021-08-23 20:22:22 -06:00
Isaac Abadi
f9b1414460 Logic to avoid duplicates for subscription files now uses the video URL instead of its path 2021-08-23 20:18:28 -06:00
Isaac Abadi
6eb1e2f898 Fixed issue where different path formatting would lead files to get duplicated in the DB 2021-08-22 23:06:16 -06:00
Isaac Abadi
30505d0e8b Cleaned up unused code in subscriptions 2021-08-22 22:50:16 -06:00
Isaac Abadi
48ab1836ca Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into download-manager 2021-08-22 22:34:19 -06:00
Isaac Abadi
20cedb6c29 Pagination and filtering of files is now server-side
Importing unregistered files does not block server start anymore
2021-08-22 22:31:01 -06:00
Isaac Abadi
9f5b6122fa Added additional protections to verify that the DB is initialized before downloader does
Began work on watching entire subscriptions as a playlist

Subscriptions now use the new download manager to download files
2021-08-21 21:54:40 -06:00
GlassedSilver
5321624604 Update README.md
Fixed Contributors link
2021-08-20 10:00:03 +02:00
Isaac Abadi
8828af4174 Fixed issue where config items that defaulted to false would not be created if they were missing 2021-08-19 23:22:37 -06:00
Isaac Abadi
2bb4860a36 Fixed issue where if multi user mode was not defined, subscriptions could not be retrieved 2021-08-19 23:09:00 -06:00
Isaac Abadi
ce3d540633 Forces file registration to avoid registering a file that already exists in an atomic fasion 2021-08-13 19:40:06 -06:00
Isaac Abadi
f7b152fcf6 Download manager is now per user
Replaced multi download mode with autoplay checkbox
2021-08-13 16:28:28 -06:00
Isaac Abadi
f892a4a305 Download manager is now thread safe 2021-08-10 23:57:26 -06:00
Isaac Abadi
fc55961822 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into download-manager 2021-08-10 21:33:31 -06:00
Isaac Abadi
ebfa49240c Added methods to modify download state
Added missing optionalJwt calls in several routes
2021-08-10 21:32:13 -06:00
Isaac Abadi
9e60d9fe3e Fixed issue where some some videos would send many requests to SponsorBlock when only one was needed 2021-08-09 00:23:09 -06:00
Isaac Abadi
ecef8842ae Converted downloads page to new downloads schema 2021-08-09 00:22:15 -06:00
Isaac Abadi
8cc653787f Cleaned up app.js backend code 2021-08-09 00:21:36 -06:00
Isaac Abadi
0360469c5a Download manager is now functional
Added UI support for new downloads schema

Implemented draft test for downloads

Cleaned up unused code snippets
2021-08-08 21:29:31 -06:00
Isaac Abadi
5a90be7703 Logger is now separated into its own module
Added eslint and fixed many logic errors based on its recommendations
2021-08-08 14:54:24 -06:00
Isaac Abadi
ff403d18d1 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into download-manager 2021-08-08 13:43:49 -06:00
Isaac Abadi
11284cb1b3 Fixed unnecessary (and mispelled) class for settings element 2021-08-08 06:00:15 -06:00
Isaac Abadi
8b1a1a56e3 Added SponsorBlock support for skipping ads when viewing supported videos
Updated default value for subscriptions check interval (new value of 86,400 only existed in the default.json)

Text inputs in settings menu are now larger
2021-08-08 05:56:47 -06:00
Tzahi12345
32370280ab Merge pull request #416 from BrianCArnold/master
Added change to make player work on iOS without being full screen.
2021-08-07 22:39:34 -06:00
Brian C. Arnold
240d6569fa Added change to make player work on iOS inline. 2021-08-06 15:50:11 -04:00
Isaac Abadi
2927a4564d Additional scaffolding for download manager
Added queue to npm backend dependencies
2021-08-05 18:57:54 -06:00
Isaac Abadi
5c94036625 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into download-manager 2021-08-02 19:59:54 -06:00
Isaac Abadi
7be90ccd94 Fixed bug where subscription videos would get duplicated 2021-08-02 18:50:44 -06:00
Isaac Abadi
01b6e22f83 Began scaffolding work for download manager 2021-08-02 18:41:30 -06:00
Isaac Abadi
b1385f451b Added option to rate limit downloads
Added option to force delay between videos in a subscription

Fixed issue where file handle was maintained on files deleted through unsubscribing
2021-08-01 22:19:15 -06:00
Tzahi12345
f40ac49082 Merge pull request #413 from Tzahi12345/cleaner-playlists-and-settings
Dedicated settings page and UI cleanup
2021-08-01 21:17:50 -06:00
controlol
7e9d1d30da patch qualityPath
qualityPath should not be escaped, this results in `could not find format error`
2021-03-04 13:46:39 +01:00
controlol
b9f6d29061 escape paths for use with commandline
escape qualityPath and fullOutput for use with commandline
In order to successfully download files from subscriptions these strings should be  escaped to work properly in the commandline. 
I have seen you use almost the same function (generateArgs()) in app.js. Even though I have never had a problem with this outside subscriptions I would suggest to do the same for that function starting on line 1405
2021-03-04 12:45:54 +01:00
Tiger Oakes
2cf0c61fac Default booleans to false 2020-10-04 19:19:31 -07:00
Tiger Oakes
389c5b5df3 Take out import type 2020-10-04 19:19:31 -07:00
Tiger Oakes
d1311d00ea Use named arguments with download file 2020-10-04 19:19:18 -07:00
Tiger Oakes
1112548246 Commit api types 2020-10-04 19:19:18 -07:00
Tiger Oakes
70d1afce76 Move user schemas so they can be imported 2020-10-04 19:19:18 -07:00
Tiger Oakes
fe7a3075d6 Rename last few active API routes 2020-10-04 19:19:18 -07:00
Tiger Oakes
4d74c375f4 Add playlist types 2020-10-04 19:18:57 -07:00
Tiger Oakes
62c79c267e Add additional types, mainly for subscriptions 2020-10-04 19:15:51 -07:00
Tiger Oakes
b667e1dc79 Add additional named types 2020-10-04 19:15:51 -07:00
Tiger Oakes
bce0115285 Generate types from OpenAPI 2020-10-04 19:15:51 -07:00
243 changed files with 35653 additions and 16963 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
.git
db
appdata
audio
video
subscriptions
users

20
.eslintrc.json Normal file
View File

@@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

View File

@@ -27,5 +27,12 @@ If applicable, add screenshots to help explain your problem.
- YoutubeDL-Material version
- Docker tag: <tag> (optional)
Ideally you'd copy the info as presented on the "About" dialogue
in YoutubeDL-Material.
(for that, click on the three dots on the top right and then
check "installation details". On later versions of YoutubeDL-
Material you will find pretty much all the crucial information
here that we need in most cases!)
**Additional context**
Add any other context about the problem here. For example, a YouTube link.

View File

@@ -25,8 +25,21 @@ jobs:
cd backend
npm install
sudo npm install -g @angular/cli
- 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
with:
name: "version.json"
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: build
run: ng build --prod
run: npm run build
- name: prepare artifact upload
shell: pwsh
run: |
@@ -38,7 +51,7 @@ jobs:
Copy-Item -Path ./backend/public -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/subscriptions -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/video -Recurse -Destination ./build/youtubedl-material
New-Item -Path ./build/youtubedl-material -Name users
New-Item -Path ./build/youtubedl-material -Name users -ItemType Directory
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
- name: upload build artifact

27
.github/workflows/docker-pr.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: docker-pr
on:
pull_request:
branches: [master]
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
with:
name: "version.json"
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: Build docker images
run: docker build . -t tzahi12345/youtubedl-material:nightly-pr

View File

@@ -13,6 +13,19 @@ 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
with:
name: "version.json"
json: '{"type": "docker", "tag": "latest", "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

View File

@@ -3,6 +3,16 @@ name: docker
on:
push:
branches: [master]
paths-ignore:
- '.github/**'
- '.vscode/**'
- 'chrome-extension/**'
- 'releases/**'
- '**/**.md'
- '**.crx'
- '**.pem'
- '.dockerignore'
- '.gitignore'
jobs:
build-and-push:
@@ -10,6 +20,19 @@ 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
with:
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
@@ -26,4 +49,8 @@ jobs:
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true
tags: tzahi12345/youtubedl-material:nightly
# Defaults:
# DOCKERHUB_USERNAME : tzahi12345
# DOCKERHUB_REPO : youtubedl-material
# DOCKERHUB_MASTER_TAG: nightly
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{secrets.DOCKERHUB_MASTER_TAG}}

12
.gitignore vendored
View File

@@ -25,6 +25,7 @@
!.vscode/extensions.json
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
@@ -65,4 +66,13 @@ backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt
backend/public
backend/public
src/assets/i18n/*.json
# User Files
db/
appdata/
audio/
video/
subscriptions/
users/

View File

@@ -1,9 +1,26 @@
FROM alpine:3.12 as frontend
FROM ubuntu:22.04 AS ffmpeg
RUN apk add --no-cache \
npm
ENV DEBIAN_FRONTEND=noninteractive
RUN npm install -g @angular/cli
COPY docker-build.sh .
RUN sh ./docker-build.sh
#--------------# Stage 2
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
WORKDIR /build
COPY [ "package.json", "package-lock.json", "/build/" ]
@@ -11,38 +28,45 @@ RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN ng build --prod
RUN npm run build
#--------------#
#--------------# Final Stage
FROM alpine:3.12
FROM ubuntu:22.04
ENV UID=1000 \
GID=1000 \
USER=youtube
USER=youtube \
NO_UPDATE_NOTIFIER=true
ENV NO_UPDATE_NOTIFIER=true
ENV FOREVER_ROOT=/app/.forever
ENV DEBIAN_FRONTEND=noninteractive
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
RUN groupadd -g $GID $USER && useradd --system -g $USER --uid $UID $USER
RUN apk add --no-cache \
ffmpeg \
RUN apt-get update && apt-get -y install \
npm \
python2 \
python3 \
su-exec \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
gosu \
atomicparsley && \
apt-get install -f && \
apt-get autoremove --purge && \
apt-get autoremove && \
apt-get clean && \
rm -rf /var/lib/apt
WORKDIR /app
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/" ]
RUN npm install forever -g
RUN npm install && chown -R $UID:$GID ./
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=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "forever", "app.js" ]
CMD [ "pm2-runtime", "pm2.config.js" ]

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,22 @@
[![GitHub issues badge](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![License badge](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 11](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 13](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
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!
@@ -67,7 +79,7 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
@@ -77,6 +89,10 @@ Alternatively, you can port forward the port specified in the config (defaults t
## Docker
### 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)
### Setup
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
@@ -106,6 +122,12 @@ To get started, go to the settings menu and enable the public API from the *Extr
Once you have enabled the API and have the key, you can start sending requests by adding the query param `apiKey=API_KEY`. Replace `API_KEY` with your actual API key, and you should be good to go! Nearly all of the backend should be at your disposal. View available endpoints in the link above.
## iOS Shortcut
If you are using iOS, try YoutubeDL-Material more conveniently with a Shortcut. With this Shorcut, you can easily start downloading YouTube video with just two taps! (Or maybe three?)
You can download Shortcut [here.](https://routinehub.co/shortcut/10283/)
## Contributing
If you're interested in contributing, first: awesome! Second, please refer to the guidelines/setup information located in the [Contributing](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Contributing) wiki page, it's a helpful way to get you on your feet and coding away.
@@ -124,12 +146,16 @@ Official translators:
* German - UnlimitedCookies
* Chinese - TyRoyal
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
See also the list of [contributors](https://github.com/Tzahi12345/YoutubeDL-Material/graphs/contributors) who participated in this project.
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
## Legal Disclaimer
This project is in no way affiliated with Google LLC, Alphabet Inc. or YouTube (or their subsidiaries) nor endorsed by them.
## Acknowledgments
* youtube-dl

21
SECURITY.md Normal file
View File

@@ -0,0 +1,21 @@
# Security Policy
## 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.
| Version | Supported |
| ------------- | ------------------ |
| 4.2 Nightlies | :white_check_mark: |
| 4.2 Release | :x: |
| < 4.2 | :x: |
## Reporting a Vulnerability
Please file an issue in our GitHub's repo, because this app
isn't meant to be safe to run as public instance yet, but rather as a LAN facing app.
We welcome PRs and help in general in making YTDL-M more secure, but it's not a priority as of now.

View File

@@ -17,7 +17,6 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"aot": true,
"outputPath": "backend/public",
"index": "src/index.html",
"main": "src/main.ts",
@@ -33,7 +32,17 @@
"styles": [
"src/styles.scss"
],
"scripts": []
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true,
"allowedCommonJsDependencies": [
"rxjs",
"crypto-js"
]
},
"configurations": {
"production": {
@@ -46,7 +55,6 @@
"optimization": true,
"outputHashing": "all",
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
@@ -60,7 +68,8 @@
"es": {
"localize": ["es"]
}
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@@ -152,16 +161,6 @@
"src/backend"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": []
}
}
}
},
@@ -176,15 +175,6 @@
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "youtube-dl-material:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": []
}
}
}
}

View File

@@ -1,49 +0,0 @@
FROM alpine:3.12 as frontend
RUN apk add --no-cache \
npm \
curl
RUN npm install -g @angular/cli
WORKDIR /build
RUN curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN ng build --prod
#--------------#
FROM arm32v7/alpine:3.12
COPY --from=frontend /build/qemu-arm-static /usr/bin
ENV UID=1000 \
GID=1000 \
USER=youtube
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
RUN apk add --no-cache \
ffmpeg \
npm \
python2 \
su-exec \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
WORKDIR /app
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

18
backend/.eslintrc.json Normal file
View File

@@ -0,0 +1,18 @@
{
"env": {
"node": true,
"es2021": true
},
"extends": [
"eslint:recommended"
],
"parser": "esprima",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [],
"rules": {
},
"root": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,16 @@
"custom_args": "",
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
"include_metadata": true,
"max_concurrent_downloads": 5,
"download_rate_limit": ""
},
"Extra": {
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"allow_autoplay": true,
"enable_downloads_manager": true,
"allow_playlist_categorization": true
},
@@ -30,7 +32,9 @@
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false
},
"Themes": {
"default_theme": "default",
@@ -65,8 +69,8 @@
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"jwt_expiration": 86400,
"jwt_expiration": 86400,
"logger_level": "info"
}
}
}
}

View File

@@ -1,7 +1,8 @@
const path = require('path');
const config_api = require('../config');
const consts = require('../consts');
const fs = require('fs-extra');
const logger = require('../logger');
const db_api = require('../db');
const jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4');
const bcrypt = require('bcryptjs');
@@ -12,20 +13,24 @@ var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
// other required vars
let logger = null;
let db_api = null;
let SERVER_SECRET = null;
let JWT_EXPIRATION = null;
let opts = null;
let saltRounds = null;
exports.initialize = function(db_api, input_logger) {
setLogger(input_logger)
setDB(db_api);
exports.initialize = function () {
/*************************
* Authentication module
************************/
if (db_api.database_initialized) {
setupRoles();
} else {
db_api.database_initialized_bs.subscribe(init => {
if (init) setupRoles();
});
}
saltRounds = 10;
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
@@ -53,12 +58,39 @@ exports.initialize = function(db_api, input_logger) {
}));
}
function setLogger(input_logger) {
logger = input_logger;
}
const setupRoles = async () => {
const required_roles = {
admin: {
permissions: [
'filemanager',
'settings',
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
]
},
user: {
permissions: [
'filemanager',
'subscriptions',
'sharing'
]
}
}
function setDB(input_db_api) {
db_api = input_db_api;
const role_keys = Object.keys(required_roles);
for (let i = 0; i < role_keys.length; i++) {
const role_key = role_keys[i];
const role_in_db = await db_api.getRecord('roles', {key: role_key});
if (!role_in_db) {
// insert task metadata into table if missing
await db_api.insertRecordIntoTable('roles', {
key: role_key,
permissions: required_roles[role_key]['permissions']
});
}
}
}
exports.passport = require('passport');
@@ -140,7 +172,7 @@ exports.registerUser = async function(req, res) {
exports.login = async (username, password) => {
const user = await db_api.getRecord('users', {name: username});
if (!user) { logger.error(`User ${username} not found`); false }
if (!user) { 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;
}
@@ -291,17 +323,12 @@ exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false
return file;
}
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) {
users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames});
return true;
}
exports.removePlaylist = async function(user_uid, playlistID) {
await db_api.removeRecord('playlist', {playlistID: playlistID});
return true;
}
exports.getUserPlaylists = async function(user_uid, user_files = null) {
exports.getUserPlaylists = async function(user_uid) {
return await db_api.getRecords('playlists', {user_uid: user_uid});
}

View File

@@ -1,19 +1,6 @@
const config_api = require('./config');
const utils = require('./utils');
var logger = null;
var db = null;
var users_db = null;
var db_api = null;
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger, input_db_api) {
setDB(input_db, input_users_db, input_db_api);
setLogger(input_logger);
}
const logger = require('./logger');
const db_api = require('./db');
/*
Categories:
@@ -72,7 +59,7 @@ async function getCategoriesAsPlaylists(files = null) {
const categories_as_playlists = [];
const available_categories = await getCategories();
if (available_categories && files) {
for (category of available_categories) {
for (let category of available_categories) {
const files_that_match = utils.addUIDsToCategory(category, files);
if (files_that_match && files_that_match.length > 0) {
category['thumbnailURL'] = files_that_match[0].thumbnailURL;
@@ -125,24 +112,23 @@ function applyCategoryRules(file_json, rules, category_name) {
return rules_apply;
}
async function addTagToVideo(tag, video, user_uid) {
// TODO: Implement
}
// async function addTagToVideo(tag, video, user_uid) {
// // TODO: Implement
// }
async function removeTagFromVideo(tag, video, user_uid) {
// TODO: Implement
}
// async function removeTagFromVideo(tag, video, user_uid) {
// // TODO: Implement
// }
// adds tag to list of existing tags (used for tag suggestions)
async function addTagToExistingTags(tag) {
const existing_tags = db.get('tags').value();
if (!existing_tags.includes(tag)) {
db.get('tags').push(tag).write();
}
}
// // adds tag to list of existing tags (used for tag suggestions)
// async function addTagToExistingTags(tag) {
// const existing_tags = db.get('tags').value();
// if (!existing_tags.includes(tag)) {
// db.get('tags').push(tag).write();
// }
// }
module.exports = {
initialize: initialize,
categorize: categorize,
getCategories: getCategories,
getCategoriesAsPlaylists: getCategoriesAsPlaylists

View File

@@ -1,3 +1,5 @@
const logger = require('./logger');
const fs = require('fs');
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
@@ -5,11 +7,7 @@ const debugMode = process.env.YTDL_MODE === 'debug';
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
var logger = null;
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_logger) {
setLogger(input_logger);
function initialize() {
ensureConfigFileExists();
ensureConfigItemsExist();
}
@@ -97,13 +95,13 @@ function getConfigItem(key) {
}
let path = CONFIG_ITEMS[key]['path'];
const val = Object.byString(config_json, path);
if (val === undefined && Object.byString(DEFAULT_CONFIG, path)) {
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
return Object.byString(DEFAULT_CONFIG, path);
}
return Object.byString(config_json, path);
};
}
function setConfigItem(key, value) {
let success = false;
@@ -175,7 +173,7 @@ module.exports = {
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
}
DEFAULT_CONFIG = {
const DEFAULT_CONFIG = {
"YoutubeDLMaterial": {
"Host": {
"url": "http://example.com",
@@ -189,14 +187,16 @@ DEFAULT_CONFIG = {
"custom_args": "",
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
"include_metadata": true,
"max_concurrent_downloads": 5,
"download_rate_limit": ""
},
"Extra": {
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"allow_autoplay": true,
"enable_downloads_manager": true,
"allow_playlist_categorization": true
},
@@ -207,7 +207,9 @@ DEFAULT_CONFIG = {
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false
},
"Themes": {
"default_theme": "default",
@@ -216,7 +218,7 @@ DEFAULT_CONFIG = {
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_check_interval": "86400",
"redownload_fresh_uploads": false
},
"Users": {

View File

@@ -1,4 +1,4 @@
let CONFIG_ITEMS = {
exports.CONFIG_ITEMS = {
// Host
'ytdl_url': {
'key': 'ytdl_url',
@@ -42,6 +42,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_include_metadata',
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
},
'ytdl_max_concurrent_downloads': {
'key': 'ytdl_max_concurrent_downloads',
'path': 'YoutubeDLMaterial.Downloader.max_concurrent_downloads'
},
'ytdl_download_rate_limit': {
'key': 'ytdl_download_rate_limit',
'path': 'YoutubeDLMaterial.Downloader.download_rate_limit'
},
// Extra
'ytdl_title_top': {
@@ -60,9 +68,9 @@ let CONFIG_ITEMS = {
'key': 'ytdl_download_only_mode',
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
},
'ytdl_allow_multi_download_mode': {
'key': 'ytdl_allow_multi_download_mode',
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
'ytdl_allow_autoplay': {
'key': 'ytdl_allow_autoplay',
'path': 'YoutubeDLMaterial.Extra.allow_autoplay'
},
'ytdl_enable_downloads_manager': {
'key': 'ytdl_enable_downloads_manager',
@@ -102,6 +110,15 @@ let CONFIG_ITEMS = {
'key': 'ytdl_twitch_auto_download_chat',
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
},
'ytdl_use_sponsorblock_api': {
'key': 'ytdl_use_sponsorblock_api',
'path': 'YoutubeDLMaterial.API.use_sponsorblock_API'
},
'ytdl_generate_nfo_files': {
'key': 'ytdl_generate_nfo_files',
'path': 'YoutubeDLMaterial.API.generate_NFO_files'
},
// Themes
'ytdl_default_theme': {
@@ -126,10 +143,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_subscriptions_check_interval',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
},
'ytdl_subscriptions_check_interval': {
'key': 'ytdl_subscriptions_check_interval',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
},
'ytdl_subscriptions_redownload_fresh_uploads': {
'key': 'ytdl_subscriptions_redownload_fresh_uploads',
'path': 'YoutubeDLMaterial.Subscriptions.redownload_fresh_uploads'
@@ -198,7 +211,7 @@ let CONFIG_ITEMS = {
}
};
AVAILABLE_PERMISSIONS = [
exports.AVAILABLE_PERMISSIONS = [
'filemanager',
'settings',
'subscriptions',
@@ -207,11 +220,85 @@ AVAILABLE_PERMISSIONS = [
'downloads_manager'
];
const DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.2',
DETAILS_BIN_PATH: DETAILS_BIN_PATH
}
// args that have a value after it (e.g. -o <output> or -f <format>)
const YTDL_ARGS_WITH_VALUES = [
'--default-search',
'--config-location',
'--proxy',
'--socket-timeout',
'--source-address',
'--geo-verification-proxy',
'--geo-bypass-country',
'--geo-bypass-ip-block',
'--playlist-start',
'--playlist-end',
'--playlist-items',
'--match-title',
'--reject-title',
'--max-downloads',
'--min-filesize',
'--max-filesize',
'--date',
'--datebefore',
'--dateafter',
'--min-views',
'--max-views',
'--match-filter',
'--age-limit',
'--download-archive',
'-r',
'--limit-rate',
'-R',
'--retries',
'--fragment-retries',
'--buffer-size',
'--http-chunk-size',
'--external-downloader',
'--external-downloader-args',
'-a',
'--batch-file',
'-o',
'--output',
'--output-na-placeholder',
'--autonumber-start',
'--load-info-json',
'--cookies',
'--cache-dir',
'--encoding',
'--user-agent',
'--referer',
'--add-header',
'--sleep-interval',
'--max-sleep-interval',
'-f',
'--format',
'--merge-output-format',
'--sub-format',
'--sub-lang',
'-u',
'--username',
'-p',
'--password',
'-2',
'--twofactor',
'--video-password',
'--ap-mso',
'--ap-username',
'--ap-password',
'--audio-format',
'--audio-quality',
'--recode-video',
'--postprocessor-args',
'--metadata-from-title',
'--fixup',
'--ffmpeg-location',
'--exec',
'--convert-subs'
];
// we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
exports.CURRENT_VERSION = 'v4.2';

View File

@@ -1,24 +1,31 @@
var fs = require('fs-extra')
var path = require('path')
var utils = require('./utils')
const { uuid } = require('uuidv4');
const config_api = require('./config');
const { MongoClient } = require("mongodb");
const { uuid } = require('uuidv4');
const config_api = require('./config');
var utils = require('./utils')
const logger = require('./logger');
const low = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync');
const { BehaviorSubject } = require('rxjs');
const local_adapter = new FileSync('./appdata/local_db.json');
const local_db = low(local_adapter);
var logger = null;
var db = null;
var users_db = null;
var database = null;
let database = null;
exports.database_initialized = false;
exports.database_initialized_bs = new BehaviorSubject(false);
const tables = {
files: {
name: 'files',
primary_key: 'uid'
primary_key: 'uid',
text_search: {
title: 'text',
uploader: 'text',
uid: 'text'
}
},
playlists: {
name: 'playlists',
@@ -43,6 +50,14 @@ const tables = {
name: 'roles',
primary_key: 'key'
},
download_queue: {
name: 'download_queue',
primary_key: 'uid'
},
tasks: {
name: 'tasks',
primary_key: 'key'
},
test: {
name: 'test'
}
@@ -62,20 +77,14 @@ function setDB(input_db, input_users_db) {
exports.users_db = input_users_db
}
function setLogger(input_logger) {
logger = input_logger;
}
exports.initialize = (input_db, input_users_db, input_logger) => {
exports.initialize = (input_db, input_users_db) => {
setDB(input_db, input_users_db);
setLogger(input_logger);
// must be done here to prevent getConfigItem from being called before init
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
}
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
if (using_local_db && !custom_connection_string) return;
const success = await exports._connectToDB(custom_connection_string);
if (success) return true;
@@ -131,8 +140,13 @@ exports._connectToDB = async (custom_connection_string = null) => {
tables_list.forEach(async table => {
const primary_key = tables[table]['primary_key'];
if (!primary_key) return;
await database.collection(table).createIndex({[primary_key]: 1}, { unique: true });
if (primary_key) {
await database.collection(table).createIndex({[primary_key]: 1}, { unique: true });
}
const text_search = tables[table]['text_search'];
if (text_search) {
await database.collection(table).createIndex(text_search);
}
});
return true;
} catch(err) {
@@ -144,51 +158,17 @@ exports._connectToDB = async (custom_connection_string = null) => {
}
}
exports.registerFileDB = async (file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null, file_object = null) => {
let db_path = null;
const file_id = utils.removeFileExtension(file_path);
if (!file_object) file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
return false;
}
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (multiUserMode) file_object['user_uid'] = multiUserMode.user;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_obj;
}
exports.registerFileDB2 = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
if (!file_object) file_object = generateFileObject2(file_path, type);
exports.registerFileDB = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
if (!file_object) file_object = generateFileObject(file_path, type);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
return false;
}
utils.fixVideoMetadataPerms2(file_path, type);
utils.fixVideoMetadataPerms(file_path, type);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail2(file_path, type);
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_path);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
@@ -205,7 +185,7 @@ exports.registerFileDB2 = async (file_path, type, user_uid = null, category = nu
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile2(file_path, type)
utils.deleteJSONFile(file_path, type)
}
return file_obj;
@@ -223,36 +203,7 @@ async function registerFileDBManual(file_object) {
return file_object;
}
function generateFileObject(id, type, customPath = null, sub = null) {
if (!customPath && sub) {
customPath = getAppendedBasePathSub(sub, config_api.getConfigItem('ytdl_subscriptions_base_path'));
}
var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
if (!jsonobj) {
return null;
}
const ext = (type === 'audio') ? '.mp3' : '.mp4'
const file_path = utils.getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
// console.
var stats = fs.statSync(path.join(__dirname, file_path));
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
var size = stats.size;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = type === 'audio';
var description = jsonobj.description;
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
function generateFileObject2(file_path, type) {
function generateFileObject(file_path, type) {
var jsonobj = utils.getJSON(file_path, type);
if (!jsonobj) {
return null;
@@ -269,8 +220,7 @@ function generateFileObject2(file_path, type) {
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
var upload_date = utils.formatDateString(jsonobj.upload_date);
var size = stats.size;
@@ -353,6 +303,7 @@ exports.getFileDirectoriesAndDBs = async () => {
}
exports.importUnregisteredFiles = async () => {
const imported_files = [];
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
// run through check list and check each file to see if it's missing from the db
@@ -365,33 +316,21 @@ exports.importUnregisteredFiles = async () => {
const file = files[j];
// check if file exists in db, if not add it
const file_is_registered = !!(await exports.getRecord('files', {id: file.id, sub_id: dir_to_check.sub_id}))
const files_with_same_url = await exports.getRecords('files', {url: file.url, sub_id: dir_to_check.sub_id});
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
if (!file_is_registered) {
// add additional info
await exports.registerFileDB2(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
logger.verbose(`Added discovered file to the database: ${file.id}`);
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
if (file_obj) {
imported_files.push(file_obj['uid']);
logger.verbose(`Added discovered file to the database: ${file.id}`);
} else {
logger.error(`Failed to import ${file['path']} automatically.`);
}
}
}
}
}
exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => {
const preimported_file_paths = [];
const files = await utils.getDownloadedFilesByType(appendedBasePath, sub.type);
for (let i = 0; i < files.length; i++) {
const file = files[i];
// check if file exists in db, if not add it
const file_is_registered = await exports.getRecord('files', {id: file.id, sub_id: sub.id});
if (!file_is_registered) {
// add additional info
await exports.registerFileDB2(file['path'], sub.type, sub.user_uid, null, sub.id, null, file);
preimported_file_paths.push(file['path']);
logger.verbose(`Preemptively added subscription file to the database: ${file.id}`);
}
}
return preimported_file_paths;
return imported_files;
}
exports.addMetadataPropertyToDB = async (property_key) => {
@@ -556,6 +495,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`);
// get ID from JSON
@@ -623,7 +563,22 @@ exports.insertRecordIntoTable = async (table, doc, replaceFilter = null) => {
return true;
}
if (replaceFilter) await database.collection(table).deleteMany(replaceFilter);
if (replaceFilter) {
const output = await database.collection(table).bulkWrite([
{
deleteMany: {
filter: replaceFilter
}
},
{
insertOne: {
document: doc
}
}
]);
logger.debug(`Inserted doc into ${table} with filter: ${JSON.stringify(replaceFilter)}`);
return !!(output['result']['ok']);
}
const output = await database.collection(table).insertOne(doc);
logger.debug(`Inserted doc into ${table}`);
@@ -680,13 +635,28 @@ exports.getRecord = async (table, filter_obj) => {
return await database.collection(table).findOne(filter_obj);
}
exports.getRecords = async (table, filter_obj = null) => {
exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => {
// local db override
if (using_local_db) {
return filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
let cursor = filter_obj ? 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));
}
if (range) {
cursor = cursor.slice(range[0], range[1]);
}
return !return_count ? cursor : cursor.length;
}
return filter_obj ? await database.collection(table).find(filter_obj).toArray() : await database.collection(table).find().toArray();
const cursor = filter_obj ? database.collection(table).find(filter_obj) : database.collection(table).find();
if (sort) {
cursor.sort({[sort['by']]: sort['order']});
}
if (range) {
cursor.skip(range[0]).limit(range[1] - range[0]);
}
return !return_count ? await cursor.toArray() : await cursor.count();
}
// Update
@@ -784,6 +754,66 @@ exports.removeRecord = async (table, filter_obj) => {
return !!(output['result']['ok']);
}
// exports.removeRecordsByUIDBulk = async (table, uids) => {
// // local db override
// if (using_local_db) {
// applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
// return true;
// }
// const table_collection = database.collection(table);
// let bulk = table_collection.initializeOrderedBulkOp(); // Initialize the Ordered Batch
// const item_ids_to_remove =
// for (let i = 0; i < item_ids_to_update.length; i++) {
// const item_id_to_update = item_ids_to_update[i];
// bulk.find({[key_label]: item_id_to_update }).updateOne({
// "$set": update_obj[item_id_to_update]
// });
// }
// const output = await bulk.execute();
// return !!(output['result']['ok']);
// }
exports.findDuplicatesByKey = async (table, key) => {
let duplicates = [];
if (using_local_db) {
// this can probably be optimized
const all_records = await exports.getRecords(table);
const existing_records = {};
for (let i = 0; i < all_records.length; i++) {
const record = all_records[i];
const value = record[key];
if (existing_records[value]) {
duplicates.push(record);
}
existing_records[value] = true;
}
return duplicates;
}
const duplicated_values = await database.collection(table).aggregate([
{"$group" : { "_id": `$${key}`, "count": { "$sum": 1 } } },
{"$match": {"_id" :{ "$ne" : null } , "count" : {"$gt": 1} } },
{"$project": {[key] : "$_id", "_id" : 0} }
]).toArray();
for (let i = 0; i < duplicated_values.length; i++) {
const duplicated_value = duplicated_values[i];
const duplicated_records = await exports.getRecords(table, duplicated_value, false);
if (duplicated_records.length > 1) {
duplicates = duplicates.concat(duplicated_records.slice(1, duplicated_records.length));
}
}
return duplicates;
}
exports.removeAllRecords = async (table = null, filter_obj = null) => {
// local db override
const tables_to_remove = table ? [table] : tables_list;
@@ -957,6 +987,52 @@ const createDownloadsRecords = (downloads) => {
return new_downloads;
}
exports.backupDB = async () => {
const backup_dir = path.join('appdata', 'db_backup');
fs.ensureDirSync(backup_dir);
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}`);
const table_to_records = {};
for (let i = 0; i < tables_list.length; i++) {
const table = tables_list[i];
table_to_records[table] = await exports.getRecords(table);
}
fs.writeJsonSync(path_to_backups, table_to_records);
return backup_file_name;
}
exports.restoreDB = async (file_name) => {
const path_to_backup = path.join('appdata', 'db_backup', file_name);
logger.debug('Reading database backup file.');
const table_to_records = fs.readJSONSync(path_to_backup);
if (!table_to_records) {
logger.error(`Failed to restore DB! Backup file '${path_to_backup}' could not be read.`);
return false;
}
logger.debug('Clearing database.');
await exports.removeAllRecords();
logger.debug('Database cleared! Beginning restore.');
let success = true;
for (let i = 0; i < tables_list.length; i++) {
const table = tables_list[i];
if (!table_to_records[table] || table_to_records[table].length === 0) continue;
success &= await exports.bulkInsertRecordsIntoTable(table, table_to_records[table]);
}
logger.debug('Restore finished!');
return success;
}
exports.transferDB = async (local_to_remote) => {
const table_to_records = {};
for (let i = 0; i < tables_list.length; i++) {
@@ -966,9 +1042,8 @@ exports.transferDB = async (local_to_remote) => {
using_local_db = !local_to_remote;
if (local_to_remote) {
// backup local DB
logger.debug('Backup up Local DB...');
await fs.copyFile('appdata/local_db.json', `appdata/local_db.json.${Date.now()/1000}.bak`);
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.');
@@ -1012,7 +1087,13 @@ const applyFilterLocalDB = (db_path, filter_obj, operation) => {
if (filter_prop_value === undefined || filter_prop_value === null) {
filtered &= record[filter_prop] === undefined || record[filter_prop] === null
} else {
filtered &= record[filter_prop] === filter_prop_value;
if (typeof filter_prop_value === 'object') {
if (filter_prop_value['$regex']) {
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
}
} else {
filtered &= record[filter_prop] === filter_prop_value;
}
}
}
return filtered;

630
backend/downloader.js Normal file
View File

@@ -0,0 +1,630 @@
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
const path = require('path');
const mergeFiles = require('merge-files');
const NodeID3 = require('node-id3')
const Mutex = require('async-mutex').Mutex;
const youtubedl = require('youtube-dl');
const logger = require('./logger');
const config_api = require('./config');
const twitch_api = require('./twitch');
const { create } = require('xmlbuilder2');
const categories_api = require('./categories');
const utils = require('./utils');
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 {
db_api.database_initialized_bs.subscribe(init => {
if (init) setupDownloads();
});
}
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
return await mutex.runExclusive(async () => {
const download = {
url: url,
type: type,
title: '',
user_uid: user_uid,
sub_id: sub_id,
sub_name: sub_name,
options: options,
uid: uuid(),
step_index: 0,
paused: false,
running: false,
finished_step: true,
error: null,
percent_complete: null,
finished: false,
timestamp_start: Date.now()
};
await db_api.insertRecordIntoTable('download_queue', download);
should_check_downloads = true;
return download;
});
}
exports.pauseDownload = async (download_uid) => {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (download['paused']) {
logger.warn(`Download ${download_uid} is already paused!`);
return false;
} else if (download['finished']) {
logger.info(`Download ${download_uid} could not be paused before completing.`);
return false;
}
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
}
exports.resumeDownload = async (download_uid) => {
return await mutex.runExclusive(async () => {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (!download['paused']) {
logger.warn(`Download ${download_uid} is not paused!`);
return false;
}
const success = db_api.updateRecord('download_queue', {uid: download_uid}, {paused: false});
should_check_downloads = true;
return success;
})
}
exports.restartDownload = async (download_uid) => {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
await exports.clearDownload(download_uid);
const success = !!(await exports.createDownload(download['url'], download['type'], download['options'], download['user_uid']));
should_check_downloads = true;
return success;
}
exports.cancelDownload = async (download_uid) => {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (download['cancelled']) {
logger.warn(`Download ${download_uid} is already cancelled!`);
return false;
} else if (download['finished']) {
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
return false;
}
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true, running: false});
}
exports.clearDownload = async (download_uid) => {
return await db_api.removeRecord('download_queue', {uid: download_uid});
}
async function handleDownloadError(download_uid, error_message) {
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error_message, finished: true, running: false});
}
async function setupDownloads() {
await fixDownloadState();
setInterval(checkDownloads, 1000);
}
async function fixDownloadState() {
const downloads = await db_api.getRecords('download_queue');
downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start);
const running_downloads = downloads.filter(download => !download['finished'] && !download['error']);
for (let i = 0; i < running_downloads.length; i++) {
const running_download = running_downloads[i];
const update_obj = {finished_step: true, paused: true, running: false};
if (running_download['step_index'] > 0) {
update_obj['step_index'] = running_download['step_index'] - 1;
}
await db_api.updateRecord('download_queue', {uid: running_download['uid']}, update_obj);
}
}
async function checkDownloads() {
if (!should_check_downloads) return;
const downloads = await db_api.getRecords('download_queue');
downloads.sort((download1, download2) => download1.timestamp_start - download2.timestamp_start);
await mutex.runExclusive(async () => {
// avoid checking downloads unnecessarily, but double check that should_check_downloads is still true
const running_downloads = downloads.filter(download => !download['paused'] && !download['finished']);
if (running_downloads.length === 0) {
should_check_downloads = false;
logger.verbose('Disabling checking downloads as none are available.');
}
return;
});
let running_downloads_count = downloads.filter(download => download['running']).length;
const waiting_downloads = downloads.filter(download => !download['paused'] && download['finished_step'] && !download['finished']);
for (let i = 0; i < waiting_downloads.length; i++) {
const waiting_download = waiting_downloads[i];
const max_concurrent_downloads = config_api.getConfigItem('ytdl_max_concurrent_downloads');
if (max_concurrent_downloads < 0 || running_downloads_count >= max_concurrent_downloads) break;
if (waiting_download['finished_step'] && !waiting_download['finished']) {
// move to next step
running_downloads_count++;
if (waiting_download['step_index'] === 0) {
collectInfo(waiting_download['uid']);
} else if (waiting_download['step_index'] === 1) {
downloadQueuedFile(waiting_download['uid']);
}
}
}
}
async function collectInfo(download_uid) {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (download['paused']) {
return;
}
logger.verbose(`Collecting info for download ${download_uid}`);
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 1, finished_step: false, running: true});
const url = download['url'];
const type = download['type'];
const options = download['options'];
if (download['user_uid'] && !options.customFileFolderPath) {
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const user_path = path.join(usersFileFolder, download['user_uid'], type);
options.customFileFolderPath = user_path + path.sep;
}
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);
if (!info) {
// info failed, error presumably already recorded
return;
}
let category = null;
// check if it fits into a category. If so, then get info again using new args
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
if (category && category['custom_output']) {
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);
}
// setup info required to calculate download progress
const expected_file_size = utils.getExpectedFileSize(info);
const files_to_check_for_progress = [];
// store info in download for future use
if (Array.isArray(info)) {
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
} else {
files_to_check_for_progress.push(utils.removeFileExtension(info['_filename']));
}
const playlist_title = Array.isArray(info) ? info[0]['playlist_title'] || info[0]['playlist'] : null;
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
finished_step: true,
running: false,
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']
});
}
async function downloadQueuedFile(download_uid) {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
if (download['paused']) {
return;
}
logger.verbose(`Downloading ${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');
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
const url = download['url'];
const type = download['type'];
const options = download['options'];
const args = download['args'];
const category = download['category'];
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath;
}
fs.ensureDirSync(fileFolderPath);
const start_time = Date.now();
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
// download file
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
const file_objs = [];
let end_time = Date.now();
let difference = (end_time - start_time)/1000;
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
clearInterval(download_checker);
if (err) {
logger.error(err.stderr);
await handleDownloadError(download_uid, err.stderr);
resolve(false);
return;
} else if (output) {
if (output.length === 0 || output[0].length === 0) {
// ERROR!
const error_message = `No output received for video download, check if it exists in your archive.`;
await handleDownloadError(download_uid, error_message);
logger.warn(error_message);
resolve(false);
return;
}
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
// get filepath with no extension
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
const ext = type === 'audio' ? '.mp3' : '.mp4';
var full_file_path = filepath_no_extension + ext;
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
let vodId = url.split('twitch.tv/videos/')[1];
vodId = vodId.split('?')[0];
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
}
// renames file if necessary due to bug
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
try {
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
} catch(e) {
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
}
}
if (type === 'audio') {
let tags = {
title: output_json['title'],
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
}
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
}
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
}
if (options.cropFileSettings) {
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
}
// registers file in DB
const file_obj = await db_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
file_objs.push(file_obj);
}
if (options.merged_string !== null && options.merged_string !== undefined) {
const archive_folder = getArchiveFolder(fileFolderPath, options, download['user_uid']);
const current_merged_archive = fs.readFileSync(path.join(archive_folder, `merged_${type}.txt`), 'utf8');
const diff = current_merged_archive.replace(options.merged_string, '');
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
fs.appendFileSync(archive_path, diff);
}
let container = null;
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']);
} else if (file_objs.length === 1) {
container = file_objs[0];
} else {
const error_message = 'Downloaded file failed to result in metadata object.';
logger.error(error_message);
await handleDownloadError(download_uid, error_message);
}
const file_uids = file_objs.map(file_obj => file_obj.uid);
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
resolve();
}
});
});
}
// helper functions
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_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;
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
const customArgs = options.customArgs;
let customOutput = options.customOutput;
const customQualityConfiguration = options.customQualityConfiguration;
// video-specific args
const selectedHeight = options.selectedHeight;
// audio-specific args
const maxBitrate = options.maxBitrate;
const youtubeUsername = options.youtubeUsername;
const youtubePassword = options.youtubePassword;
let downloadConfig = null;
let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4'];
const is_youtube = url.includes('youtu');
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) {
downloadConfig = customArgs.split(',,');
} else {
if (customQualityConfiguration) {
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (is_audio) {
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
}
if (customOutput) {
customOutput = options.noRelativePath ? customOutput : path.join(fileFolderPath, customOutput);
downloadConfig = ['-o', `${customOutput}.%(ext)s`, '--write-info-json', '--print-json'];
} else {
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
}
if (qualityPath) downloadConfig.push(...qualityPath);
if (is_audio && !options.skip_audio_args) {
downloadConfig.push('-x');
downloadConfig.push('--audio-format', 'mp3');
}
if (youtubeUsername && youtubePassword) {
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
}
if (useCookies) {
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
const useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
const customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
if (!useDefaultDownloadingAgent && customDownloadingAgent) {
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
}
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
const archive_folder = getArchiveFolder(fileFolderPath, options, user_uid);
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
await fs.ensureDir(archive_folder);
await fs.ensureFile(archive_path);
const blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
await fs.ensureFile(blacklist_path);
const merged_path = path.join(archive_folder, `merged_${type}.txt`);
await fs.ensureFile(merged_path);
// merges blacklist and regular archive
let inputPathList = [archive_path, blacklist_path];
await mergeFiles(inputPathList, merged_path);
options.merged_string = await fs.readFile(merged_path, "utf8");
downloadConfig.push('--download-archive', merged_path);
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
if (globalArgs && globalArgs !== '') {
// adds global args
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
// if global args has an output, replce the original output with that of global args
const original_output_index = downloadConfig.indexOf('-o');
downloadConfig.splice(original_output_index, 2);
}
downloadConfig = downloadConfig.concat(globalArgs.split(',,'));
}
if (options.additionalArgs && options.additionalArgs !== '') {
downloadConfig = utils.injectArgs(downloadConfig, options.additionalArgs.split(',,'));
}
const rate_limit = config_api.getConfigItem('ytdl_download_rate_limit');
if (rate_limit && downloadConfig.indexOf('-r') === -1 && downloadConfig.indexOf('--limit-rate') === -1) {
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');
}
}
// filter out incompatible args
downloadConfig = filterArgs(downloadConfig, is_audio);
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
return downloadConfig;
}
async function getVideoInfoByURL(url, args = [], download_uid = null) {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
new_args.splice(archiveArgIndex, 2);
}
new_args.push('--dump-json');
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
if (output) {
let outputs = [];
try {
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
outputs.push(output_json);
}
resolve(outputs.length === 1 ? outputs[0] : outputs);
} catch(e) {
const error = `Error while retrieving info on video with URL ${url} with the following message: output JSON could not be parsed. Output JSON: ${output}`;
logger.error(error);
if (download_uid) {
await handleDownloadError(download_uid, error);
}
resolve(null);
}
} else {
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
if (err.stderr) error_message += `\n\n${err.stderr}`;
logger.error(error_message);
if (download_uid) {
await handleDownloadError(download_uid, error_message);
}
resolve(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));
}
async function checkDownloadPercent(download_uid) {
/*
This is more of an art than a science, we're just selecting files that start with the file name,
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
be divided by the "total expected bytes."
*/
const download = await db_api.getRecord('download_queue', {uid: download_uid});
const files_to_check_for_progress = download['files_to_check_for_progress'];
const resulting_file_size = download['expected_file_size'];
if (!resulting_file_size) return;
let sum_size = 0;
for (let i = 0; i < files_to_check_for_progress.length; i++) {
const file_to_check_for_progress = files_to_check_for_progress[i];
const dir = path.dirname(file_to_check_for_progress);
if (!fs.existsSync(dir)) continue;
fs.readdir(dir, async (err, files) => {
for (let j = 0; j < files.length; j++) {
const file = files[j];
if (!file.includes(path.basename(file_to_check_for_progress))) continue;
try {
const file_stats = fs.statSync(path.join(dir, file));
if (file_stats && file_stats.size) {
sum_size += file_stats.size;
}
} catch (e) {}
}
const percent_complete = (sum_size/resulting_file_size * 100).toFixed(2);
await db_api.updateRecord('download_queue', {uid: download_uid}, {percent_complete: percent_complete});
});
}
}
exports.generateNFOFile = (info, output_path) => {
const nfo_obj = {
episodedetails: {
title: info['fulltitle'],
episode: info['playlist_index'] ? info['playlist_index'] : undefined,
premiered: utils.formatDateString(info['upload_date']),
plot: `${info['uploader_url']}\n${info['description']}\n${info['playlist_title'] ? info['playlist_title'] : ''}`,
director: info['artist'] ? info['artist'] : info['uploader']
}
};
const doc = create(nfo_obj);
const xml = doc.end({ prettyPrint: true });
fs.writeFileSync(output_path, xml);
}
function getArchiveFolder(fileFolderPath, options, user_uid) {
if (options.customArchivePath) {
return path.join(options.customArchivePath);
} else if (user_uid) {
return path.join(fileFolderPath, 'archives');
} else {
return path.join(archivePath);
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
apps : [{
name : "YoutubeDL-Material",
script : "./app.js",
watch : "placeholder",
watch_delay: 5000
}]
}

View File

@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
CMD="forever app.js"
CMD="pm2-runtime pm2.config.js"
# if the first arg starts with "-" pass it to program
if [ "${1#-}" != "$1" ]; then
@@ -11,7 +11,7 @@ fi
# chown current working directory to current user
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
exec su-exec "$UID:$GID" "$0" "$@"
exec gosu "$UID:$GID" "$0" "$@"
fi
exec "$@"

View File

@@ -0,0 +1,58 @@
#!/bin/sh
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
# Date: 2022-05-03
# 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
# User defines / Docker env defaults
PATH_SUBS=/app/subscriptions
PATH_AUDIO=/app/audio
PATH_VIDS=/app/video
clear -x
echo "\n"
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
echo "Welcome to the INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M."
echo "This script will set YTDL-M's download paths' owner to ${USER} (${UID}:${GID})"
echo "and permissions to the default of 644."
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
echo "\n"
# check whether dirs exist
i=0
[ -d $PATH_SUBS ] && i=$((i+1)) && echo "✔ (${i}/3) Found Subscriptions directory at ${PATH_SUBS}"
[ -d $PATH_AUDIO ] && i=$((i+1)) && echo "✔ (${i}/3) Found Audio directory at ${PATH_AUDIO}"
[ -d $PATH_VIDS ] && i=$((i+1)) && echo "✔ (${i}/3) Found Video directory at ${PATH_VIDS}"
# Ask to proceed or cancel, exit on missing paths
case $i in
0)
echo "\nCouldn't find any download path to fix permissions for! \nPlease edit this script to configure!"
exit 2;;
3)
echo "\nFound all download paths to fix permissions for. \nProceed? (Y/N)";;
*)
echo "\nOnly found ${i} out of 3 download paths! Something about this script's config must be wrong. \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
echo "\n Running jobs now... (this may take a while)\n"
[ -d $PATH_SUBS ] && chown "$UID:$GID" -R $PATH_SUBS && echo "✔ Set owner of ${PATH_SUBS} to ${USER}."
[ -d $PATH_SUBS ] && chmod 644 -R $PATH_SUBS && echo "✔ Set permissions of ${PATH_SUBS} to 644."
[ -d $PATH_AUDIO ] && chown "$UID:$GID" -R $PATH_AUDIO && echo "✔ Set owner of ${PATH_AUDIO} to ${USER}."
[ -d $PATH_AUDIO ] && chmod 644 -R $PATH_AUDIO && echo "✔ Set permissions of ${PATH_AUDIO} to 644."
[ -d $PATH_VIDS ] && chown "$UID:$GID" -R $PATH_VIDS && echo "✔ Set owner of ${PATH_VIDS} to ${USER}."
[ -d $PATH_VIDS ] && chmod 644 -R $PATH_VIDS && echo "✔ Set permissions of ${PATH_VIDS} to 644."
echo "\n✔ Done."
echo "\n If you noticed file access errors those MAY be due to currently running downloads."
echo " Feel free to re-run this script, however download parts should have correct file permissions anyhow. :)"
exit
else
echo "\nOkay, bye."
fi

23
backend/logger.js Normal file
View File

@@ -0,0 +1,23 @@
const winston = require('winston');
let debugMode = process.env.YTDL_MODE === 'debug';
const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => {
return `${timestamp} ${level.toUpperCase()}: ${message}`;
});
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), defaultFormat),
defaultMeta: {},
transports: [
//
// - Write to all logs with level `info` and below to `combined.log`
// - Write all logs error (and below) to `error.log`.
//
new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'appdata/logs/combined.log' }),
new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'})
]
});
module.exports = logger;

1310
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,40 +30,42 @@
},
"homepage": "",
"dependencies": {
"archiver": "^3.1.1",
"async": "^3.1.0",
"axios": "^0.21.1",
"archiver": "^5.3.1",
"async": "^3.2.3",
"async-mutex": "^0.3.1",
"axios": "^0.21.2",
"bcryptjs": "^2.4.0",
"compression": "^1.7.4",
"config": "^3.2.3",
"exe": "^1.0.2",
"express": "^4.17.1",
"express": "^4.17.3",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"jsonwebtoken": "^8.5.1",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"merge-files": "^0.1.2",
"mocha": "^8.4.0",
"moment": "^2.29.1",
"mocha": "^9.2.2",
"moment": "^2.29.2",
"mongodb": "^3.6.9",
"multer": "^1.4.2",
"node-fetch": "^2.6.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",
"passport-ldapauth": "^2.1.4",
"passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"ps-node": "^0.1.6",
"read-last-lines": "^1.7.2",
"rxjs": "^7.3.0",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"winston": "^3.2.1",
"winston": "^3.7.2",
"xmlbuilder2": "^3.0.2",
"youtube-dl": "^3.0.2"
}
}

9
backend/pm2.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
apps : [{
name : "YoutubeDL-Material",
script : "./app.js",
watch : "placeholder",
out_file: "/dev/null",
error_file: "/dev/null"
}]
}

View File

@@ -1,28 +1,15 @@
const FileSync = require('lowdb/adapters/FileSync')
const fs = require('fs-extra');
const path = require('path');
const youtubedl = require('youtube-dl');
var fs = require('fs-extra');
const { uuid } = require('uuidv4');
var path = require('path');
var youtubedl = require('youtube-dl');
const config_api = require('./config');
const twitch_api = require('./twitch');
var utils = require('./utils');
const utils = require('./utils');
const logger = require('./logger');
const debugMode = process.env.YTDL_MODE === 'debug';
var logger = null;
var db = null;
var users_db = null;
let db_api = null;
function setDB(input_db_api) { db_api = input_db_api }
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db_api, input_logger) {
setDB(input_db_api);
setLogger(input_logger);
}
const db_api = require('./db');
const downloader_api = require('./downloader');
async function subscribe(sub, user_uid = null) {
const result_obj = {
@@ -46,13 +33,13 @@ async function subscribe(sub, user_uid = null) {
sub['user_uid'] = user_uid ? user_uid : undefined;
await db_api.insertRecordIntoTable('subscriptions', sub);
let success = await getSubscriptionInfo(sub, user_uid);
let success = await getSubscriptionInfo(sub);
if (success) {
getVideosForSub(sub, user_uid);
} else {
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
};
}
result_obj.success = success;
result_obj.sub = sub;
@@ -61,13 +48,7 @@ async function subscribe(sub, user_uid = null) {
}
async function getSubscriptionInfo(sub, user_uid = null) {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
async function getSubscriptionInfo(sub) {
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
@@ -114,22 +95,6 @@ async function getSubscriptionInfo(sub, user_uid = null) {
}
}
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useArchive && !sub.archive) {
// must create the archive
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
const archive_path = path.join(archive_dir, 'archive.txt');
// creates archive directory and text file if it doesn't exist
fs.ensureDirSync(archive_dir);
fs.ensureFileSync(archive_path);
// updates subscription
sub.archive = archive_dir;
await db_api.updateRecord('subscriptions', {id: sub.id}, {archive: archive_dir});
}
// TODO: get even more info
resolve(true);
@@ -146,9 +111,23 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let result_obj = { success: false, error: '' };
let id = sub.id;
const sub_files = await db_api.getRecords('files', {sub_id: id});
for (let i = 0; i < sub_files.length; i++) {
const sub_file = sub_files[i];
if (config_api.descriptors[sub_file['uid']]) {
try {
for (let i = 0; i < config_api.descriptors[sub_file['uid']].length; i++) {
config_api.descriptors[sub_file['uid']][i].destroy();
}
} catch(e) {
continue;
}
}
}
await db_api.removeRecord('subscriptions', {id: id});
await db_api.removeAllRecords('files', {sub_id: id});
@@ -162,6 +141,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
if (sub.archive && (await fs.pathExists(sub.archive))) {
const archive_file_path = path.join(sub.archive, 'archive.txt');
// deletes archive if it exists
// TODO: Keep entries in blacklist_video.txt by moving them to a global blacklist
if (await fs.pathExists(archive_file_path)) {
await fs.unlink(archive_file_path);
}
@@ -249,30 +229,15 @@ async function getVideosForSub(sub, user_uid = null) {
let appendedBasePath = getAppendedBasePath(sub, basePath);
fs.ensureDirSync(appendedBasePath);
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
}
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
return new Promise(async resolve => {
const preimported_file_paths = [];
const PREIMPORT_INTERVAL = 5000;
const preregister_check = setInterval(async () => {
if (sub.streamingOnly) return;
await db_api.preimportUnregisteredSubscriptionFile(sub, appendedBasePath);
}, PREIMPORT_INTERVAL);
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
// cleanup
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
clearInterval(preregister_check);
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
@@ -280,19 +245,21 @@ async function getVideosForSub(sub, user_uid = null) {
if (err.stderr.includes('This video is unavailable')) {
logger.info('An error was encountered with at least one video, backup method will be used.')
try {
const outputs = err.stdout.split(/\r\n|\r|\n/);
for (let i = 0; i < outputs.length; i++) {
const output = JSON.parse(outputs[i]);
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']);
}
}
}
// 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']);
// }
// }
// }
} catch(e) {
logger.error('Backup method failed. See error below:');
logger.error(e);
@@ -300,41 +267,68 @@ async function getVideosForSub(sub, user_uid = null) {
}
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 reset_videos = i === 0;
await handleOutputJSON(sub, output_json, multiUserMode, preimported_file_paths, reset_videos);
}
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
await setFreshUploads(sub, user_uid);
checkVideosForFreshUploads(sub, user_uid);
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);
}
resolve(true);
}
resolve(files_to_download);
}
});
}, err => {
logger.error(err);
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
clearInterval(preregister_check);
});
}
function generateOptionsForSubscriptionDownload(sub, user_uid) {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let 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,
customFileFolderPath: getAppendedBasePath(sub, basePath),
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
additionalArgs: sub.custom_args
}
return base_download_options;
}
async function generateArgsForSubscription(sub, user_uid, redownload = false, desired_path = null) {
// get basePath
let basePath = null;
@@ -349,14 +343,14 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
const file_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
let fullOutput = `${appendedBasePath}/${file_output}.%(ext)s`;
let fullOutput = `"${appendedBasePath}/${file_output}.%(ext)s"`;
if (desired_path) {
fullOutput = `${desired_path}.%(ext)s`;
fullOutput = `"${desired_path}.%(ext)s"`;
} else if (sub.custom_output) {
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
fullOutput = `"${appendedBasePath}/${sub.custom_output}.%(ext)s"`;
}
let downloadConfig = ['-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
let downloadConfig = ['--dump-json', '-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
@@ -371,7 +365,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push(...qualityPath)
if (sub.custom_args) {
customArgsArray = sub.custom_args.split(',,');
const customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {
// if custom args has a custom quality, replce the original quality with that of custom args
const original_output_index = downloadConfig.indexOf('-f');
@@ -386,7 +380,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
if (useArchive && !redownload) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
if (sub.type && sub.type === 'audio') {
archive_path = path.join(archive_dir, 'merged_audio.txt');
} else {
archive_path = path.join(archive_dir, 'merged_video.txt');
}
}
downloadConfig.push('--download-archive', archive_path);
}
@@ -413,6 +411,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push('--write-thumbnail');
}
const rate_limit = config_api.getConfigItem('ytdl_download_rate_limit');
if (rate_limit && downloadConfig.indexOf('-r') === -1 && downloadConfig.indexOf('--limit-rate') === -1) {
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');
@@ -421,43 +424,24 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
return downloadConfig;
}
async function handleOutputJSON(sub, output_json, multiUserMode = null, reset_videos = false) {
// TODO: remove streaming only mode
if (false && sub.streamingOnly) {
if (reset_videos) {
sub_db.assign({videos: []}).write();
}
// remove unnecessary info
output_json.formats = null;
// add to db
sub_db.get('videos').push(output_json).write();
} else {
path_object = path.parse(output_json['_filename']);
const path_string = path.format(path_object);
const file_exists = await db_api.getRecord('files', {path: path_string, sub_id: sub.id});
if (file_exists) {
// TODO: fix issue where files of different paths due to custom path get downloaded multiple times
// file already exists in DB, return early to avoid reseting the download date
return;
}
await db_api.registerFileDB2(output_json['_filename'], sub.type, sub.user_uid, null, sub.id);
const url = output_json['webpage_url'];
if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
const file_name = path.basename(output_json['_filename']);
const id = file_name.substring(0, file_name.length-4);
let vodId = url.split('twitch.tv/videos/')[1];
vodId = vodId.split('?')[0];
twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub);
async function getFilesToDownload(sub, output_jsons) {
const files_to_download = [];
for (let i = 0; i < output_jsons.length; i++) {
const output_json = output_jsons[i];
const file_missing = !(await db_api.getRecord('files', {sub_id: sub.id, url: output_json['webpage_url']})) && !(await db_api.getRecord('download_queue', {sub_id: sub.id, url: output_json['webpage_url'], error: null, finished: false}));
if (file_missing) {
const file_with_path_exists = await db_api.getRecord('files', {sub_id: sub.id, path: output_json['_filename']});
if (file_with_path_exists) {
// or maybe just overwrite???
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
}
files_to_download.push(output_json);
}
}
return files_to_download;
}
async function getSubscriptions(user_uid = null) {
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
}
@@ -465,7 +449,7 @@ async function getSubscriptions(user_uid = null) {
async function getAllSubscriptions() {
const all_subs = await db_api.getRecords('subscriptions');
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
return all_subs.filter(sub => !!(sub.user_uid) === multiUserMode);
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
}
async function getSubscription(subID) {
@@ -476,7 +460,7 @@ async function getSubscriptionByName(subName, user_uid = null) {
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
}
async function updateSubscription(sub, user_uid = null) {
async function updateSubscription(sub) {
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
return true;
}
@@ -487,28 +471,30 @@ async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
});
}
async function updateSubscriptionProperty(sub, assignment_obj, user_uid = null) {
async function updateSubscriptionProperty(sub, assignment_obj) {
// TODO: combine with updateSubscription
await db_api.updateRecord('subscriptions', {id: sub.id}, assignment_obj);
return true;
}
async function setFreshUploads(sub, user_uid) {
async function setFreshUploads(sub) {
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub.videos.forEach(async video => {
if (current_date === video['upload_date'].replace(/-/g, '')) {
sub_files.forEach(async file => {
if (current_date === file['upload_date'].replace(/-/g, '')) {
// set upload as fresh
const video_uid = video['uid'];
await db_api.setVideoProperty(video_uid, {'fresh_upload': true}, user_uid, sub['id']);
const file_uid = file['uid'];
await db_api.setVideoProperty(file_uid, {'fresh_upload': true});
}
});
}
async function checkVideosForFreshUploads(sub, user_uid) {
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub.videos.forEach(async video => {
if (video['fresh_upload'] && current_date > video['upload_date'].replace(/-/g, '')) {
await checkVideoIfBetterExists(video, sub, user_uid)
sub_files.forEach(async file => {
if (file['fresh_upload'] && current_date > file['upload_date'].replace(/-/g, '')) {
await checkVideoIfBetterExists(file, sub, user_uid)
}
});
}
@@ -530,19 +516,18 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
} else if (output) {
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]}, user_uid, sub['id']);
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]});
}
});
}
}
});
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false}, user_uid, sub['id']);
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
}
// helper functions
function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
@@ -556,7 +541,6 @@ module.exports = {
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
setLogger : setLogger,
initialize : initialize,
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
}

195
backend/tasks.js Normal file
View File

@@ -0,0 +1,195 @@
const db_api = require('./db');
const youtubedl_api = require('./youtube-dl');
const fs = require('fs-extra');
const logger = require('./logger');
const scheduler = require('node-schedule');
const TASKS = {
backup_local_db: {
run: db_api.backupDB,
title: 'Backup DB',
job: null
},
missing_files_check: {
run: checkForMissingFiles,
confirm: deleteMissingFiles,
title: 'Missing files check',
job: null
},
missing_db_records: {
run: db_api.importUnregisteredFiles,
title: 'Import missing DB records',
job: null
},
duplicate_files_check: {
run: checkForDuplicateFiles,
confirm: removeDuplicates,
title: 'Find duplicate files in DB',
job: null
},
youtubedl_update_check: {
run: youtubedl_api.checkForYoutubeDLUpdate,
confirm: youtubedl_api.updateYoutubeDL,
title: 'Update youtube-dl',
job: null
}
}
function scheduleJob(task_key, schedule) {
// schedule has to be converted from our format to one node-schedule can consume
let converted_schedule = null;
if (schedule['type'] === 'timestamp') {
converted_schedule = new Date(schedule['data']['timestamp']);
} else if (schedule['type'] === 'recurring') {
const dayOfWeek = schedule['data']['dayOfWeek'] != null ? schedule['data']['dayOfWeek'] : null;
const hour = schedule['data']['hour'] != null ? schedule['data']['hour'] : null;
const minute = schedule['data']['minute'] != null ? schedule['data']['minute'] : null;
converted_schedule = new scheduler.RecurrenceRule(null, null, null, dayOfWeek, hour, minute);
} else {
logger.error(`Failed to schedule job '${task_key}' as the type '${schedule['type']}' is invalid.`)
return null;
}
return scheduler.scheduleJob(converted_schedule, async () => {
const task_state = await db_api.getRecord('tasks', {key: task_key});
if (task_state['running'] || task_state['confirming']) {
logger.verbose(`Skipping running task ${task_state['key']} as it is already in progress.`);
return;
}
// remove schedule if it's a one-time task
if (task_state['schedule']['type'] !== 'recurring') await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
// we're just "running" the task, any confirmation should be user-initiated
exports.executeRun(task_key);
});
}
if (db_api.database_initialized) {
exports.setupTasks();
} else {
db_api.database_initialized_bs.subscribe(init => {
if (init) exports.setupTasks();
});
}
exports.setupTasks = async () => {
const tasks_keys = Object.keys(TASKS);
for (let i = 0; i < tasks_keys.length; i++) {
const task_key = tasks_keys[i];
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
if (!task_in_db) {
// insert task metadata into table if missing
await db_api.insertRecordIntoTable('tasks', {
key: task_key,
title: TASKS[task_key]['title'],
last_ran: null,
last_confirmed: null,
running: false,
confirming: false,
data: null,
error: null,
schedule: null,
options: {}
});
} else {
// reset task if necessary
await db_api.updateRecord('tasks', {key: task_key}, {running: false, confirming: false});
// schedule task and save job
if (task_in_db['schedule']) {
// prevent timestamp schedules from being set to the past
if (task_in_db['schedule']['type'] === 'timestamp' && task_in_db['schedule']['data']['timestamp'] < Date.now()) {
await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
continue;
}
TASKS[task_key]['job'] = scheduleJob(task_key, task_in_db['schedule']);
}
}
}
}
exports.executeTask = async (task_key) => {
if (!TASKS[task_key]) {
logger.error(`Task ${task_key} does not exist!`);
return;
}
logger.verbose(`Executing task ${task_key}`);
await exports.executeRun(task_key);
if (!TASKS[task_key]['confirm']) return;
await exports.executeConfirm(task_key);
logger.verbose(`Finished executing ${task_key}`);
}
exports.executeRun = async (task_key) => {
logger.verbose(`Running task ${task_key}`);
// don't set running to true when backup up DB as it will be stick "running" if restored
if (task_key !== 'backup_local_db') await db_api.updateRecord('tasks', {key: task_key}, {running: true});
const data = await TASKS[task_key].run();
await db_api.updateRecord('tasks', {key: task_key}, {data: TASKS[task_key]['confirm'] ? data : null, last_ran: Date.now()/1000, running: false});
logger.verbose(`Finished running task ${task_key}`);
}
exports.executeConfirm = async (task_key) => {
logger.verbose(`Confirming task ${task_key}`);
if (!TASKS[task_key]['confirm']) {
return null;
}
await db_api.updateRecord('tasks', {key: task_key}, {confirming: true});
const task_obj = await db_api.getRecord('tasks', {key: task_key});
const data = task_obj['data'];
await TASKS[task_key].confirm(data);
await db_api.updateRecord('tasks', {key: task_key}, {confirming: false, last_confirmed: Date.now()/1000, data: null});
logger.verbose(`Finished confirming task ${task_key}`);
}
exports.updateTaskSchedule = async (task_key, schedule) => {
logger.verbose(`Updating schedule for task ${task_key}`);
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
if (TASKS[task_key]['job']) {
TASKS[task_key]['job'].cancel();
}
if (schedule) {
TASKS[task_key]['job'] = scheduleJob(task_key, schedule);
}
}
// missing files check
async function checkForMissingFiles() {
const missing_files = [];
const all_files = await db_api.getRecords('files');
for (let i = 0; i < all_files.length; i++) {
const file_to_check = all_files[i];
const file_exists = fs.existsSync(file_to_check['path']);
if (!file_exists) missing_files.push(file_to_check['uid']);
}
return {uids: missing_files};
}
async function deleteMissingFiles(data) {
const uids = data['uids'];
for (let i = 0; i < uids.length; i++) {
const uid = uids[i];
await db_api.removeRecord('files', {uid: uid});
}
}
// duplicate files check
async function checkForDuplicateFiles() {
const duplicate_files = await db_api.findDuplicatesByKey('files', 'path');
const duplicate_uids = duplicate_files.map(duplicate_file => duplicate_file['uid']);
if (duplicate_uids && duplicate_uids.length > 0) {
return {uids: duplicate_uids};
}
return {uids: []};
}
async function removeDuplicates(data) {
for (let i = 0; i < data['uids'].length; i++) {
await db_api.removeRecord('files', {uid: data['uids'][i]});
}
}
exports.TASKS = TASKS;

File diff suppressed because one or more lines are too long

View File

@@ -40,7 +40,7 @@ const subscriptions_api = require('../subscriptions');
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
db_api.initialize(db, users_db, logger);
db_api.initialize(db, users_db);
describe('Database', async function() {
@@ -70,6 +70,17 @@ describe('Database', async function() {
const success = await db_api.getRecord('test', {test: 'test'});
assert(success);
});
it('Restore db', async function() {
const db_stats = await db_api.getDBStats();
const file_name = await db_api.backupDB();
await db_api.restoreDB(file_name);
const new_db_stats = await db_api.getDBStats();
assert(JSON.stringify(db_stats), JSON.stringify(new_db_stats));
});
});
describe('Export', function() {
@@ -83,12 +94,37 @@ describe('Database', async function() {
await db_api.removeAllRecords('test');
});
it('Add and read record', async function() {
this.timeout(120000);
await db_api.insertRecordIntoTable('test', {test_add: 'test', test_undefined: undefined, test_null: undefined});
const added_record = await db_api.getRecord('test', {test_add: 'test', test_undefined: undefined, test_null: null});
assert(added_record['test_add'] === 'test');
await db_api.removeRecord('test', {test_add: 'test'});
});
it('Find duplicates by key', async function() {
const test_duplicates = [
{
test: 'testing',
key: '1'
},
{
test: 'testing',
key: '2'
},
{
test: 'testing_missing',
key: '3'
},
{
test: 'testing',
key: '4'
}
];
await db_api.insertRecordsIntoTable('test', test_duplicates);
const duplicates = await db_api.findDuplicatesByKey('test', 'test');
console.log(duplicates);
});
it('Update record', async function() {
await db_api.insertRecordIntoTable('test', {test_update: 'test'});
await db_api.updateRecord('test', {test_update: 'test'}, {added_field: true});
@@ -122,6 +158,7 @@ describe('Database', async function() {
});
it('Bulk add', async function() {
this.timeout(120000);
const NUM_RECORDS_TO_ADD = 2002; // max batch ops is 1000
const test_records = [];
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
@@ -286,5 +323,174 @@ describe('Multi User', async function() {
// assert(video_obj);
// });
// });
});
describe('Downloader', function() {
const downloader_api = require('../downloader');
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
const options = {
ui_uid: uuid(),
user: 'admin'
}
beforeEach(async function() {
await db_api.connectToDB();
await db_api.removeAllRecords('download_queue');
});
it('Get file info', async function() {
});
it('Download file', async function() {
this.timeout(300000);
const returned_download = await downloader_api.createDownload(url, 'video', options);
console.log(returned_download);
await utils.wait(20000);
});
it('Queue file', async function() {
this.timeout(300000);
const returned_download = await downloader_api.createDownload(url, 'video', options);
console.log(returned_download);
await utils.wait(20000);
});
it('Pause file', async function() {
});
it('Generate args', async function() {
const args = await downloader_api.generateArgs(url, 'video', options);
console.log(args);
});
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);
});
it('Generate kodi NFO file', async function() {
const nfo_file_path = './test/sample.nfo';
if (fs.existsSync(nfo_file_path)) {
fs.unlinkSync(nfo_file_path);
}
const sample_json = fs.readJSONSync('./test/sample.info.json');
downloader_api.generateNFOFile(sample_json, nfo_file_path);
assert(fs.existsSync(nfo_file_path), true);
fs.unlinkSync(nfo_file_path);
});
it('Inject args', async function() {
const original_args1 = ['--no-resize-buffer', '-o', '%(title)s', '--no-mtime'];
const new_args1 = ['--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
const updated_args1 = utils.injectArgs(original_args1, new_args1);
const expected_args1 = ['--no-resize-buffer', '--no-mtime', '--age-limit', '25', '--yes-playlist', '--abort-on-error', '-o', '%(id)s'];
assert(JSON.stringify(updated_args1), JSON.stringify(expected_args1));
const original_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3'];
const new_args2 = ['--add-metadata', '--embed-thumbnail', '--convert-thumbnails', 'jpg'];
const updated_args2 = utils.injectArgs(original_args2, new_args2);
const expected_args2 = ['-o', '%(title)s.%(ext)s', '--write-info-json', '--print-json', '--audio-quality', '0', '-x', '--audio-format', 'mp3', '--add-metadata', '--embed-thumbnail', '--convert_thumbnails', 'jpg'];
console.log(updated_args2);
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
});
});
describe('Tasks', function() {
const tasks_api = require('../tasks');
beforeEach(async function() {
await db_api.connectToDB();
await db_api.removeAllRecords('tasks');
const dummy_task = {
run: async () => { await utils.wait(500); return true; },
confirm: async () => { await utils.wait(500); return true; },
title: 'Dummy task',
job: null
};
tasks_api.TASKS['dummy_task'] = dummy_task;
await tasks_api.initialize();
});
it('Backup db', async function() {
const backups_original = await utils.recFindByExt('appdata', 'bak');
const original_length = backups_original.length;
await tasks_api.executeTask('backup_local_db');
const backups_new = await utils.recFindByExt('appdata', 'bak');
const new_length = backups_new.length;
assert(original_length, new_length-1);
});
it('Check for missing files', async function() {
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);
});
it('Check for duplicate files', async function() {
this.timeout(300000);
await db_api.removeAllRecords('files', {uid: 'test1'});
await db_api.removeAllRecords('files', {uid: 'test2'});
const test_duplicate_file1 = {uid: 'test1', path: 'test/missing_file.mp4'};
const test_duplicate_file2 = {uid: 'test2', path: 'test/missing_file.mp4'};
const test_duplicate_file3 = {uid: 'test3', path: 'test/missing_file.mp4'};
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');
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);
assert(duplicated_record_count == 1, true);
});
it('Import unregistered files', async function() {
this.timeout(300000);
// pre-test cleanup
await db_api.removeAllRecords('files', {title: 'Sample File'});
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
// copies in files
fs.copyFileSync('test/sample.info.json', 'video/sample.info.json');
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
await tasks_api.executeTask('missing_db_records');
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
assert(!!imported_file, true);
// post-test cleanup
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
});
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);
await tasks_api.updateTaskSchedule('dummy_task', null);
assert(!!tasks_api.TASKS['dummy_task']['job'], false);
});
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);
await utils.wait(2000);
const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(dummy_task_obj['data'], true);
});
});

View File

@@ -1,8 +1,13 @@
const fs = require('fs-extra')
const path = require('path')
const config_api = require('./config');
const CONSTS = require('./consts')
const fs = require('fs-extra');
const path = require('path');
const ffmpeg = require('fluent-ffmpeg');
const archiver = require('archiver');
const fetch = require('node-fetch');
const ProgressBar = require('progress');
const config_api = require('./config');
const logger = require('./logger');
const CONSTS = require('./consts');
const is_windows = process.platform === 'win32';
@@ -43,8 +48,7 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
files.push(jsonobj);
continue;
}
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
var upload_date = formatDateString(jsonobj.upload_date);
var isaudio = type === 'audio';
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
@@ -54,13 +58,13 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
return files;
}
async function createContainerZipFile(container_obj, container_file_objs) {
async function createContainerZipFile(file_name, container_file_objs) {
const container_files_to_download = [];
for (let i = 0; i < container_file_objs.length; i++) {
const container_file_obj = container_file_objs[i];
container_files_to_download.push(container_file_obj.path);
}
return await createZipFile(path.join('appdata', container_obj.name + '.zip'), container_files_to_download);
return await createZipFile(path.join('appdata', file_name + '.zip'), container_files_to_download);
}
async function createZipFile(zip_file_path, file_paths) {
@@ -142,24 +146,7 @@ function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
}
function getDownloadedThumbnail(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
let jpgPath = path.join(customPath, name + '.jpg');
let webpPath = path.join(customPath, name + '.webp');
let pngPath = path.join(customPath, name + '.png');
if (fs.existsSync(jpgPath))
return jpgPath;
else if (fs.existsSync(webpPath))
return webpPath;
else if (fs.existsSync(pngPath))
return pngPath;
else
return null;
}
function getDownloadedThumbnail2(file_path, type) {
function getDownloadedThumbnail(file_path) {
const file_path_no_extension = removeFileExtension(file_path);
let jpgPath = file_path_no_extension + '.jpg';
@@ -182,10 +169,6 @@ function getExpectedFileSize(input_info_jsons) {
let expected_filesize = 0;
info_jsons.forEach(info_json => {
if (info_json['filesize']) {
expected_filesize += info_json['filesize'];
return;
}
const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0;
formats.forEach(format_id => {
@@ -201,29 +184,7 @@ function getExpectedFileSize(input_info_jsons) {
return expected_filesize;
}
function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
const files_to_fix = [
// JSONs
path.join(customPath, name + '.info.json'),
path.join(customPath, name + ext + '.info.json'),
// Thumbnails
path.join(customPath, name + '.webp'),
path.join(customPath, name + '.jpg')
];
for (const file of files_to_fix) {
if (!fs.existsSync(file)) continue;
fs.chmodSync(file, 0o644);
}
}
function fixVideoMetadataPerms2(file_path, type) {
function fixVideoMetadataPerms(file_path, type) {
if (is_windows) return;
const ext = type === 'audio' ? '.mp3' : '.mp4';
@@ -245,19 +206,7 @@ function fixVideoMetadataPerms2(file_path, type) {
}
}
function deleteJSONFile(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
let json_path = path.join(customPath, name + '.info.json');
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
function deleteJSONFile2(file_path, type) {
function deleteJSONFile(file_path, type) {
const ext = type === 'audio' ? '.mp3' : '.mp4';
const file_path_no_extension = removeFileExtension(file_path);
@@ -293,7 +242,6 @@ async function removeIDFromArchive(archive_path, id) {
const updatedData = dataArray.join('\n');
await fs.writeFile(archive_path, updatedData);
if (line) return line;
if (err) throw err;
}
function durationStringToNumber(dur_str) {
@@ -321,7 +269,7 @@ function getCurrentDownloader() {
return details_json['downloader'];
}
async function recFindByExt(base,ext,files,result)
async function recFindByExt(base, ext, files, result, recursive = true)
{
files = files || (await fs.readdir(base))
result = result || []
@@ -330,6 +278,7 @@ async function recFindByExt(base,ext,files,result)
var newbase = path.join(base,file)
if ( (await fs.stat(newbase)).isDirectory() )
{
if (!recursive) continue;
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
}
else
@@ -349,6 +298,57 @@ function removeFileExtension(filename) {
return filename_parts.join('.');
}
function formatDateString(date_string) {
return date_string ? `${date_string.substring(0, 4)}-${date_string.substring(4, 6)}-${date_string.substring(6, 8)}` : 'N/A';
}
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) {
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
ngrams = [...ngrams, token.substr(0, i)]
}
} else {
ngrams = [...ngrams, token]
}
return ngrams
}, []).join(" ")
}
return str
}
// ffmpeg helper functions
async function cropFile(file_path, start, end, ext) {
return new Promise(resolve => {
const temp_file_path = `${file_path}.cropped${ext}`;
let base_ffmpeg_call = ffmpeg(file_path);
if (start) {
base_ffmpeg_call = base_ffmpeg_call.seekOutput(start);
}
if (end) {
base_ffmpeg_call = base_ffmpeg_call.duration(end - start);
}
base_ffmpeg_call
.on('end', () => {
logger.verbose(`Cropping for '${file_path}' complete.`);
fs.unlinkSync(file_path);
fs.moveSync(temp_file_path, file_path);
resolve(true);
})
.on('error', (err) => {
logger.error(`Failed to crop ${file_path}.`);
logger.error(err);
resolve(false);
}).save(temp_file_path);
});
}
/**
* setTimeout, but its a promise.
* @param {number} ms
@@ -359,6 +359,95 @@ function removeFileExtension(filename) {
});
}
async function checkExistsWithTimeout(filePath, timeout) {
return new Promise(function (resolve, reject) {
var timer = setTimeout(function () {
if (watcher) watcher.close();
reject(new Error('File did not exists and was not created during the timeout.'));
}, timeout);
fs.access(filePath, fs.constants.R_OK, function (err) {
if (!err) {
clearTimeout(timer);
if (watcher) watcher.close();
resolve();
}
});
var dir = path.dirname(filePath);
var basename = path.basename(filePath);
var watcher = fs.watch(dir, function (eventType, filename) {
if (eventType === 'rename' && filename === basename) {
clearTimeout(timer);
if (watcher) watcher.close();
resolve();
}
});
});
}
// helper function to download file using fetch
async function fetchFile(url, path, file_label) {
var len = null;
const res = await fetch(url);
len = parseInt(res.headers.get("Content-Length"), 10);
var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
complete: '=',
incomplete: ' ',
width: 20,
total: len
});
const fileStream = fs.createWriteStream(path);
await new Promise((resolve, reject) => {
res.body.pipe(fileStream);
res.body.on("error", (err) => {
reject(err);
});
res.body.on('data', function (chunk) {
bar.tick(chunk.length);
});
fileStream.on("finish", function() {
resolve();
});
});
}
// 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
// - if it doesn't exist and has value, add both arg and value
// - if it doesn't exist and doesn't have value, add arg
function injectArgs(original_args, new_args) {
const updated_args = original_args.slice();
try {
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);
original_args.splice(original_index, 2);
}
updated_args.push(new_arg, new_args[i + 1]);
} else {
if (!original_args.includes(new_arg)) {
updated_args.push(new_arg);
}
}
}
} catch (err) {
logger.warn(err);
logger.warn(`Failed to inject args (${new_args}) into (${original_args})`);
}
return updated_args;
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
@@ -384,13 +473,10 @@ module.exports = {
getJSON: getJSON,
getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getDownloadedThumbnail2: getDownloadedThumbnail2,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms,
fixVideoMetadataPerms2: fixVideoMetadataPerms2,
deleteJSONFile: deleteJSONFile,
deleteJSONFile2: deleteJSONFile2,
removeIDFromArchive, removeIDFromArchive,
removeIDFromArchive: removeIDFromArchive,
getDownloadedFilesByType: getDownloadedFilesByType,
createContainerZipFile: createContainerZipFile,
durationStringToNumber: durationStringToNumber,
@@ -399,6 +485,12 @@ module.exports = {
getCurrentDownloader: getCurrentDownloader,
recFindByExt: recFindByExt,
removeFileExtension: removeFileExtension,
formatDateString: formatDateString,
cropFile: cropFile,
createEdgeNGrams: createEdgeNGrams,
wait: wait,
checkExistsWithTimeout: checkExistsWithTimeout,
fetchFile: fetchFile,
injectArgs: injectArgs,
File: File
}

127
backend/youtube-dl.js Normal file
View File

@@ -0,0 +1,127 @@
const fs = require('fs-extra');
const fetch = require('node-fetch');
const logger = require('./logger');
const utils = require('./utils');
const CONSTS = require('./consts');
const config_api = require('./config.js');
const is_windows = process.platform === 'win32';
const download_sources = {
'youtube-dl': {
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
'func': downloadLatestYoutubeDLBinary
},
'youtube-dlc': {
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
'func': downloadLatestYoutubeDLCBinary
},
'yt-dlp': {
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
'func': downloadLatestYoutubeDLPBinary
}
}
exports.checkForYoutubeDLUpdate = async () => {
return new Promise(async resolve => {
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
const tags_url = download_sources[default_downloader]['tags_url'];
// get current version
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});
}
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
let current_version = current_app_details['version'];
let current_downloader = current_app_details['downloader'];
let stored_binary_path = current_app_details['path'];
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${CONSTS.DETAILS_BIN_PATH}, attempting to guess actual path...`);
const guessed_base_path = 'node_modules/youtube-dl/bin/';
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
if (fs.existsSync(guessed_file_path)) {
stored_binary_path = guessed_file_path;
// logger.info('INFO: Guess successful! Update process continuing...')
} else {
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
resolve(null);
return;
}
}
// got version, now let's check the latest version from the youtube-dl API
fetch(tags_url, {method: 'Get'})
.then(async res => res.json())
.then(async (json) => {
// check if the versions are different
if (!json || !json[0]) {
logger.error(`Failed to check ${default_downloader} version for an update.`)
resolve(null);
return;
}
const latest_update_version = json[0]['name'];
if (current_version !== latest_update_version || default_downloader !== current_downloader) {
// versions different or different downloader is being used, download new update
resolve(latest_update_version);
} else {
resolve(null);
}
return;
})
.catch(err => {
logger.error(`Failed to check ${default_downloader} version for an update.`)
logger.error(err);
resolve(null);
return;
});
});
}
exports.updateYoutubeDL = async (latest_update_version) => {
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
await download_sources[default_downloader]['func'](latest_update_version);
}
async function downloadLatestYoutubeDLBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
await utils.fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
updateDetailsJSON(new_version, 'youtube-dl');
}
async function downloadLatestYoutubeDLCBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
updateDetailsJSON(new_version, 'youtube-dlc');
}
async function downloadLatestYoutubeDLPBinary(new_version) {
const file_ext = is_windows ? '.exe' : '';
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
updateDetailsJSON(new_version, 'yt-dlp');
}
function updateDetailsJSON(new_version, downloader) {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
if (new_version) details_json['version'] = new_version;
details_json['downloader'] = downloader;
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
}

43
docker-build.sh Normal file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
# THANK YOU TALULAH (https://github.com/nottalulah) for your help in figuring this out
# and also optimizing some code with this commit.
# xoxo :D
case $(uname -m) in
x86_64)
ARCH=amd64;;
aarch64)
ARCH=arm64;;
armhf)
ARCH=armhf;;
armv7)
ARCH=armel;;
armv7l)
ARCH=armel;;
*)
echo "Unsupported architecture: $(uname -m)"
exit 1
esac
echo "(INFO) Architecture detected: $ARCH"
echo "(1/5) READY - Acquire temp dependencies in ffmpeg obtain layer"
apt-get update && apt-get -y install curl xz-utils
echo "(2/5) DOWNLOAD - Acquire latest ffmpeg and ffprobe from John van Sickle's master-sourced builds in ffmpeg obtain layer"
curl -o ffmpeg.txz \
--connect-timeout 5 \
--max-time 10 \
--retry 5 \
--retry-delay 0 \
--retry-max-time 40 \
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz"
mkdir /tmp/ffmpeg
tar xf ffmpeg.txz -C /tmp/ffmpeg
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
apt-get -y remove curl xz-utils
apt-get -y autoremove
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

View File

@@ -7,6 +7,8 @@ services:
ytdl_use_local_db: 'false'
write_ytdl_config: 'true'
restart: always
depends_on:
- ytdl-mongo-db
volumes:
- ./appdata:/app/appdata
- ./audio:/app/audio
@@ -15,7 +17,7 @@ services:
- ./users:/app/users
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:latest
image: tzahi12345/youtubedl-material:nightly
ytdl-mongo-db:
image: mongo
ports:
@@ -23,5 +25,6 @@ services:
logging:
driver: "none"
container_name: mongo-db
restart: always
volumes:
- ./db/:/data/db

13373
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,15 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build": "ng build --configuration production",
"prebuild": "node src/postbuild.mjs",
"heroku-postbuild": "npm install --prefix backend",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"electron": "ng build --base-href ./ && electron ."
"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"
},
"engines": {
"node": "12.3.1",
@@ -18,55 +21,62 @@
},
"private": true,
"dependencies": {
"@angular-devkit/core": "^11.0.4",
"@angular/animations": "^11.0.4",
"@angular/cdk": "^11.0.2",
"@angular/common": "^11.0.4",
"@angular/compiler": "^11.0.4",
"@angular/core": "^11.0.4",
"@angular/forms": "^11.0.4",
"@angular/localize": "^11.0.4",
"@angular/material": "^11.0.2",
"@angular/platform-browser": "^11.0.4",
"@angular/platform-browser-dynamic": "^11.0.4",
"@angular/router": "^11.0.4",
"@angular-devkit/core": "^13.3.3",
"@angular/animations": "^13.3.4",
"@angular/cdk": "^13.3.4",
"@angular/common": "^13.3.4",
"@angular/compiler": "^13.3.4",
"@angular/core": "^13.3.4",
"@angular/forms": "^13.3.4",
"@angular/localize": "^13.3.4",
"@angular/material": "^13.3.4",
"@angular/platform-browser": "^13.3.4",
"@angular/platform-browser-dynamic": "^13.3.4",
"@angular/router": "^13.3.4",
"@fontsource/material-icons": "^4.5.4",
"@ngneat/content-loader": "^5.0.0",
"@videogular/ngx-videogular": "^2.1.0",
"@videogular/ngx-videogular": "^5.0.1",
"core-js": "^2.4.1",
"crypto-js": "^4.1.1",
"file-saver": "^2.0.2",
"filesize": "^6.1.0",
"fingerprintjs2": "^2.1.0",
"material-icons": "^0.5.4",
"fs-extra": "^10.0.0",
"material-icons": "^1.10.8",
"nan": "^2.14.1",
"ng-lazyload-image": "^7.0.1",
"ngx-avatar": "^4.0.0",
"ngx-file-drop": "^9.0.1",
"ngx-avatars": "^1.3.1",
"ngx-file-drop": "^13.0.0",
"rxjs": "^6.6.3",
"rxjs-compat": "^6.0.0-rc.0",
"tslib": "^2.0.0",
"typescript": "~4.0.5",
"web-animations-js": "^2.3.2",
"zone.js": "~0.10.2"
"typescript": "~4.6.3",
"xliff-to-json": "^1.0.4",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.1100.4",
"@angular/cli": "^11.0.4",
"@angular/compiler-cli": "^11.0.4",
"@angular/language-service": "^11.0.4",
"@angular-devkit/build-angular": "^13.3.3",
"@angular/cli": "^13.3.3",
"@angular/compiler-cli": "^13.3.4",
"@angular/language-service": "^13.3.4",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"codelyzer": "^6.0.0",
"electron": "^8.0.1",
"electron": "^13.6.6",
"eslint": "^7.32.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma": "~6.3.16",
"karma-chrome-launcher": "~3.1.0",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"openapi-typescript-codegen": "^0.21.0",
"protractor": "~7.0.0",
"ts-node": "~3.0.4",
"tslint": "~6.1.0"

View File

@@ -1,11 +1,11 @@
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
/* HSL */
$color1: hsla(351%, 56%, 80%, 1);
$softblue: hsla(205%, 100%, 86%, 1);
$color3: hsla(174%, 100%, 83%, 1);
$color4: hsla(133%, 93%, 78%, 1);
$color5: hsla(165%, 20%, 59%, 1);
$color1: hsla(351, 56%, 80%, 1);
$softblue: hsla(205, 100%, 86%, 1);
$color3: hsla(174, 100%, 83%, 1);
$color4: hsla(133, 93%, 78%, 1);
$color5: hsla(165, 20%, 59%, 1);
/* RGB */
$color1: rgba(232, 174, 183, 1);

111
src/api-types/index.ts Normal file
View File

@@ -0,0 +1,111 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest';
export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest';
export type { body_19 } from './models/body_19';
export type { body_20 } from './models/body_20';
export type { Category } from './models/Category';
export { CategoryRule } from './models/CategoryRule';
export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermissionsRequest';
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
export type { ConcurrentStream } from './models/ConcurrentStream';
export type { Config } from './models/Config';
export type { ConfigResponse } from './models/ConfigResponse';
export type { CreateCategoryRequest } from './models/CreateCategoryRequest';
export type { CreateCategoryResponse } from './models/CreateCategoryResponse';
export type { CreatePlaylistRequest } from './models/CreatePlaylistRequest';
export type { CreatePlaylistResponse } from './models/CreatePlaylistResponse';
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 { DeleteCategoryRequest } from './models/DeleteCategoryRequest';
export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request';
export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest';
export type { DeleteSubscriptionFileRequest } from './models/DeleteSubscriptionFileRequest';
export type { DeleteUserRequest } from './models/DeleteUserRequest';
export type { Download } from './models/Download';
export type { DownloadArchiveRequest } from './models/DownloadArchiveRequest';
export type { DownloadFileRequest } from './models/DownloadFileRequest';
export type { DownloadRequest } from './models/DownloadRequest';
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 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 { GetAllFilesResponse } from './models/GetAllFilesResponse';
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
export type { GetDBBackupsResponse } from './models/GetDBBackupsResponse';
export type { GetDownloadRequest } from './models/GetDownloadRequest';
export type { GetDownloadResponse } from './models/GetDownloadResponse';
export type { GetFileFormatsRequest } from './models/GetFileFormatsRequest';
export type { GetFileFormatsResponse } from './models/GetFileFormatsResponse';
export type { GetFileRequest } from './models/GetFileRequest';
export type { GetFileResponse } from './models/GetFileResponse';
export type { GetFullTwitchChatRequest } from './models/GetFullTwitchChatRequest';
export type { GetFullTwitchChatResponse } from './models/GetFullTwitchChatResponse';
export type { GetLogsRequest } from './models/GetLogsRequest';
export type { GetLogsResponse } from './models/GetLogsResponse';
export type { GetMp3sResponse } from './models/GetMp3sResponse';
export type { GetMp4sResponse } from './models/GetMp4sResponse';
export type { GetPlaylistRequest } from './models/GetPlaylistRequest';
export type { GetPlaylistResponse } from './models/GetPlaylistResponse';
export type { GetPlaylistsRequest } from './models/GetPlaylistsRequest';
export type { GetPlaylistsResponse } from './models/GetPlaylistsResponse';
export type { GetRolesResponse } from './models/GetRolesResponse';
export type { GetSubscriptionRequest } from './models/GetSubscriptionRequest';
export type { GetSubscriptionResponse } from './models/GetSubscriptionResponse';
export type { GetTaskRequest } from './models/GetTaskRequest';
export type { GetTaskResponse } from './models/GetTaskResponse';
export type { GetUsersResponse } from './models/GetUsersResponse';
export type { IncrementViewCountRequest } from './models/IncrementViewCountRequest';
export type { inline_response_200_15 } from './models/inline_response_200_15';
export type { LoginRequest } from './models/LoginRequest';
export type { LoginResponse } from './models/LoginResponse';
export type { Playlist } from './models/Playlist';
export type { RegisterRequest } from './models/RegisterRequest';
export type { RegisterResponse } from './models/RegisterResponse';
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 { SubscribeRequest } from './models/SubscribeRequest';
export type { SubscribeResponse } from './models/SubscribeResponse';
export type { Subscription } from './models/Subscription';
export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
export type { SuccessObject } from './models/SuccessObject';
export type { TableInfo } from './models/TableInfo';
export type { Task } from './models/Task';
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
export type { TransferDBRequest } from './models/TransferDBRequest';
export type { TransferDBResponse } from './models/TransferDBResponse';
export type { TwitchChatMessage } from './models/TwitchChatMessage';
export type { UnsubscribeRequest } from './models/UnsubscribeRequest';
export type { UnsubscribeResponse } from './models/UnsubscribeResponse';
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 { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
export type { UpdaterStatus } from './models/UpdaterStatus';
export type { UpdateServerRequest } from './models/UpdateServerRequest';
export type { UpdateTaskDataRequest } from './models/UpdateTaskDataRequest';
export type { UpdateTaskScheduleRequest } from './models/UpdateTaskScheduleRequest';
export type { UpdateUserRequest } from './models/UpdateUserRequest';
export type { User } from './models/User';
export { UserPermission } from './models/UserPermission';
export type { Version } from './models/Version';
export type { VersionInfoResponse } from './models/VersionInfoResponse';
export { YesNo } from './models/YesNo';

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface AddFileToPlaylistRequest {
file_uid: string;
playlist_id: string;
}

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { UserPermission } from './UserPermission';
import { YesNo } from './YesNo';
export interface BaseChangePermissionsRequest {
permission: UserPermission;
new_value: YesNo;
}

View File

@@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { CategoryRule } from './CategoryRule';
export interface Category {
name?: string;
uid?: string;
rules?: Array<CategoryRule>;
/**
* Overrides file output for downloaded files in category
*/
custom_output?: string;
}

View File

@@ -0,0 +1,26 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface CategoryRule {
preceding_operator?: CategoryRule.preceding_operator;
comparator?: CategoryRule.comparator;
}
export namespace CategoryRule {
export enum preceding_operator {
OR = 'or',
AND = 'and',
}
export enum comparator {
INCLUDES = 'includes',
NOT_INCLUDES = 'not_includes',
EQUALS = 'equals',
NOT_EQUALS = 'not_equals',
}
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
export interface ChangeRolePermissionsRequest extends BaseChangePermissionsRequest {
role: string;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
export interface ChangeUserPermissionsRequest extends BaseChangePermissionsRequest {
user_uid: string;
}

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface CheckConcurrentStreamRequest {
/**
* UID of the concurrent stream
*/
uid: string;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { ConcurrentStream } from './ConcurrentStream';
export interface CheckConcurrentStreamResponse {
stream: ConcurrentStream;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface ConcurrentStream {
playback_timestamp?: number;
unix_timestamp?: number;
playing?: boolean;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface Config {
YoutubeDLMaterial: any;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Config } from './Config';
export interface ConfigResponse {
config_file: Config;
success: boolean;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface CreateCategoryRequest {
name: string;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Category } from './Category';
export interface CreateCategoryResponse {
new_category?: Category;
success?: boolean;
}

View File

@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface CreatePlaylistRequest {
playlistName: string;
uids: Array<string>;
type: FileType;
thumbnailURL: string;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Playlist } from './Playlist';
export interface CreatePlaylistResponse {
new_playlist: Playlist;
success: boolean;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface CropFileSettings {
cropFileStart: number;
cropFileEnd: number;
}

View File

@@ -0,0 +1,21 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DBBackup {
name: string;
timestamp: number;
size: number;
source: DBBackup.source;
}
export namespace DBBackup {
export enum source {
LOCAL = 'local',
REMOTE = 'remote',
}
}

View File

@@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { TableInfo } from './TableInfo';
export interface DBInfoResponse {
using_local_db?: boolean;
stats_by_table?: {
files?: TableInfo,
playlists?: TableInfo,
categories?: TableInfo,
subscriptions?: TableInfo,
users?: TableInfo,
roles?: TableInfo,
download_queue?: TableInfo,
};
}

View File

@@ -0,0 +1,22 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DatabaseFile {
id: string;
title: string;
thumbnailURL: string;
isAudio: boolean;
/**
* In seconds
*/
duration: number;
url: string;
uploader: string;
size: number;
path: string;
upload_date: string;
uid: string;
sharingEnabled?: boolean;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DeleteCategoryRequest {
category_uid: string;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DeleteMp3Mp4Request {
uid: string;
blacklistMode?: boolean;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface DeletePlaylistRequest {
playlist_id: string;
type: FileType;
}

View File

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

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DeleteUserRequest {
uid: string;
}

View File

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

View File

@@ -0,0 +1,26 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface Download {
uid: string;
ui_uid?: string;
running: boolean;
finished: boolean;
paused: boolean;
finished_step: boolean;
url: string;
type: string;
title: string;
step_index: number;
percent_complete: number;
timestamp_start: number;
/**
* Error text, set if download fails.
*/
error?: string | null;
user_uid?: string;
sub_id?: string;
sub_name?: string;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DownloadArchiveRequest {
sub: {
archive_dir: string,
};
}

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface DownloadFileRequest {
uid?: string;
uuid?: string;
sub_id?: string;
playlist_id?: string;
url?: string;
type?: FileType;
}

View File

@@ -0,0 +1,44 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { CropFileSettings } from './CropFileSettings';
import { FileType } from './FileType';
export interface DownloadRequest {
url: string;
/**
* Video format code. Overrides other quality options.
*/
customQualityConfiguration?: string;
/**
* Custom command-line arguments for youtube-dl. Overrides all other options, except url.
*/
customArgs?: string;
/**
* Additional command-line arguments for youtube-dl. Added to whatever args would normally be used.
*/
additionalArgs?: string;
/**
* Custom output filename template.
*/
customOutput?: string;
/**
* Login with this account ID
*/
youtubeUsername?: string;
/**
* Account password
*/
youtubePassword?: string;
/**
* Height of the video, if known
*/
selectedHeight?: string;
/**
* Specify ffmpeg/avconv audio quality
*/
maxBitrate?: string;
type?: FileType;
cropFileSettings?: CropFileSettings;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Download } from './Download';
export interface DownloadResponse {
download?: Download;
}

View File

@@ -0,0 +1,23 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import { Subscription } from './Subscription';
export interface DownloadTwitchChatByVODIDRequest {
/**
* File ID
*/
id: string;
/**
* ID of the VOD
*/
vodId: string;
type: FileType;
/**
* User UID
*/
uuid?: string;
sub?: Subscription;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { TwitchChatMessage } from './TwitchChatMessage';
export interface DownloadTwitchChatByVODIDResponse {
chat: Array<TwitchChatMessage>;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface DownloadVideosForSubscriptionRequest {
subID: string;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface File {
id?: string;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export enum FileType {
AUDIO = 'audio',
VIDEO = 'video',
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GenerateArgsResponse {
args?: Array<string>;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GenerateNewApiKeyResponse {
new_api_key: string;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Category } from './Category';
export interface GetAllCategoriesResponse {
categories: Array<Category>;
}

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GetAllDownloadsRequest {
/**
* Filters downloads with the array
*/
uids?: Array<string> | null;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Download } from './Download';
export interface GetAllDownloadsResponse {
downloads?: Array<Download>;
}

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import { Playlist } from './Playlist';
export interface GetAllFilesResponse {
files: Array<DatabaseFile>;
/**
* All video playlists
*/
playlists: Array<Playlist>;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Subscription } from './Subscription';
export interface GetAllSubscriptionsResponse {
subscriptions: Array<Subscription>;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Task } from './Task';
export interface GetAllTasksResponse {
tasks?: Array<Task>;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { DBBackup } from './DBBackup';
export interface GetDBBackupsResponse {
tasks?: Array<DBBackup>;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GetDownloadRequest {
download_uid: string;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Download } from './Download';
export interface GetDownloadResponse {
download?: Download;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GetFileFormatsRequest {
url?: string;
}

View File

@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { File } from './File';
export interface GetFileFormatsResponse {
success: boolean;
result: {
formats?: Array<any>,
};
}

View File

@@ -0,0 +1,17 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface GetFileRequest {
/**
* Video UID
*/
uid: string;
type?: FileType;
/**
* User UID
*/
uuid?: string;
}

View File

@@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
export interface GetFileResponse {
success: boolean;
file?: DatabaseFile;
}

View File

@@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import { Subscription } from './Subscription';
export interface GetFullTwitchChatRequest {
/**
* File ID
*/
id: string;
type: FileType;
/**
* User UID
*/
uuid?: string;
sub?: Subscription;
}

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GetFullTwitchChatResponse {
success: boolean;
error?: string;
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GetLogsRequest {
lines?: number;
}

View File

@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export interface GetLogsResponse {
/**
* Number of lines to retrieve from the bottom
*/
logs?: string;
success?: boolean;
}

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import { Playlist } from './Playlist';
export interface GetMp3sResponse {
mp3s: Array<DatabaseFile>;
/**
* All audio playlists
*/
playlists: Array<Playlist>;
}

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { DatabaseFile } from './DatabaseFile';
import { Playlist } from './Playlist';
export interface GetMp4sResponse {
mp4s: Array<DatabaseFile>;
/**
* All video playlists
*/
playlists: Array<Playlist>;
}

View File

@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface GetPlaylistRequest {
playlist_id: string;
type?: FileType;
uuid?: string;
include_file_metadata?: boolean;
}

View File

@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
import { Playlist } from './Playlist';
export interface GetPlaylistResponse {
playlist: Playlist;
type: FileType;
success: boolean;
}

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