Compare commits

...

121 Commits

Author SHA1 Message Date
Tzahi12345
33a99d9c8d Added files_api (migrated functions from db_api that are file related)
Archive dialog can now always be opened
2023-04-29 19:41:34 -04:00
Tzahi12345
0e5c78db0d Modified archive logic to align with #366 2023-04-29 19:34:08 -04:00
Glassed Silver
e7b9dfd312 Merge pull request #873 from Tzahi12345/pre-4.3.1-bug-fixes
Pre 4.3.1 bug fixes
2023-04-28 10:26:11 +02:00
Glassed Silver
1e2922559c Merge pull request #868 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-04-26 15:30:41 +02:00
Tzahi12345
cfbee6d6f1 Sub name duplicated bug fix 2023-04-25 23:59:13 -04:00
Tzahi12345
c75d58efd5 Fixed issue where duplicate sub names were possible (#801) 2023-04-25 23:36:15 -04:00
Tzahi12345
efbf395368 Fixed settings tab url labels 2023-04-25 22:14:52 -04:00
Tzahi12345
dab9fc83ba Added support for discord webhooks
Improved download error notifications
2023-04-25 22:14:35 -04:00
Tzahi12345
e086bbc301 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into pre-4.3.1-bug-fixes 2023-04-25 21:57:24 -04:00
Tzahi12345
0b3a21b383 Added missing config file settings 2023-04-24 21:17:33 -04:00
Tzahi12345
f973426bd2 Hotfix for error that prevents downloads from occurring 2023-04-24 21:11:10 -04:00
Tzahi12345
a4c78e3a3d Minor verbose message update 2023-04-24 21:10:51 -04:00
Tzahi12345
50d3bc183b Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into pre-4.3.1-bug-fixes 2023-04-24 20:14:55 -04:00
Tzahi12345
5a379a6a2b Updated package-lock.json (#877) 2023-04-24 20:03:11 -04:00
Tzahi12345
71692f6b13 Updated dependencies 2023-04-24 19:55:53 -04:00
Tzahi12345
1746b08d4c Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into pre-4.3.1-bug-fixes 2023-04-24 19:32:19 -04:00
Tzahi12345
3bc0ec8bb5 Added category tests
Fixed syntax errors in tests.js
2023-04-24 19:30:25 -04:00
Tzahi12345
2df4dc1bfc Updated file not found error message in /stream 2023-04-24 19:29:35 -04:00
Tzahi12345
0e190fca2a Ghost file card count are now per-sub 2023-04-24 19:28:52 -04:00
Tzahi12345
5aea0b7a3d Archive text file is now temporarily added to a sub dir when checking a sub for speed purposes 2023-04-24 19:28:11 -04:00
Tzahi12345
d76aaf83f6 Merge pull request #707 from Tzahi12345/categories-fix
Categories matching bug fix
2023-04-24 10:35:08 -04:00
Tzahi12345
a996b9f0d2 ng-deeps now only apply to current component
https://stackoverflow.com/questions/46786986/how-and-where-to-use-ng-deep
2023-04-23 22:31:11 -04:00
Tzahi12345
d3b88412c6 Fixed thumbnails for auto-generated playlists 2023-04-23 22:27:09 -04:00
Tzahi12345
6cee892e18 Added label for category field in video-info-dialog 2023-04-23 22:20:50 -04:00
Tzahi12345
e2438a236b Adjusted category UI styling to Angular 15 updates 2023-04-23 22:14:20 -04:00
Ettore Atalan
7a4ae052ed Translated using Weblate (German)
Currently translated at 95.0% (456 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2023-04-23 23:48:35 +02:00
Ettore Atalan
b65a7b3dd4 Translated using Weblate (German)
Currently translated at 91.8% (441 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2023-04-23 23:48:35 +02:00
Tzahi12345
955c401f0b Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into categories-fix 2023-04-22 17:47:16 -04:00
Tzahi12345
f0a34df7c6 Fixed syntax error in utils 2023-04-21 16:48:24 -04:00
Tzahi12345
e2c68713ba Fixed bug where added args would get injected improperly if they had a value (#745) 2023-04-21 16:34:47 -04:00
Tzahi12345
24cabc1f02 Fixed issue where videos would be downloaded in the collectInfo stage (#774) 2023-04-21 16:12:41 -04:00
Tzahi12345
1edcfca6c3 Fixed bug where notifications wouldn't be set as read if using local db 2023-04-21 15:57:59 -04:00
Tzahi12345
e7fa25cf38 Fixed tasks UI bug 2023-04-21 15:34:14 -04:00
Tzahi12345
527b1f1cb9 Merge pull request #872 from Tzahi12345/twitch-downloader-fix
Twitch chat download fixes
2023-04-21 00:16:06 -04:00
Tzahi12345
24d8072eb5 Fixed minor syntax error in Dockerfile 2023-04-20 21:47:20 -04:00
Tzahi12345
c81bf980ca Separated image for TwitchDownloader download in Dockerfile 2023-04-20 21:41:36 -04:00
Tzahi12345
a91381720f Added link in error message to get TiwtchDownloaderCLI 2023-04-20 21:16:06 -04:00
Tzahi12345
edd4a0928c Fixed twitch downloading by using TiwtchDownloader's CLI
Removed unecessary settings

Created dedicated folder for docker utils
2023-04-20 21:11:48 -04:00
Tzahi12345
770916492e Fixed authentication error in notifications (2) 2023-04-17 23:56:52 -04:00
Tzahi12345
6400b807c2 Fixed auth error in notifications 2023-04-17 23:54:34 -04:00
Tzahi12345
3a7e2d9d0f Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2023-04-16 21:09:35 -04:00
Tzahi12345
ca5381fe0f Updated tasks DB-related code to not insert properties that prevent local_db from being imported
Added DB functionality to remove properties from records

DB records in local DB can now be updated if nested
2023-04-16 21:08:18 -04:00
Glassed Silver
bd8d91ebe5 Merge pull request #862 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-04-16 14:05:13 +02:00
Kawaxte
27f05dbae3 Translated using Weblate (Estonian)
Currently translated at 31.8% (153 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/et/
2023-04-16 13:51:39 +02:00
Glassed Silver
c7bf1d0e27 Merge pull request #786 from beauharrison/docker-custom-user-startup
Fixed long docker startup time by optimizing chown use
2023-04-16 10:30:24 +02:00
Glassed Silver
57be0a032e Merge pull request #858 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-04-16 07:50:35 +02:00
Glassed Silver
6fe4b22efc Merge pull request #860 from Tzahi12345/docker-fix
Docker fixes
2023-04-16 07:48:50 +02:00
Tzahi12345
ed492e54c9 Fixed pycryptodomex build error
Changed tcd package to fix broken twitch downloads (#859)
2023-04-15 16:45:32 -04:00
Kawaxte
af2d583924 Added translation using Weblate (Estonian) 2023-04-15 12:14:14 +02:00
yangyangdaji
c61d51be76 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (480 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2023-04-14 16:49:59 +02:00
Tzahi12345
f3a7d198dc Downgraded mongo image to mongo:4 for compatibility purposes 2023-04-12 19:05:26 -04:00
Tzahi12345
3c03cd96d9 Added link to notifications docs in settings menu 2023-04-12 00:45:13 -04:00
Tzahi12345
43848792fa Updated translations source file 2023-04-12 00:27:54 -04:00
Tzahi12345
fb27264d33 Added support for custom webhook URLs for notifications 2023-04-11 23:45:30 -04:00
Tzahi12345
7593a23c2e Updated README 2023-04-07 21:12:22 -04:00
Tzahi12345
aedde4b4fc Removed unused dependency 2023-04-07 21:04:32 -04:00
Tzahi12345
cd2a727e23 Added some documentation to downloader.js 2023-04-07 21:01:08 -04:00
Tzahi12345
30c7a96540 Dockerfile now installs pycryptodomex (#819) 2023-04-07 20:24:57 -04:00
Tzahi12345
5197a5f1cc Minor refactor of utils.js 2023-04-07 20:23:19 -04:00
Tzahi12345
12e69afa84 Merge pull request #708 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-04-02 21:14:09 -04:00
Tzahi12345
e720edf9f0 Added hint that restart is required after changing the downloader
Updated translation source file
2023-04-01 19:26:25 -04:00
Tzahi12345
3544a2316d Added warning to using non yt-dlp downloaders 2023-04-01 19:14:07 -04:00
Tzahi12345
4b2e5fb636 Incremented version to v4.3.1 2023-04-01 19:08:03 -04:00
Tzahi12345
929e01e5eb Merge pull request #809 from Tzahi12345/notifications-update
v4.3.1 update
2023-04-01 19:02:35 -04:00
Frederik Tranberg
c869c84553 Translated using Weblate (Danish)
Currently translated at 3.3% (13 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/da/
2023-03-22 09:39:44 +01:00
Frederik Tranberg
32b2a02f79 Added translation using Weblate (Danish) 2023-03-21 09:00:08 +01:00
Thunderstrike116
105140e674 Translated using Weblate (Greek)
Currently translated at 2.0% (8 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/el/
2023-03-16 20:38:14 +01:00
Thunderstrike116
475efc4d9e Added translation using Weblate (Greek) 2023-03-15 19:49:26 +01:00
yangyangdaji
c8a3551402 Translated using Weblate (Turkish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/tr/
2023-03-04 06:37:16 +01:00
yangyangdaji
c526457ee0 Translated using Weblate (Japanese)
Currently translated at 61.8% (240 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ja/
2023-03-04 06:37:16 +01:00
yangyangdaji
859861fae8 Translated using Weblate (Swedish)
Currently translated at 30.1% (117 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/sv/
2023-03-04 06:37:16 +01:00
yangyangdaji
c63744fb3a Translated using Weblate (Telugu)
Currently translated at 70.1% (272 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/te/
2023-03-04 06:37:15 +01:00
yangyangdaji
bbc5b6d222 Translated using Weblate (Macedonian)
Currently translated at 78.0% (303 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/mk/
2023-03-04 06:37:15 +01:00
yangyangdaji
95c0a4977c Translated using Weblate (Korean)
Currently translated at 70.3% (273 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2023-03-04 06:37:14 +01:00
yangyangdaji
40eefc2ea3 Translated using Weblate (Portuguese)
Currently translated at 70.3% (273 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt/
2023-03-04 06:37:14 +01:00
yangyangdaji
8fb0b17441 Translated using Weblate (Dutch)
Currently translated at 99.7% (387 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2023-03-04 06:37:14 +01:00
yangyangdaji
191f3b3781 Translated using Weblate (Russian)
Currently translated at 76.5% (297 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2023-03-04 06:37:13 +01:00
yangyangdaji
95342d6d97 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2023-03-04 06:37:13 +01:00
yangyangdaji
5c70e71710 Translated using Weblate (German)
Currently translated at 99.7% (387 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2023-03-04 06:37:12 +01:00
Oğuz Ersen
2d0137db43 Translated using Weblate (Turkish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/tr/
2023-02-27 20:38:51 +01:00
Allan Nordhøy
01b307ddb2 Translated using Weblate (Norwegian Bokmål)
Currently translated at 46.1% (179 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nb_NO/
2023-02-09 14:40:17 +01:00
gallegonovato
9e0d91992d Translated using Weblate (Spanish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2023-02-09 14:40:16 +01:00
Allan Nordhøy
4e6b895af3 Translated using Weblate (English)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/en/
2023-02-09 14:40:16 +01:00
maboroshin
bdaf336712 Translated using Weblate (Japanese)
Currently translated at 62.1% (241 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ja/
2023-02-05 20:40:06 +01:00
ssantos
9539e78295 Translated using Weblate (Portuguese)
Currently translated at 70.1% (272 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt/
2023-01-12 16:52:02 +01:00
Nathan-Moignard
1797772395 Translated using Weblate (French)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2023-01-03 00:57:26 +01:00
John Willemsen
2a19e60c85 Translated using Weblate (Dutch)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2022-12-15 13:48:01 +01:00
lk.KEVIN
3ba1b05e84 Translated using Weblate (Spanish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2022-12-04 20:47:19 +01:00
lk.KEVIN
52b435b8ae Translated using Weblate (Spanish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2022-11-28 11:47:58 +01:00
Oğuz Ersen
b78bb83ec9 Translated using Weblate (Turkish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/tr/
2022-11-27 08:48:33 +01:00
Oğuz Ersen
c6ede725e1 Added translation using Weblate (Turkish) 2022-11-26 06:51:43 +01:00
Beau Harrison
142d708ee3 Fixed long docker startup time by optimizing chown use 2022-11-17 08:16:24 +10:00
Tanat
477d2f6672 Translated using Weblate (Korean)
Currently translated at 70.1% (272 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ko/
2022-11-14 15:47:57 +01:00
Maite Guix
5cf6e1817f Translated using Weblate (Catalan)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2022-10-24 07:50:45 +02:00
yangyangdaji
1d6be1442c Translated using Weblate (French)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2022-10-21 11:03:42 +02:00
yangyangdaji
8c938b635c Translated using Weblate (Polish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2022-10-02 12:18:19 +02:00
yangyangdaji
b56eea3b76 Translated using Weblate (Indonesian)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/id/
2022-10-02 12:18:18 +02:00
yangyangdaji
2aa5d3e91e Translated using Weblate (French)
Currently translated at 99.7% (387 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2022-10-02 12:18:18 +02:00
yangyangdaji
89a16ef555 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-10-02 12:18:18 +02:00
Kachelkaiser
f818ed744b Translated using Weblate (German)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/de/
2022-09-23 10:23:36 +02:00
Isaac Abadi
2e52ec22e0 Default sort for videos is now download date 2022-09-15 21:38:19 -04:00
Isaac Abadi
efdd0dd228 Categories fix for yt-dlp 2022-09-15 21:38:00 -04:00
atilluF
48248c7ddf Translated using Weblate (Italian)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/it/
2022-09-03 13:22:38 +02:00
Xyx S
49e2458747 Translated using Weblate (Japanese)
Currently translated at 29.6% (115 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ja/
2022-09-03 02:25:55 +02:00
Xyx S
1f973efe60 Translated using Weblate (English)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/en/
2022-09-03 02:25:55 +02:00
YMisterXY
3847f3e0d3 Translated using Weblate (Polish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2022-08-31 21:24:32 +02:00
atilluF
26d3875293 Translated using Weblate (Italian)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/it/
2022-08-31 21:24:31 +02:00
YMisterXY
55a4e2e1f2 Translated using Weblate (Polish)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2022-08-30 16:55:08 +02:00
YMisterXY
f26016d4ec Added translation using Weblate (Polish) 2022-08-29 19:53:05 +02:00
KoalaUniverse
cd7adcecdd Translated using Weblate (Dutch)
Currently translated at 89.4% (347 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/nl/
2022-08-26 18:21:21 +02:00
Hugel
09847f74ae Translated using Weblate (Japanese)
Currently translated at 16.4% (64 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ja/
2022-08-25 05:21:39 +02:00
Hugel
8ea78f38ed Translated using Weblate (Japanese)
Currently translated at 15.9% (62 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ja/
2022-08-24 04:24:02 +02:00
yangyangdaji
0675ef21c7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-08-21 12:22:10 +02:00
Maite Guix
dfe554d880 Translated using Weblate (Catalan)
Currently translated at 89.4% (347 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ca/
2022-08-13 20:22:44 +02:00
Maxime Leroy
6f1a40d329 Translated using Weblate (French)
Currently translated at 99.7% (387 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2022-07-22 21:14:47 +02:00
yangyangdaji
9c7416b2eb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-07-18 07:19:52 +02:00
yangyangdaji
54d8d7844a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hans/
2022-07-16 08:15:48 +02:00
はらたく
1533bc951b Translated using Weblate (Japanese)
Currently translated at 15.2% (59 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ja/
2022-07-14 02:15:26 +02:00
Azhar Pusparadhian
31f8827e61 Translated using Weblate (Indonesian)
Currently translated at 100.0% (388 of 388 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/id/
2022-07-14 02:15:26 +02:00
はらたく
5f87356544 Added translation using Weblate (Japanese) 2022-07-12 11:18:03 +02:00
Isaac Abadi
415c97cb09 Fixed issue where categories were not being properly applied to matching files (#701) 2022-07-07 01:07:22 -04:00
65 changed files with 31148 additions and 1148 deletions

View File

@@ -2,7 +2,7 @@
FROM ubuntu:22.04 AS ffmpeg
ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability
COPY ffmpeg-fetch.sh .
COPY docker-utils/ffmpeg-fetch.sh .
RUN chmod +x ffmpeg-fetch.sh
RUN sh ./ffmpeg-fetch.sh
@@ -47,21 +47,31 @@ RUN npm config set strict-ssl false && \
npm install --prod && \
ls -al
FROM base as python
WORKDIR /app
COPY docker-utils/GetTwitchDownloader.py .
RUN apt update && \
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install PyGithub requests
RUN python GetTwitchDownloader.py
# Final image
FROM base
RUN npm install -g pm2 && \
apt update && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install tcd
RUN pip install pycryptodomex
WORKDIR /app
# User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
RUN chown $UID:$GID .
RUN chmod +x /app/fix-scripts/*.sh
# Add some persistence data
#VOLUME ["/app/appdata"]

View File

@@ -6,7 +6,7 @@
[![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 13](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 15](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
Now with [Docker](#Docker) support!
@@ -14,7 +14,7 @@ Now with [Docker](#Docker) support!
## Getting Started
Check out the prerequisites, and go to the installation section. Easy as pie!
Check out the prerequisites, and go to the [installation](#Installing) section. Easy as pie!
Here's an image of what it'll look like once you're done:
@@ -52,6 +52,8 @@ Optional dependencies:
### Installing
If you are using Docker, skip to the [Docker](#Docker) section. Otherwise, continue:
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file.

View File

@@ -34,6 +34,7 @@ const categories_api = require('./categories');
const twitch_api = require('./twitch');
const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive');
const files_api = require('./files');
var app = express();
@@ -162,6 +163,7 @@ app.use(bodyParser.json());
// use passport
app.use(auth_api.passport.initialize());
app.use(auth_api.passport.session());
// actual functions
@@ -172,10 +174,10 @@ async function checkMigrations() {
if (!simplified_db_migration_complete) {
logger.info('Beginning migration: 4.1->4.2+')
let success = await simplifyDBFileStructure();
success = success && await db_api.addMetadataPropertyToDB('view_count');
success = success && await db_api.addMetadataPropertyToDB('description');
success = success && await db_api.addMetadataPropertyToDB('height');
success = success && await db_api.addMetadataPropertyToDB('abr');
success = success && await files_api.addMetadataPropertyToDB('view_count');
success = success && await files_api.addMetadataPropertyToDB('description');
success = success && await files_api.addMetadataPropertyToDB('height');
success = success && await files_api.addMetadataPropertyToDB('abr');
// sets migration to complete
db.set('simplified_db_migration_complete', true).write();
if (success) { logger.info('4.1->4.2+ migration complete!'); }
@@ -567,14 +569,7 @@ function loadConfigValues() {
url_domain = new URL(url);
let logger_level = config_api.getConfigItem('ytdl_logger_level');
const possible_levels = ['error', 'warn', 'info', 'verbose', 'debug'];
if (!possible_levels.includes(logger_level)) {
logger.error(`${logger_level} is not a valid logger level! Choose one of the following: ${possible_levels.join(', ')}.`)
logger_level = 'info';
}
logger.level = logger_level;
winston.loggers.get('console').level = logger_level;
logger.transports[2].level = logger_level;
utils.updateLoggerLevel(logger_level);
}
function calculateSubcriptionRetrievalDelay(subscriptions_amount) {
@@ -730,7 +725,7 @@ const optionalJwt = async function (req, res, next) {
const uuid = using_body ? req.body.uuid : req.query.uuid;
const uid = using_body ? req.body.uid : req.query.uid;
const playlist_id = using_body ? req.body.playlist_id : req.query.playlist_id;
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await db_api.getPlaylist(playlist_id, uuid, true);
const file = !playlist_id ? auth_api.getUserVideo(uuid, uid, true) : await files_api.getPlaylist(playlist_id, uuid, true);
if (file) {
req.can_watch = true;
return next();
@@ -941,7 +936,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
const sub_id = req.body.sub_id;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const {files, file_count} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
const {files, file_count} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
res.send({
files: files,
@@ -1107,7 +1102,7 @@ app.post('/api/incrementViewCount', async (req, res) => {
uuid = req.user.uid;
}
const file_obj = await db_api.getVideo(file_uid, uuid, sub_id);
const file_obj = await files_api.getVideo(file_uid, uuid, sub_id);
const current_view_count = file_obj && file_obj['local_view_count'] ? file_obj['local_view_count'] : 0;
const new_view_count = current_view_count + 1;
@@ -1235,7 +1230,7 @@ app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
let deleteForever = req.body.deleteForever;
let file_uid = req.body.file_uid;
let success = await db_api.deleteFile(file_uid, deleteForever);
let success = await files_api.deleteFile(file_uid, deleteForever);
if (success) {
res.send({
@@ -1323,7 +1318,7 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let playlistName = req.body.playlistName;
let uids = req.body.uids;
const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
const new_playlist = await files_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
res.send({
new_playlist: new_playlist,
@@ -1336,13 +1331,13 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
let uuid = req.body.uuid ? req.body.uuid : (req.user && req.user.uid ? req.user.uid : null);
let include_file_metadata = req.body.include_file_metadata;
const playlist = await db_api.getPlaylist(playlist_id, uuid);
const playlist = await files_api.getPlaylist(playlist_id, uuid);
const file_objs = [];
if (playlist && include_file_metadata) {
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const file_obj = await db_api.getVideo(uid, uuid);
const file_obj = await files_api.getVideo(uid, uuid);
if (file_obj) file_objs.push(file_obj);
// TODO: remove file from playlist if could not be found
}
@@ -1380,7 +1375,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
playlist.uids.push(file_uid);
let success = await db_api.updatePlaylist(playlist);
let success = await files_api.updatePlaylist(playlist);
res.send({
success: success
});
@@ -1388,7 +1383,7 @@ app.post('/api/addFileToPlaylist', optionalJwt, async (req, res) => {
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
let playlist = req.body.playlist;
let success = await db_api.updatePlaylist(playlist, req.user && req.user.uid);
let success = await files_api.updatePlaylist(playlist, req.user && req.user.uid);
res.send({
success: success
});
@@ -1418,7 +1413,7 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => {
const blacklistMode = req.body.blacklistMode;
let wasDeleted = false;
wasDeleted = await db_api.deleteFile(uid, blacklistMode);
wasDeleted = await files_api.deleteFile(uid, blacklistMode);
res.send(wasDeleted);
});
@@ -1450,7 +1445,7 @@ app.post('/api/deleteAllFiles', optionalJwt, async (req, res) => {
for (let i = 0; i < files.length; i++) {
let wasDeleted = false;
wasDeleted = await db_api.deleteFile(files[i].uid, blacklistMode);
wasDeleted = await files_api.deleteFile(files[i].uid, blacklistMode);
if (wasDeleted) {
delete_count++;
}
@@ -1476,10 +1471,10 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
if (playlist_id) {
zip_file_generated = true;
const playlist_files_to_download = [];
const playlist = await db_api.getPlaylist(playlist_id, uuid);
const playlist = await files_api.getPlaylist(playlist_id, uuid);
for (let i = 0; i < playlist['uids'].length; i++) {
const playlist_file_uid = playlist['uids'][i];
const file_obj = await db_api.getVideo(playlist_file_uid, uuid);
const file_obj = await files_api.getVideo(playlist_file_uid, uuid);
playlist_files_to_download.push(file_obj);
}
@@ -1493,7 +1488,7 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
// generate zip
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
} else {
const file_obj = await db_api.getVideo(uid, uuid, sub_id)
const file_obj = await files_api.getVideo(uid, uuid, sub_id)
file_path_to_download = file_obj.path;
}
if (!path.isAbsolute(file_path_to_download)) file_path_to_download = path.join(__dirname, file_path_to_download);
@@ -1640,12 +1635,12 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
if (!multiUserMode || req.isAuthenticated() || req.can_watch) {
file_obj = await db_api.getVideo(uid, uuid, sub_id);
file_obj = await files_api.getVideo(uid, uuid, sub_id);
if (file_obj) file_path = file_obj['path'];
else file_path = null;
}
if (!fs.existsSync(file_path)) {
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj.id}`);
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
}
const stat = fs.statSync(file_path);
const fileSize = stat.size;
@@ -2038,7 +2033,7 @@ app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => {
// notifications
app.post('/api/getNotifications', optionalJwt, async (req, res) => {
const uuid = req.user.uid;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const notifications = await db_api.getRecords('notifications', {user_uid: uuid});
@@ -2047,7 +2042,7 @@ app.post('/api/getNotifications', optionalJwt, async (req, res) => {
// set notifications to read
app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
const uuid = req.user.uid;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const success = await db_api.updateRecords('notifications', {user_uid: uuid}, {read: true});
@@ -2055,7 +2050,7 @@ app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => {
});
app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
const uid = req.body.uid;
const uid = req.isAuthenticated() ? req.user.uid : null;
const success = await db_api.removeRecord('notifications', {uid: uid});
@@ -2063,7 +2058,7 @@ app.post('/api/deleteNotification', optionalJwt, async (req, res) => {
});
app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
const uuid = req.user.uid;
const uuid = req.isAuthenticated() ? req.user.uid : null;
const success = await db_api.removeAllRecords('notifications', {user_uid: uuid});
@@ -2088,7 +2083,7 @@ app.get('/api/rss', async function (req, res) {
const sub_id = req.query.sub_id ? decodeURIComponent(req.query.sub_id) : null;
const uuid = req.query.uuid ? decodeURIComponent(req.query.uuid) : null;
const {files} = await db_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
const {files} = await files_api.getAllFiles(sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid);
const feed = new Feed({
title: 'Downloads',

View File

@@ -23,7 +23,12 @@
"download_only_mode": false,
"allow_autoplay": true,
"enable_downloads_manager": true,
"allow_playlist_categorization": true
"allow_playlist_categorization": true,
"force_autoplay": false,
"enable_notifications": true,
"enable_all_notifications": true,
"allowed_notification_types": [],
"enable_rss_feed": false
},
"API": {
"use_API_key": false,
@@ -35,7 +40,17 @@
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false
"generate_NFO_files": false,
"use_ntfy_API": false,
"ntfy_topic_URL": "",
"use_gotify_API": false,
"gotify_server_URL": "",
"gotify_app_token": "",
"use_telegram_API": false,
"telegram_bot_token": "",
"telegram_chat_id": "",
"webhook_URL": "",
"discord_webhook_URL": ""
},
"Themes": {
"default_theme": "default",

View File

@@ -208,9 +208,6 @@ const DEFAULT_CONFIG = {
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false,
"use_sponsorblock_API": false,
"generate_NFO_files": false,
@@ -221,7 +218,9 @@ const DEFAULT_CONFIG = {
"gotify_app_token": "",
"use_telegram_API": false,
"telegram_bot_token": "",
"telegram_chat_id": ""
"telegram_chat_id": "",
"webhook_URL": "",
"discord_webhook_URL": ""
},
"Themes": {
"default_theme": "default",

View File

@@ -110,18 +110,6 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_youtube_api_key',
'path': 'YoutubeDLMaterial.API.youtube_API_key'
},
'ytdl_use_twitch_api': {
'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API'
},
'ytdl_twitch_client_id': {
'key': 'ytdl_twitch_client_id',
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
},
'ytdl_twitch_client_secret': {
'key': 'ytdl_twitch_client_secret',
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
},
'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat',
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
@@ -166,6 +154,14 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_telegram_chat_id',
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
},
'ytdl_webhook_url': {
'key': 'ytdl_webhook_url',
'path': 'YoutubeDLMaterial.API.webhook_URL'
},
'ytdl_discord_webhook_url': {
'key': 'ytdl_discord_webhook_url',
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
},
// Themes
@@ -350,4 +346,6 @@ const YTDL_ARGS_WITH_VALUES = [
// we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
exports.CURRENT_VERSION = 'v4.3';
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
exports.CURRENT_VERSION = 'v4.3.1';

View File

@@ -1,11 +1,11 @@
var fs = require('fs-extra')
var path = require('path')
const fs = require('fs-extra')
const path = require('path')
const { MongoClient } = require("mongodb");
const { uuid } = require('uuidv4');
const _ = require('lodash');
const config_api = require('./config');
var utils = require('./utils')
const utils = require('./utils')
const logger = require('./logger');
const low = require('lowdb')
@@ -167,82 +167,9 @@ exports._connectToDB = async (custom_connection_string = null) => {
}
}
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.fixVideoMetadataPerms(file_path, type);
// add thumbnail path
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']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (user_uid) file_object['user_uid'] = user_uid;
if (sub_id) file_object['sub_id'] = sub_id;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_path, type)
}
return file_obj;
}
async function registerFileDBManual(file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
await exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object;
}
function generateFileObject(file_path, type) {
var jsonobj = utils.getJSON(file_path, type);
if (!jsonobj) {
return null;
} else if (!jsonobj['_filename']) {
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
return null;
}
const ext = (type === 'audio') ? '.mp3' : '.mp4'
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
// console.
var stats = fs.statSync(true_file_path);
const file_id = utils.removeFileExtension(path.basename(file_path));
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = utils.formatDateString(jsonobj.upload_date);
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(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
function getAppendedBasePathSub(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
exports.setVideoProperty = async (file_uid, assignment_obj) => {
// TODO: check if video exists, throw error if not
await db_api.updateRecord('files', {uid: file_uid}, assignment_obj);
}
exports.getFileDirectoriesAndDBs = async () => {
@@ -317,277 +244,6 @@ exports.getFileDirectoriesAndDBs = async () => {
return dirs_to_check;
}
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
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
for (let j = 0; j < files.length; j++) {
const file = files[j];
// check if file exists in db, if not add it
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
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.`);
}
}
}
}
return imported_files;
}
exports.addMetadataPropertyToDB = async (property_key) => {
try {
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
const update_obj = {};
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
for (let j = 0; j < files.length; j++) {
const file = files[j];
if (file[property_key]) {
update_obj[file.uid] = {[property_key]: file[property_key]};
}
}
}
return await exports.bulkUpdateRecordsByKey('files', 'uid', update_obj);
} catch(err) {
logger.error(err);
return false;
}
}
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL'];
let new_playlist = {
name: playlist_name,
uids: uids,
id: uuid(),
thumbnailURL: thumbnailToUse,
registered: Date.now(),
randomize_order: false
};
new_playlist.user_uid = user_uid ? user_uid : undefined;
await exports.insertRecordIntoTable('playlists', new_playlist);
const duration = await exports.calculatePlaylistDuration(new_playlist);
await exports.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
return new_playlist;
}
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
let playlist = await exports.getRecord('playlists', {id: playlist_id});
if (!playlist) {
playlist = await exports.getRecord('categories', {uid: playlist_id});
if (playlist) {
const uids = (await exports.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
playlist['uids'] = uids;
playlist['auto'] = true;
}
}
// converts playlists to new UID-based schema
if (playlist && playlist['fileNames'] && !playlist['uids']) {
playlist['uids'] = [];
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
for (let i = 0; i < playlist['fileNames'].length; i++) {
const fileName = playlist['fileNames'][i];
const uid = await exports.getVideoUIDByID(fileName, user_uid);
if (uid) playlist['uids'].push(uid);
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
}
exports.updatePlaylist(playlist, user_uid);
}
// prevent unauthorized users from accessing the file info
if (require_sharing && !playlist['sharingEnabled']) return null;
return playlist;
}
exports.updatePlaylist = async (playlist) => {
let playlistID = playlist.id;
const duration = await exports.calculatePlaylistDuration(playlist);
playlist.duration = duration;
return await exports.updateRecord('playlists', {id: playlistID}, playlist);
}
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
let success = await exports.updateRecord('playlists', {id: playlist_id}, assignment_obj);
if (!success) {
success = await exports.updateRecord('categories', {uid: playlist_id}, assignment_obj);
}
if (!success) {
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
}
return success;
}
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
if (!playlist_file_objs) {
playlist_file_objs = [];
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const file_obj = await exports.getVideo(uid);
if (file_obj) playlist_file_objs.push(file_obj);
}
}
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
}
exports.deleteFile = async (uid, blacklistMode = false) => {
const file_obj = await exports.getVideo(uid);
const type = file_obj.isAudio ? 'audio' : 'video';
const folderPath = path.dirname(file_obj.path);
const ext = type === 'audio' ? 'mp3' : 'mp4';
const name = file_obj.id;
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
var jsonPath = `${file_obj.path}.info.json`;
var altJSONPath = `${filePathNoExtension}.info.json`;
var thumbnailPath = `${filePathNoExtension}.webp`;
var altThumbnailPath = `${filePathNoExtension}.jpg`;
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
let jsonExists = await fs.pathExists(jsonPath);
let thumbnailExists = await fs.pathExists(thumbnailPath);
if (!jsonExists) {
if (await fs.pathExists(altJSONPath)) {
jsonExists = true;
jsonPath = altJSONPath;
}
}
if (!thumbnailExists) {
if (await fs.pathExists(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
let fileExists = await fs.pathExists(file_obj.path);
if (config_api.descriptors[uid]) {
try {
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
config_api.descriptors[uid][i].destroy();
}
} catch(e) {
}
}
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
// get id/extractor from JSON
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
let retrievedID = null;
let retrievedExtractor = null;
if (info_json) {
retrievedID = info_json['id'];
retrievedExtractor = info_json['extractor'];
}
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
if (!blacklistMode) {
// workaround until a files_api is created (using archive_api would make a circular dependency)
await exports.removeAllRecords('archives', {extractor: retrievedExtractor, id: retrievedID, type: type, user_uid: file_obj.user_uid, sub_id: file_obj.sub_id});
// await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
}
}
if (jsonExists) await fs.unlink(jsonPath);
if (thumbnailExists) await fs.unlink(thumbnailPath);
await exports.removeRecord('files', {uid: uid});
if (fileExists) {
await fs.unlink(file_obj.path);
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
return false;
} else {
return true;
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
}
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
exports.getVideoUIDByID = async (file_id, uuid = null) => {
const file_obj = await exports.getRecord('files', {id: file_id});
return file_obj ? file_obj['uid'] : null;
}
exports.getVideo = async (file_uid) => {
return await exports.getRecord('files', {uid: file_uid});
}
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
const filter_obj = {user_uid: uuid};
const regex = true;
if (text_search) {
if (regex) {
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
} else {
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
}
}
if (favorite_filter) {
filter_obj['favorite'] = true;
}
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
const files = JSON.parse(JSON.stringify(await exports.getRecords('files', filter_obj, false, sort, range, text_search)));
const file_count = await exports.getRecords('files', filter_obj, true);
return {files, file_count};
}
exports.setVideoProperty = async (file_uid, assignment_obj) => {
// TODO: check if video exists, throw error if not
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
}
// Basic DB functions
// Create
@@ -698,9 +354,15 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort
// Update
exports.updateRecord = async (table, filter_obj, update_obj) => {
exports.updateRecord = async (table, filter_obj, update_obj, nested_mode = false) => {
// local db override
if (using_local_db) {
if (nested_mode) {
// if object is nested we need to handle it differently
update_obj = utils.convertFlatObjectToNestedObject(update_obj);
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').merge(update_obj).write();
return true;
}
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
return true;
}
@@ -714,7 +376,14 @@ exports.updateRecord = async (table, filter_obj, update_obj) => {
exports.updateRecords = async (table, filter_obj, update_obj) => {
// local db override
if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').each((record) => {
const props_to_update = Object.keys(update_obj);
for (let i = 0; i < props_to_update.length; i++) {
const prop_to_update = props_to_update[i];
const prop_value = update_obj[prop_to_update];
record[prop_to_update] = prop_value;
}
}).write();
return true;
}
@@ -722,6 +391,18 @@ exports.updateRecords = async (table, filter_obj, update_obj) => {
return !!(output['result']['ok']);
}
exports.removePropertyFromRecord = async (table, filter_obj, remove_obj) => {
// local db override
if (using_local_db) {
const props_to_remove = Object.keys(remove_obj);
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').unset(props_to_remove).write();
return true;
}
const output = await database.collection(table).updateOne(filter_obj, {$unset: remove_obj});
return !!(output['result']['ok']);
}
exports.bulkUpdateRecordsByKey = async (table, key_label, update_obj) => {
// local db override
if (using_local_db) {

View File

@@ -1,7 +1,6 @@
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;
@@ -14,6 +13,7 @@ const { create } = require('xmlbuilder2');
const categories_api = require('./categories');
const utils = require('./utils');
const db_api = require('./db');
const files_api = require('./files');
const notifications_api = require('./notifications');
const archive_api = require('./archive');
@@ -28,6 +28,25 @@ if (db_api.database_initialized) {
});
}
/*
This file handles all the downloading functionality.
To download a file, we go through 4 steps. Here they are with their respective index & function:
0: Create the download
- createDownload()
1: Get info for the download (we need this step for categories and archive functionality)
- collectInfo()
2: Download the file
- downloadQueuedFile()
3: Complete
- N/A
We use checkDownloads() to move downloads through the steps and call their respective functions.
*/
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => {
return await mutex.runExclusive(async () => {
const download = {
@@ -110,7 +129,7 @@ exports.clearDownload = async (download_uid) => {
async function handleDownloadError(download, error_message, error_type = null) {
if (!download || !download['uid']) return;
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_type);
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
}
@@ -203,6 +222,7 @@ async function collectInfo(download_uid) {
return;
}
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive && !options.ignoreArchive) {
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
@@ -227,11 +247,10 @@ async function collectInfo(download_uid) {
options.customOutput = category['custom_output'];
options.noRelativePath = true;
args = await exports.generateArgs(url, type, options, download['user_uid']);
args = utils.filterArgs(args, ['--no-simulate']);
info = await exports.getVideoInfoByURL(url, args, download_uid);
}
download['category'] = category;
const stripped_category = category ? {name: category['name'], uid: category['uid']} : null;
// setup info required to calculate download progress
@@ -254,6 +273,7 @@ async function collectInfo(download_uid) {
files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title'],
category: stripped_category,
prefetched_info: null
});
}
@@ -296,7 +316,7 @@ async function downloadQueuedFile(download_uid) {
clearInterval(download_checker);
if (err) {
logger.error(err.stderr);
await handleDownloadError(download, err.stderr);
await handleDownloadError(download, err.stderr, 'unknown_error');
resolve(false);
return;
} else if (output) {
@@ -332,7 +352,7 @@ async function downloadQueuedFile(download_uid) {
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')) {
&& 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']);
@@ -366,10 +386,9 @@ async function downloadQueuedFile(download_uid) {
}
// 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);
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive && !options.ignoreArchive) await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
@@ -381,7 +400,7 @@ async function downloadQueuedFile(download_uid) {
if (file_objs.length > 1) {
// create playlist
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
} else if (file_objs.length === 1) {
container = file_objs[0];
} else {
@@ -403,6 +422,10 @@ async function downloadQueuedFile(download_uid) {
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
}
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
@@ -530,7 +553,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const temp_args = utils.filterArgs(args, ['--no-simulate']);
const new_args = [...temp_args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
@@ -573,7 +597,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
logger.error(error_message);
if (download_uid) {
const download = await db_api.getRecord('download_queue', {uid: download_uid});
await handleDownloadError(download, error_message);
await handleDownloadError(download, error_message, 'info_retrieve_failed');
}
resolve(null);
}

View File

@@ -10,7 +10,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."
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || 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 gosu "$UID:$GID" "$0" "$@"
fi

350
backend/files.js Normal file
View File

@@ -0,0 +1,350 @@
const fs = require('fs-extra')
const path = require('path')
const { uuid } = require('uuidv4');
const config_api = require('./config');
const db_api = require('./db');
const archive_api = require('./archive');
const utils = require('./utils')
const logger = require('./logger');
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.fixVideoMetadataPerms(file_path, type);
// add thumbnail path
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']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (user_uid) file_object['user_uid'] = user_uid;
if (sub_id) file_object['sub_id'] = sub_id;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_path, type)
}
return file_obj;
}
async function registerFileDBManual(file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
const path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
await db_api.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object;
}
function generateFileObject(file_path, type) {
const jsonobj = utils.getJSON(file_path, type);
if (!jsonobj) {
return null;
} else if (!jsonobj['_filename']) {
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
return null;
}
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
// console.
const stats = fs.statSync(true_file_path);
const file_id = utils.removeFileExtension(path.basename(file_path));
const title = jsonobj.title;
const url = jsonobj.webpage_url;
const uploader = jsonobj.uploader;
const upload_date = utils.formatDateString(jsonobj.upload_date);
const size = stats.size;
const thumbnail = jsonobj.thumbnail;
const duration = jsonobj.duration;
const isaudio = type === 'audio';
const description = jsonobj.description;
const file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
exports.importUnregisteredFiles = async () => {
const imported_files = [];
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
// run through check list and check each file to see if it's missing from the db
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
for (let j = 0; j < files.length; j++) {
const file = files[j];
// check if file exists in db, if not add it
const files_with_same_url = await db_api.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
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.`);
}
}
}
}
return imported_files;
}
exports.addMetadataPropertyToDB = async (property_key) => {
try {
const dirs_to_check = await db_api.getFileDirectoriesAndDBs();
const update_obj = {};
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
for (let j = 0; j < files.length; j++) {
const file = files[j];
if (file[property_key]) {
update_obj[file.uid] = {[property_key]: file[property_key]};
}
}
}
return await db_api.bulkUpdateRecordsByKey('files', 'uid', update_obj);
} catch(err) {
logger.error(err);
return false;
}
}
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL'];
let new_playlist = {
name: playlist_name,
uids: uids,
id: uuid(),
thumbnailURL: thumbnailToUse,
registered: Date.now(),
randomize_order: false
};
new_playlist.user_uid = user_uid ? user_uid : undefined;
await db_api.insertRecordIntoTable('playlists', new_playlist);
const duration = await exports.calculatePlaylistDuration(new_playlist);
await db_api.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
return new_playlist;
}
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
let playlist = await db_api.getRecord('playlists', {id: playlist_id});
if (!playlist) {
playlist = await db_api.getRecord('categories', {uid: playlist_id});
if (playlist) {
const uids = (await db_api.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid);
playlist['uids'] = uids;
playlist['auto'] = true;
}
}
// converts playlists to new UID-based schema
if (playlist && playlist['fileNames'] && !playlist['uids']) {
playlist['uids'] = [];
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
for (let i = 0; i < playlist['fileNames'].length; i++) {
const fileName = playlist['fileNames'][i];
const uid = await exports.getVideoUIDByID(fileName, user_uid);
if (uid) playlist['uids'].push(uid);
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
}
exports.updatePlaylist(playlist, user_uid);
}
// prevent unauthorized users from accessing the file info
if (require_sharing && !playlist['sharingEnabled']) return null;
return playlist;
}
exports.updatePlaylist = async (playlist) => {
let playlistID = playlist.id;
const duration = await exports.calculatePlaylistDuration(playlist);
playlist.duration = duration;
return await db_api.updateRecord('playlists', {id: playlistID}, playlist);
}
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
let success = await db_api.updateRecord('playlists', {id: playlist_id}, assignment_obj);
if (!success) {
success = await db_api.updateRecord('categories', {uid: playlist_id}, assignment_obj);
}
if (!success) {
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
}
return success;
}
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
if (!playlist_file_objs) {
playlist_file_objs = [];
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const file_obj = await exports.getVideo(uid);
if (file_obj) playlist_file_objs.push(file_obj);
}
}
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
}
exports.deleteFile = async (uid, blacklistMode = false) => {
const file_obj = await exports.getVideo(uid);
const type = file_obj.isAudio ? 'audio' : 'video';
const folderPath = path.dirname(file_obj.path);
const name = file_obj.id;
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
var jsonPath = `${file_obj.path}.info.json`;
var altJSONPath = `${filePathNoExtension}.info.json`;
var thumbnailPath = `${filePathNoExtension}.webp`;
var altThumbnailPath = `${filePathNoExtension}.jpg`;
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
let jsonExists = await fs.pathExists(jsonPath);
let thumbnailExists = await fs.pathExists(thumbnailPath);
if (!jsonExists) {
if (await fs.pathExists(altJSONPath)) {
jsonExists = true;
jsonPath = altJSONPath;
}
}
if (!thumbnailExists) {
if (await fs.pathExists(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
let fileExists = await fs.pathExists(file_obj.path);
if (config_api.descriptors[uid]) {
try {
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
config_api.descriptors[uid][i].destroy();
}
} catch(e) {
}
}
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive || file_obj.sub_id) {
// get id/extractor from JSON
const info_json = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
let retrievedID = null;
let retrievedExtractor = null;
if (info_json) {
retrievedID = info_json['id'];
retrievedExtractor = info_json['extractor'];
}
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
if (!blacklistMode) {
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id)
} else {
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, type, file_obj.user_uid, file_obj.sub_id);
if (!exists_in_archive) {
await archive_api.addToArchive(retrievedExtractor, retrievedID, type, file_obj.title, file_obj.user_uid, file_obj.sub_id);
}
}
}
if (jsonExists) await fs.unlink(jsonPath);
if (thumbnailExists) await fs.unlink(thumbnailPath);
await db_api.removeRecord('files', {uid: uid});
if (fileExists) {
await fs.unlink(file_obj.path);
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
return false;
} else {
return true;
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
}
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
exports.getVideoUIDByID = async (file_id, uuid = null) => {
const file_obj = await db_api.getRecord('files', {id: file_id});
return file_obj ? file_obj['uid'] : null;
}
exports.getVideo = async (file_uid) => {
return await db_api.getRecord('files', {uid: file_uid});
}
exports.getAllFiles = async (sort, range, text_search, file_type_filter, favorite_filter, sub_id, uuid) => {
const filter_obj = {user_uid: uuid};
const regex = true;
if (text_search) {
if (regex) {
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
} else {
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
}
}
if (favorite_filter) {
filter_obj['favorite'] = true;
}
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
const files = JSON.parse(JSON.stringify(await db_api.getRecords('files', filter_obj, false, sort, range, text_search)));
const file_count = await db_api.getRecords('files', filter_obj, true);
return {files, file_count};
}

View File

@@ -2,12 +2,16 @@ const db_api = require('./db');
const config_api = require('./config');
const logger = require('./logger');
const utils = require('./utils');
const consts = require('./consts');
const { uuid } = require('uuidv4');
const fetch = require('node-fetch');
const { gotify } = require("gotify");
const TelegramBot = require('node-telegram-bot-api');
const REST = require('@discordjs/rest').REST;
const API = require('@discordjs/core').API;
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
const NOTIFICATION_TYPE_TO_TITLE = {
task_finished: 'Task finished',
@@ -18,7 +22,7 @@ const NOTIFICATION_TYPE_TO_TITLE = {
const NOTIFICATION_TYPE_TO_BODY = {
task_finished: (notification) => notification['data']['task_title'],
download_complete: (notification) => {return `${notification['data']['file_title']}\nOriginal URL: ${notification['data']['original_url']}`},
download_error: (notification) => {return `Error: ${notification['data']['download_error_type']}\nURL: ${notification['data']['download_url']}`}
download_error: (notification) => {return `Error: ${notification['data']['download_error_message']}\nError code: ${notification['data']['download_error_type']}\n\nOriginal URL: ${notification['data']['download_url']}`}
}
const NOTIFICATION_TYPE_TO_URL = {
@@ -36,20 +40,31 @@ const NOTIFICATION_TYPE_TO_THUMBNAIL = {
exports.sendNotification = async (notification) => {
// info necessary if we are using 3rd party APIs
const type = notification['type'];
const title = NOTIFICATION_TYPE_TO_TITLE[type];
const body = NOTIFICATION_TYPE_TO_BODY[type](notification);
const url = NOTIFICATION_TYPE_TO_URL[type](notification);
const thumbnail = NOTIFICATION_TYPE_TO_THUMBNAIL[type](notification);
const data = {
title: NOTIFICATION_TYPE_TO_TITLE[type],
body: NOTIFICATION_TYPE_TO_BODY[type](notification),
type: type,
url: NOTIFICATION_TYPE_TO_URL[type](notification),
thumbnail: NOTIFICATION_TYPE_TO_THUMBNAIL[type](notification)
}
if (config_api.getConfigItem('ytdl_use_ntfy_API') && config_api.getConfigItem('ytdl_ntfy_topic_url')) {
sendNtfyNotification(body, title, type, url, thumbnail);
sendNtfyNotification(data);
}
if (config_api.getConfigItem('ytdl_use_gotify_API') && config_api.getConfigItem('ytdl_gotify_server_url') && config_api.getConfigItem('ytdl_gotify_app_token')) {
sendGotifyNotification(body, title, type, url, thumbnail);
sendGotifyNotification(data);
}
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
sendTelegramNotification(body, title, type, url, thumbnail);
sendTelegramNotification(data);
}
if (config_api.getConfigItem('ytdl_webhook_url')) {
sendGenericNotification(data);
}
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
sendDiscordNotification(data);
}
await db_api.insertRecordIntoTable('notifications', notification);
return notification;
}
@@ -71,9 +86,9 @@ exports.sendDownloadNotification = async (file, user_uid) => {
return await exports.sendNotification(notification);
}
exports.sendDownloadErrorNotification = async (download, user_uid, error_type = null) => {
exports.sendDownloadErrorNotification = async (download, user_uid, error_message, error_type = null) => {
if (!notificationEnabled('download_error')) return;
const data = {download_uid: download.uid, download_url: download.url, download_error_type: error_type};
const data = {download_uid: download.uid, download_url: download.url, download_error_message: error_message, download_error_type: error_type};
const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid);
return await exports.sendNotification(notification);
}
@@ -95,7 +110,7 @@ function notificationEnabled(type) {
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
}
function sendNtfyNotification(body, title, type, url, thumbnail) {
function sendNtfyNotification({body, title, type, url, thumbnail}) {
logger.verbose('Sending notification to ntfy');
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
method: 'POST',
@@ -109,7 +124,7 @@ function sendNtfyNotification(body, title, type, url, thumbnail) {
});
}
async function sendGotifyNotification(body, title, type, url, thumbnail) {
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
logger.verbose('Sending notification to gotify');
await gotify({
server: config_api.getConfigItem('ytdl_gotify_server_url'),
@@ -127,11 +142,46 @@ async function sendGotifyNotification(body, title, type, url, thumbnail) {
});
}
async function sendTelegramNotification(body, title, type, url, thumbnail) {
async function sendTelegramNotification({body, title, type, url, thumbnail}) {
logger.verbose('Sending notification to Telegram');
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
const bot = new TelegramBot(bot_token);
if (thumbnail) await bot.sendPhoto(chat_id, thumbnail);
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
}
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
const url_split = discord_webhook_url.split('webhooks/');
const [webhook_id, webhook_token] = url_split[1].split('/');
const rest = new REST({ version: '10' });
const api = new API(rest);
const embed = new EmbedBuilder()
.setTitle(title)
.setColor(0x00FFFF)
.setURL(url)
.setDescription(`ID: ${type}`);
if (thumbnail) embed.setThumbnail(thumbnail);
if (type === 'download_error') embed.setColor(0xFC2003);
const result = await api.webhooks.execute(webhook_id, webhook_token, {
content: body,
username: 'YoutubeDL-Material',
avatar_url: consts.ICON_URL,
embeds: [embed],
});
return result;
}
function sendGenericNotification(data) {
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
logger.verbose(`Sending generic notification to ${webhook_url}`);
fetch(webhook_url, {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data),
});
}

View File

@@ -19,6 +19,101 @@
"kuler": "^2.0.0"
}
},
"@discordjs/builders": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.1.tgz",
"integrity": "sha512-CCcLwn/8ANhlAbhlE18fcaN0hfXTen53/JiwZs1t9oE/Cqa9maA8ZRarkCIsXF4J7J/MYnd0J6IsxeKsq+f6mw==",
"requires": {
"@discordjs/formatters": "^0.3.0",
"@discordjs/util": "^0.2.0",
"@sapphire/shapeshift": "^3.8.1",
"discord-api-types": "^0.37.37",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.3",
"tslib": "^2.5.0"
},
"dependencies": {
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}
}
},
"@discordjs/collection": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.0.tgz",
"integrity": "sha512-suyVndkEAAWrGxyw/CPGdtXoRRU6AUNkibtnbJevQzpelkJh3Q1gQqWDpqf5i39CnAn5+LrN0YS+cULeEjq2Yw=="
},
"@discordjs/core": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@discordjs/core/-/core-0.5.2.tgz",
"integrity": "sha512-OEgK8GYNB1IJK3nPQ3QBvNUmuvlPTitc0j9oXe801Z7xWOFwL/lePAGhd6cAFH7yYaslwhCoSh85KI9glrmjNQ==",
"requires": {
"@discordjs/rest": "^1.7.0",
"@discordjs/util": "^0.2.0",
"@discordjs/ws": "^0.8.1",
"@sapphire/snowflake": "^3.4.2",
"@vladfrangu/async_event_emitter": "^2.2.1",
"discord-api-types": "^0.37.38"
}
},
"@discordjs/formatters": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.0.tgz",
"integrity": "sha512-Fc4MomalbP8HMKEMor3qUiboAKDtR7PSBoPjwm7WYghVRwgJlj5WYvUsriLsxeKk8+Qq2oy+HJlGTUkGvX0YnA==",
"requires": {
"discord-api-types": "^0.37.37"
}
},
"@discordjs/rest": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.7.0.tgz",
"integrity": "sha512-r2HzmznRIo8IDGYBWqQfkEaGN1LrFfWQd3dSyC4tOpMU8nuVvFUEw6V/lwnG44jyOq+vgyDny2fxeUDMt9I4aQ==",
"requires": {
"@discordjs/collection": "^1.5.0",
"@discordjs/util": "^0.2.0",
"@sapphire/async-queue": "^1.5.0",
"@sapphire/snowflake": "^3.4.0",
"discord-api-types": "^0.37.37",
"file-type": "^18.2.1",
"tslib": "^2.5.0",
"undici": "^5.21.0"
},
"dependencies": {
"file-type": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-18.3.0.tgz",
"integrity": "sha512-pkPZ5OGIq0TYb37b8bHDLNeQSe1H2KlaQ2ySGpJkkr2KZdaWsO4QhPzHA0mQcsUW2cSqJk+4gM/UyLz/UFbXdQ==",
"requires": {
"readable-web-to-node-stream": "^3.0.2",
"strtok3": "^7.0.0",
"token-types": "^5.0.1"
}
}
}
},
"@discordjs/util": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.2.0.tgz",
"integrity": "sha512-/8qNbebFzLWKOOg+UV+RB8itp4SmU5jw0tBUD3ifElW6rYNOj1Ku5JaSW7lLl/WgjjxF01l/1uQPCzkwr110vg=="
},
"@discordjs/ws": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-0.8.1.tgz",
"integrity": "sha512-RZwlluBGmrgAgvTHP8w9IW7Kp/idWEQgSHBs5h0ecqiWGVCueoIr6jMmvbxqZ7vVirric3zRhNdmG/TNRxhWLg==",
"requires": {
"@discordjs/collection": "^1.5.0",
"@discordjs/rest": "^1.7.0",
"@discordjs/util": "^0.2.0",
"@sapphire/async-queue": "^1.5.0",
"@types/ws": "^8.5.4",
"@vladfrangu/async_event_emitter": "^2.2.1",
"discord-api-types": "^0.37.38",
"tslib": "^2.5.0",
"ws": "^8.13.0"
}
},
"@oozcitak/dom": {
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
@@ -51,6 +146,32 @@
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
},
"@sapphire/async-queue": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz",
"integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA=="
},
"@sapphire/shapeshift": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.2.tgz",
"integrity": "sha512-NXpnJAsxN3/h9TqQPntOeVWZrpIuucqXI3IWF6tj2fWCoRLCuVK5wx7Dtg7pRrtkYfsMUbDqgKoX26vrC5iYfA==",
"requires": {
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
},
"dependencies": {
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}
}
},
"@sapphire/snowflake": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.2.tgz",
"integrity": "sha512-KJwlv5gkGjs1uFV7/xx81n3tqgBwBJvH94n1xDyH3q+JSmtsMeSleJffarEBfG2yAFeJiFA4BnGOK6FFPHc19g=="
},
"@sindresorhus/is": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz",
@@ -64,6 +185,11 @@
"defer-to-connect": "^2.0.0"
}
},
"@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -109,11 +235,29 @@
"@types/node": "*"
}
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
},
"@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
"requires": {
"@types/node": "*"
}
},
"@ungap/promise-all-settled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
},
"@vladfrangu/async_event_emitter": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.1.tgz",
"integrity": "sha512-XtUEAS0m6uVddXW+EImGunLiJZzWNWAZQBoQCUneowrYXPQ6y7c0iWEm/wVYyGpTixTIhUfLRSoYCwojL64htA=="
},
"abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@@ -255,7 +399,7 @@
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"array.prototype.findindex": {
"version": "2.2.1",
@@ -287,11 +431,11 @@
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"async-mutex": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.1.tgz",
"integrity": "sha512-vRfQwcqBnJTLzVQo72Sf7KIUbcSUP5hNchx6udI1U6LuPQpfePgdjJzlCe76yFZ8pxlLjn9lwcl/Ya0TSOv0Tw==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz",
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==",
"requires": {
"tslib": "^2.1.0"
"tslib": "^2.4.0"
}
},
"asynckit": {
@@ -393,20 +537,22 @@
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
},
"body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.8.1",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.9.7",
"raw-body": "2.4.3",
"type-is": "~1.6.18"
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"dependencies": {
"bytes": {
@@ -717,9 +863,9 @@
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
},
"cookie": {
"version": "0.4.2",
@@ -828,20 +974,25 @@
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
},
"discord-api-types": {
"version": "0.37.40",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.40.tgz",
"integrity": "sha512-LMALvtO+p6ERK8rwWoaI490NfIE/egbqjR4/rfLL1z9gQE1gqLiTpIUUDIunfAtKYzeH6ucyXhaXXWpfZh/Q6g=="
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -891,7 +1042,7 @@
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"enabled": {
"version": "2.0.0",
@@ -901,7 +1052,7 @@
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"end-of-stream": {
"version": "1.4.4",
@@ -1012,7 +1163,7 @@
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"escape-string-regexp": {
"version": "4.0.0",
@@ -1027,7 +1178,7 @@
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"eventemitter3": {
"version": "3.1.2",
@@ -1052,37 +1203,38 @@
}
},
"express": {
"version": "4.17.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
"integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.19.2",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.2",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.9.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
@@ -1097,6 +1249,11 @@
"negotiator": "0.6.3"
}
},
"cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1122,6 +1279,33 @@
}
}
},
"express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"requires": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"dependencies": {
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -1169,16 +1353,16 @@
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
}
},
@@ -1256,7 +1440,7 @@
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"fs-constants": {
"version": "1.0.0",
@@ -1521,19 +1705,19 @@
}
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"requires": {
"depd": "~1.1.2",
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
@@ -2139,9 +2323,9 @@
}
},
"luxon": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ=="
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
"integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw=="
},
"md5": {
"version": "2.2.1",
@@ -2167,15 +2351,7 @@
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"merge-files": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/merge-files/-/merge-files-0.1.2.tgz",
"integrity": "sha512-WTvtH6ZwVy1/scvp1M+Re6PVni87QTjpSLAwxh0L+PlYIxc4VGFFpLjvP7jdJ43gaJ5n+RUIriJ6wKqmqvVVmg==",
"requires": {
"multistream": "^2.1.0"
}
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"merge-stream": {
"version": "2.0.0",
@@ -2185,7 +2361,7 @@
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
},
"mime": {
"version": "1.6.0",
@@ -2428,31 +2604,6 @@
"xtend": "^4.0.0"
}
},
"multistream": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz",
"integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==",
"requires": {
"inherits": "^2.0.1",
"readable-stream": "^2.0.5"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -2612,9 +2763,9 @@
}
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
@@ -2802,12 +2953,17 @@
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"peek-readable": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
"integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A=="
},
"performance-now": {
"version": "2.1.0",
@@ -2876,9 +3032,17 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.4"
}
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
},
"randombytes": {
"version": "2.1.0",
@@ -2894,12 +3058,12 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
"integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"requires": {
"bytes": "3.1.2",
"http-errors": "1.8.1",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
@@ -2929,6 +3093,14 @@
"util-deprecate": "^1.0.1"
}
},
"readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"requires": {
"readable-stream": "^3.6.0"
}
},
"readdir-glob": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
@@ -2938,13 +3110,24 @@
}
},
"regexp.prototype.flags": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
"integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"functions-have-names": "^1.2.2"
"define-properties": "^1.2.0",
"functions-have-names": "^1.2.3"
},
"dependencies": {
"define-properties": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
"requires": {
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
}
}
}
},
"request": {
@@ -3110,23 +3293,23 @@
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "1.8.1",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
"statuses": "2.0.1"
},
"dependencies": {
"ms": {
@@ -3145,14 +3328,14 @@
}
},
"serve-static": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.2"
"send": "0.18.0"
}
},
"setimmediate": {
@@ -3250,9 +3433,9 @@
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"stealthy-require": {
"version": "1.1.1",
@@ -3358,6 +3541,15 @@
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
},
"strtok3": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz",
"integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==",
"requires": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^5.0.0"
}
},
"table-parser": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
@@ -3417,6 +3609,15 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"token-types": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz",
"integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
"requires": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
}
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@@ -3441,10 +3642,15 @@
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"ts-mixer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz",
"integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ=="
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"tunnel-agent": {
"version": "0.6.0",
@@ -3488,6 +3694,14 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@@ -3499,6 +3713,14 @@
"which-boxed-primitive": "^1.0.2"
}
},
"undici": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz",
"integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==",
"requires": {
"busboy": "^1.6.0"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
@@ -3507,7 +3729,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"unzipper": {
"version": "0.10.10",
@@ -3561,16 +3783,17 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz",
"integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw=="
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"uuidv4": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.0.6.tgz",
"integrity": "sha512-10YcruyGJtsG5SJnPG+8atr8toJa7xAOrcO7B7plYYiwpH1mQ8UZHjNSa2MrwGi6KWuyVrXGHr+Rce22F9UAiw==",
"version": "6.2.13",
"resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz",
"integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==",
"requires": {
"uuid": "7.0.2"
"@types/uuid": "8.3.4",
"uuid": "8.3.2"
}
},
"vary": {
@@ -3700,6 +3923,11 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",

View File

@@ -19,14 +19,17 @@
},
"homepage": "",
"dependencies": {
"@discordjs/builders": "^1.6.1",
"@discordjs/core": "^0.5.2",
"archiver": "^5.3.1",
"async": "^3.2.3",
"async-mutex": "^0.3.1",
"async-mutex": "^0.4.0",
"axios": "^0.21.2",
"bcryptjs": "^2.4.0",
"compression": "^1.7.4",
"config": "^3.2.3",
"express": "^4.17.3",
"express": "^4.18.2",
"express-session": "^1.17.3",
"feed": "^4.2.2",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
@@ -35,7 +38,6 @@
"lodash": "^4.17.21",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"merge-files": "^0.1.2",
"mocha": "^9.2.2",
"moment": "^2.29.4",
"mongodb": "^3.6.9",
@@ -55,7 +57,7 @@
"rxjs": "^7.3.0",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"uuidv4": "^6.2.13",
"winston": "^3.7.2",
"xmlbuilder2": "^3.0.2",
"youtube-dl": "^3.0.2"

View File

@@ -92,7 +92,10 @@ async function getSubscriptionInfo(sub) {
}
// if it's now valid, update
if (sub.name) {
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub.name});
let sub_name = sub.name;
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
if (sub_name_exists) sub_name += ` - ${sub.id}`;
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
}
}
@@ -196,8 +199,13 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
return false;
} else {
// check if the user wants the video to be redownloaded (deleteForever === false)
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useArchive && !deleteForever) {
if (deleteForever) {
// ensure video is in the archives
const exists_in_archive = await archive_api.existsInArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
if (!exists_in_archive) {
await archive_api.addToArchive(retrievedExtractor, retrievedID, sub.type, file.title, user_uid, sub.id);
}
} else {
await archive_api.removeFromArchive(retrievedExtractor, retrievedID, sub.type, user_uid, sub.id);
}
return true;
@@ -229,13 +237,20 @@ async function getVideosForSub(sub, user_uid = null) {
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
// get videos
logger.verbose(`Subscription: getting videos for subscription ${sub.name} with args: ${downloadConfig.join(',')}`);
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
return new Promise(async resolve => {
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
// cleanup
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
// remove temporary archive file if it exists
const archive_path = path.join(appendedBasePath, 'archive.txt');
const archive_exists = await fs.pathExists(archive_path);
if (archive_exists) {
await fs.unlink(archive_path);
}
logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message);
@@ -354,6 +369,13 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push(...qualityPath)
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
const archive_path = path.join(appendedBasePath, 'archive.txt');
await fs.writeFile(archive_path, archive_text);
downloadConfig.push('--download-archive', archive_path);
if (sub.custom_args) {
const customArgsArray = sub.custom_args.split(',,');
if (customArgsArray.indexOf('-f') !== -1) {
@@ -408,11 +430,8 @@ async function getFilesToDownload(sub, output_jsons) {
logger.info(`Skipping adding file ${output_json['_filename']} for subscription ${sub.name} as a file with that path already exists.`)
continue;
}
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
if (exists_in_archive) continue;
}
const exists_in_archive = await archive_api.existsInArchive(output_json['extractor'], output_json['id'], sub.type, sub.user_uid, sub.id);
if (exists_in_archive) continue;
files_to_download.push(output_json);
}

View File

@@ -2,6 +2,7 @@ const db_api = require('./db');
const notifications_api = require('./notifications');
const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive');
const files_api = require('./files');
const fs = require('fs-extra');
const logger = require('./logger');
@@ -20,7 +21,7 @@ const TASKS = {
job: null
},
missing_db_records: {
run: db_api.importUnregisteredFiles,
run: files_api.importUnregisteredFiles,
title: 'Import missing DB records',
job: null
},
@@ -101,7 +102,7 @@ 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 mergedDefaultOptions = Object.assign(defaultOptions['all'], defaultOptions[task_key] || {});
const mergedDefaultOptions = Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {});
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
if (!task_in_db) {
// insert task metadata into table if missing, eventually move title to UI
@@ -115,14 +116,16 @@ exports.setupTasks = async () => {
data: null,
error: null,
schedule: null,
options: Object.assign(defaultOptions['all'], defaultOptions[task_key] || {})
options: Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {})
});
} else {
// verify all options exist in task
for (const key of Object.keys(mergedDefaultOptions)) {
const option_key = `options.${key}`;
// Remove any potential mangled option keys (#861)
await db_api.removePropertyFromRecord('tasks', {key: task_key}, {[option_key]: true});
if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) {
const option_key = `options.${key}`
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]});
await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}, true);
}
}
@@ -257,7 +260,7 @@ async function autoDeleteFiles(data) {
logger.info(`Removing ${data['files_to_remove'].length} old files!`);
for (let i = 0; i < data['files_to_remove'].length; i++) {
const file_to_remove = data['files_to_remove'][i];
await db_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files']));
await files_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files']));
}
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
const assert = require('assert');
const low = require('lowdb')
const winston = require('winston');
@@ -38,6 +39,8 @@ var db_api = require('../db');
const utils = require('../utils');
const subscriptions_api = require('../subscriptions');
const archive_api = require('../archive');
const categories_api = require('../categories');
const files_api = require('../files');
const fs = require('fs-extra');
const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
@@ -175,6 +178,24 @@ describe('Database', async function() {
await db_api.removeRecord('test', {test_update: 'test'});
});
it('Update records', async function() {
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test1'});
await db_api.insertRecordIntoTable('test', {test_update: 'test', key: 'test2'});
await db_api.updateRecords('test', {test_update: 'test'}, {added_field: true});
const updated_records = await db_api.getRecords('test', {added_field: true});
assert(updated_records.length === 2);
await db_api.removeRecord('test', {test_update: 'test'});
});
it('Remove property from record', async function() {
await db_api.insertRecordIntoTable('test', {test_keep: 'test', test_remove: 'test'});
await db_api.removePropertyFromRecord('test', {test_keep: 'test'}, {test_remove: true});
const updated_record = await db_api.getRecord('test', {test_keep: 'test'});
assert(updated_record['test_keep']);
assert(!updated_record['test_remove']);
await db_api.removeRecord('test', {test_keep: 'test'});
});
it('Remove record', async function() {
await db_api.insertRecordIntoTable('test', {test_remove: 'test'});
const delete_succeeded = await db_api.removeRecord('test', {test_remove: 'test'});
@@ -330,11 +351,13 @@ describe('Multi User', async function() {
});
});
describe('Video player - normal', async function() {
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
await db_api.insertRecordIntoTable('files', sample_video_json);
beforeEach(async function() {
await db_api.removeRecord('files', {uid: sample_video_json['uid']});
await db_api.insertRecordIntoTable('files', sample_video_json);
});
const video_to_test = sample_video_json['uid'];
it('Get video', async function() {
const video_obj = await db_api.getVideo(video_to_test);
const video_obj = await files_api.getVideo(video_to_test);
assert(video_obj);
});
@@ -352,12 +375,12 @@ describe('Multi User', async function() {
});
describe('Zip generators', function() {
it('Playlist zip generator', async function() {
const playlist = await db_api.getPlaylist(playlist_to_test, user_to_test);
const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
assert(playlist);
const playlist_files_to_download = [];
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const playlist_file = await db_api.getVideo(uid, user_to_test);
const playlist_file = await files_api.getVideo(uid, user_to_test);
playlist_files_to_download.push(playlist_file);
}
const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download);
@@ -385,7 +408,7 @@ describe('Multi User', async function() {
// const sub_to_test = '';
// const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
// it('Get video', async function() {
// const video_obj = db_api.getVideo(video_to_test, 'admin', );
// const video_obj = files_api.getVideo(video_to_test, 'admin', );
// assert(video_obj);
// });
@@ -488,18 +511,23 @@ describe('Downloader', function() {
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));
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));
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'];
assert(JSON.stringify(updated_args2) === JSON.stringify(expected_args2));
const original_args3 = ['-o', '%(title)s.%(ext)s'];
const new_args3 = ['--min-filesize','1'];
const updated_args3 = utils.injectArgs(original_args3, new_args3);
const expected_args3 = ['-o', '%(title)s.%(ext)s', '--min-filesize', '1'];
assert(JSON.stringify(updated_args3) === JSON.stringify(expected_args3));
});
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1493770675';
const example_vod = '1710641401';
it('Download VOD', async function() {
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
@@ -581,7 +609,7 @@ describe('Tasks', function() {
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);
assert(!!imported_file === true);
// post-test cleanup
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
@@ -699,4 +727,129 @@ describe('Utils', async function() {
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
});
it('Convert flat object to nested object', async function() {
// No modfication
const flat_obj0 = {'test1': {'test_sub': true}, 'test2': {test_sub: true}};
const nested_obj0 = utils.convertFlatObjectToNestedObject(flat_obj0);
assert(nested_obj0['test1'] && nested_obj0['test1']['test_sub']);
assert(nested_obj0['test2'] && nested_obj0['test2']['test_sub']);
// Standard setup
const flat_obj1 = {'test1.test_sub': true, 'test2.test_sub': true};
const nested_obj1 = utils.convertFlatObjectToNestedObject(flat_obj1);
assert(nested_obj1['test1'] && nested_obj1['test1']['test_sub']);
assert(nested_obj1['test2'] && nested_obj1['test2']['test_sub']);
// Nested branches
const flat_obj2 = {'test1.test_sub': true, 'test1.test2.test_sub': true};
const nested_obj2 = utils.convertFlatObjectToNestedObject(flat_obj2);
assert(nested_obj2['test1'] && nested_obj2['test1']['test_sub']);
assert(nested_obj2['test1'] && nested_obj2['test1']['test2'] && nested_obj2['test1']['test2']['test_sub']);
});
});
describe('Categories', async function() {
beforeEach(async function() {
await db_api.connectToDB();
const new_category = {
name: 'test_category',
uid: uuid(),
rules: [],
custom_output: ''
};
await db_api.insertRecordIntoTable('categories', new_category);
});
afterEach(async function() {
await db_api.removeAllRecords('categories', {name: 'test_category'});
});
it('Categorize - includes', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(category && category.name === 'test_category');
});
it('Categorize - not includes', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'not_includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(!category);
});
it('Categorize - equals', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'equals',
property: 'uploader',
value: 'Sample Uploader'
});
const category = await categories_api.categorize([sample_video_json]);
console.log(category);
assert(category && category.name === 'test_category');
});
it('Categorize - not equals', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'not_equals',
property: 'uploader',
value: 'Sample Uploader'
});
const category = await categories_api.categorize([sample_video_json]);
assert(!category);
});
it('Categorize - AND', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'equals',
property: 'uploader',
value: 'Sample Uploader'
});
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: 'and',
comparator: 'not_includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(!category);
});
it('Categorize - OR', async function() {
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: null,
comparator: 'equals',
property: 'uploader',
value: 'Sample Uploader'
});
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
preceding_operator: 'or',
comparator: 'not_includes',
property: 'title',
value: 'Sample'
});
const category = await categories_api.categorize([sample_video_json]);
assert(category);
});
});

View File

@@ -4,19 +4,28 @@ const logger = require('./logger');
const moment = require('moment');
const fs = require('fs-extra')
const path = require('path');
const { promisify } = require('util');
const child_process = require('child_process');
async function getCommentsForVOD(clientID, clientSecret, vodId) {
const { promisify } = require('util');
const child_process = require('child_process');
async function getCommentsForVOD(vodId) {
const exec = promisify(child_process.exec);
// Reject invalid params to prevent command injection attack
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) {
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!');
if (!vodId.match(/^[0-9a-z]+$/)) {
logger.error('VOD ID must be purely alphanumeric. Twitch chat download failed!');
return null;
}
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]});
const is_windows = process.platform === 'win32';
const cliExt = is_windows ? '.exe' : ''
const cliPath = `TwitchDownloaderCLI${cliExt}`
if (!fs.existsSync(cliPath)) {
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
return null;
}
const result = await exec(`TwitchDownloaderCLI chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
if (result['stderr']) {
logger.error(`Failed to download twitch comments for ${vodId}`);
@@ -73,9 +82,7 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
const chat = await getCommentsForVOD(vodId);
// save file if needed params are included
let file_path = null;

View File

@@ -4,6 +4,7 @@ const ffmpeg = require('fluent-ffmpeg');
const archiver = require('archiver');
const fetch = require('node-fetch');
const ProgressBar = require('progress');
const winston = require('winston');
const config_api = require('./config');
const logger = require('./logger');
@@ -12,7 +13,7 @@ const CONSTS = require('./consts');
const is_windows = process.platform === 'win32';
// replaces .webm with appropriate extension
function getTrueFileName(unfixed_path, type) {
exports.getTrueFileName = (unfixed_path, type) => {
let fixed_path = unfixed_path;
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
@@ -27,13 +28,13 @@ function getTrueFileName(unfixed_path, type) {
return fixed_path;
}
async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
exports.getDownloadedFilesByType = async (basePath, type, full_metadata = false) => {
// return empty array if the path doesn't exist
if (!(await fs.pathExists(basePath))) return [];
let files = [];
const ext = type === 'audio' ? 'mp3' : 'mp4';
var located_files = await recFindByExt(basePath, ext);
var located_files = await exports.recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) {
let file = located_files[i];
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
@@ -41,33 +42,33 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
var stats = await fs.stat(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = await getJSONByType(type, id, basePath);
var jsonobj = await exports.getJSONByType(type, id, basePath);
if (!jsonobj) continue;
if (full_metadata) {
jsonobj['id'] = id;
files.push(jsonobj);
continue;
}
var upload_date = formatDateString(jsonobj.upload_date);
var upload_date = exports.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,
var file_obj = new exports.File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
stats.size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
files.push(file_obj);
}
return files;
}
async function createContainerZipFile(file_name, container_file_objs) {
exports.createContainerZipFile = async (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', file_name + '.zip'), container_files_to_download);
return await exports.createZipFile(path.join('appdata', file_name + '.zip'), container_files_to_download);
}
async function createZipFile(zip_file_path, file_paths) {
exports.createZipFile = async (zip_file_path, file_paths) => {
let output = fs.createWriteStream(zip_file_path);
var archive = archiver('zip', {
@@ -91,11 +92,11 @@ async function createZipFile(zip_file_path, file_paths) {
await archive.finalize();
// wait a tiny bit for the zip to reload in fs
await wait(100);
await exports.wait(100);
return zip_file_path;
}
function getJSONMp4(name, customPath, openReadPerms = false) {
exports.getJSONMp4 = (name, customPath, openReadPerms = false) => {
var obj = null; // output
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
var jsonPath = path.join(customPath, name + ".info.json");
@@ -110,7 +111,7 @@ function getJSONMp4(name, customPath, openReadPerms = false) {
return obj;
}
function getJSONMp3(name, customPath, openReadPerms = false) {
exports.getJSONMp3 = (name, customPath, openReadPerms = false) => {
var obj = null;
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
var jsonPath = path.join(customPath, name + ".info.json");
@@ -127,11 +128,11 @@ function getJSONMp3(name, customPath, openReadPerms = false) {
return obj;
}
function getJSON(file_path, type) {
exports.getJSON = (file_path, type) => {
const ext = type === 'audio' ? '.mp3' : '.mp4';
let obj = null;
var jsonPath = removeFileExtension(file_path) + '.info.json';
var alternateJsonPath = removeFileExtension(file_path) + `${ext}.info.json`;
var jsonPath = exports.removeFileExtension(file_path) + '.info.json';
var alternateJsonPath = exports.removeFileExtension(file_path) + `${ext}.info.json`;
if (fs.existsSync(jsonPath))
{
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
@@ -142,12 +143,12 @@ function getJSON(file_path, type) {
return obj;
}
function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
exports.getJSONByType = (type, name, customPath, openReadPerms = false) => {
return type === 'audio' ? exports.getJSONMp3(name, customPath, openReadPerms) : exports.getJSONMp4(name, customPath, openReadPerms)
}
function getDownloadedThumbnail(file_path) {
const file_path_no_extension = removeFileExtension(file_path);
exports.getDownloadedThumbnail = (file_path) => {
const file_path_no_extension = exports.removeFileExtension(file_path);
let jpgPath = file_path_no_extension + '.jpg';
let webpPath = file_path_no_extension + '.webp';
@@ -163,7 +164,7 @@ function getDownloadedThumbnail(file_path) {
return null;
}
function getExpectedFileSize(input_info_jsons) {
exports.getExpectedFileSize = (input_info_jsons) => {
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
@@ -186,12 +187,12 @@ function getExpectedFileSize(input_info_jsons) {
return expected_filesize;
}
function fixVideoMetadataPerms(file_path, type) {
exports.fixVideoMetadataPerms = (file_path, type) => {
if (is_windows) return;
const ext = type === 'audio' ? '.mp3' : '.mp4';
const file_path_no_extension = removeFileExtension(file_path);
const file_path_no_extension = exports.removeFileExtension(file_path);
const files_to_fix = [
// JSONs
@@ -208,10 +209,10 @@ function fixVideoMetadataPerms(file_path, type) {
}
}
function deleteJSONFile(file_path, type) {
exports.deleteJSONFile = (file_path, type) => {
const ext = type === 'audio' ? '.mp3' : '.mp4';
const file_path_no_extension = removeFileExtension(file_path);
const file_path_no_extension = exports.removeFileExtension(file_path);
let json_path = file_path_no_extension + '.info.json';
let alternate_json_path = file_path_no_extension + ext + '.info.json';
@@ -220,7 +221,7 @@ function deleteJSONFile(file_path, type) {
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
function durationStringToNumber(dur_str) {
exports.durationStringToNumber = (dur_str) => {
if (typeof dur_str === 'number') return dur_str;
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
@@ -230,23 +231,22 @@ function durationStringToNumber(dur_str) {
return num_sum;
}
function getMatchingCategoryFiles(category, files) {
exports.getMatchingCategoryFiles = (category, files) => {
return files && files.filter(file => file.category && file.category.uid === category.uid);
}
function addUIDsToCategory(category, files) {
const files_that_match = getMatchingCategoryFiles(category, files);
exports.addUIDsToCategory = (category, files) => {
const files_that_match = exports.getMatchingCategoryFiles(category, files);
category['uids'] = files_that_match.map(file => file.uid);
return files_that_match;
}
function getCurrentDownloader() {
exports.getCurrentDownloader = () => {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
return details_json['downloader'];
}
async function recFindByExt(base, ext, files, result, recursive = true)
{
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
files = files || (await fs.readdir(base))
result = result || []
@@ -255,7 +255,7 @@ async function recFindByExt(base, ext, files, result, recursive = true)
if ( (await fs.stat(newbase)).isDirectory() )
{
if (!recursive) continue;
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
result = await exports.recFindByExt(newbase,ext,await fs.readdir(newbase),result)
}
else
{
@@ -268,17 +268,17 @@ async function recFindByExt(base, ext, files, result, recursive = true)
return result
}
function removeFileExtension(filename) {
exports.removeFileExtension = (filename) => {
const filename_parts = filename.split('.');
filename_parts.splice(filename_parts.length - 1);
return filename_parts.join('.');
}
function formatDateString(date_string) {
exports.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) {
exports.createEdgeNGrams = (str) => {
if (str && str.length > 3) {
const minGram = 3
const maxGram = str.length
@@ -300,7 +300,7 @@ function createEdgeNGrams(str) {
// ffmpeg helper functions
async function cropFile(file_path, start, end, ext) {
exports.cropFile = async (file_path, start, end, ext) => {
return new Promise(resolve => {
const temp_file_path = `${file_path}.cropped${ext}`;
let base_ffmpeg_call = ffmpeg(file_path);
@@ -329,13 +329,13 @@ async function cropFile(file_path, start, end, ext) {
* setTimeout, but its a promise.
* @param {number} ms
*/
async function wait(ms) {
exports.wait = async (ms) => {
await new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function checkExistsWithTimeout(filePath, timeout) {
exports.checkExistsWithTimeout = async (filePath, timeout) => {
return new Promise(function (resolve, reject) {
var timer = setTimeout(function () {
@@ -364,7 +364,7 @@ async function checkExistsWithTimeout(filePath, timeout) {
}
// helper function to download file using fetch
async function fetchFile(url, path, file_label) {
exports.fetchFile = async (url, path, file_label) => {
var len = null;
const res = await fetch(url);
@@ -391,7 +391,7 @@ async function fetchFile(url, path, file_label) {
});
}
async function restartServer(is_update = false) {
exports.restartServer = async (is_update = false) => {
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
// the following line restarts the server through pm2
@@ -404,7 +404,7 @@ async function restartServer(is_update = false) {
// - 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) {
exports.injectArgs = (original_args, new_args) => {
const updated_args = original_args.slice();
try {
for (let i = 0; i < new_args.length; i++) {
@@ -414,10 +414,11 @@ function injectArgs(original_args, new_args) {
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.splice(original_index, 2);
}
updated_args.push(new_arg, new_args[i + 1]);
i++; // we need to skip the arg value on the next loop
} else {
if (!original_args.includes(new_arg)) {
updated_args.push(new_arg);
@@ -432,11 +433,11 @@ function injectArgs(original_args, new_args) {
return updated_args;
}
function filterArgs(args, args_to_remove) {
exports.filterArgs = (args, args_to_remove) => {
return args.filter(x => !args_to_remove.includes(x));
}
const searchObjectByString = function(o, s) {
exports.searchObjectByString = (o, s) => {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
@@ -451,7 +452,7 @@ const searchObjectByString = function(o, s) {
return o;
}
function stripPropertiesFromObject(obj, properties, whitelist = false) {
exports.stripPropertiesFromObject = (obj, properties, whitelist = false) => {
if (!whitelist) {
const new_obj = JSON.parse(JSON.stringify(obj));
for (let field of properties) {
@@ -467,7 +468,7 @@ function stripPropertiesFromObject(obj, properties, whitelist = false) {
return new_obj;
}
function getArchiveFolder(type, user_uid = null, sub = null) {
exports.getArchiveFolder = (type, user_uid = null, sub = null) => {
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
const subsFolderPath = config_api.getConfigItem('ytdl_subscriptions_base_path');
@@ -486,10 +487,38 @@ function getArchiveFolder(type, user_uid = null, sub = null) {
}
}
function getBaseURL() {
exports.getBaseURL = () => {
return `${config_api.getConfigItem('ytdl_url')}:${config_api.getConfigItem('ytdl_port')}`
}
exports.updateLoggerLevel = (new_logger_level) => {
const possible_levels = ['error', 'warn', 'info', 'verbose', 'debug'];
if (!possible_levels.includes(new_logger_level)) {
logger.error(`${new_logger_level} is not a valid logger level! Choose one of the following: ${possible_levels.join(', ')}.`)
new_logger_level = 'info';
}
logger.level = new_logger_level;
winston.loggers.get('console').level = new_logger_level;
logger.transports[2].level = new_logger_level;
}
exports.convertFlatObjectToNestedObject = (obj) => {
const result = {};
for (const key in obj) {
const nestedKeys = key.split('.');
let currentObj = result;
for (let i = 0; i < nestedKeys.length; i++) {
if (i === nestedKeys.length - 1) {
currentObj[nestedKeys[i]] = obj[key];
} else {
currentObj[nestedKeys[i]] = currentObj[nestedKeys[i]] || {};
currentObj = currentObj[nestedKeys[i]];
}
}
}
return result;
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
@@ -508,36 +537,6 @@ function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, p
this.height = height;
this.abr = abr;
this.favorite = false;
}
}
exports.File = File;
module.exports = {
getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4,
getJSON: getJSON,
getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
getDownloadedFilesByType: getDownloadedFilesByType,
createContainerZipFile: createContainerZipFile,
durationStringToNumber: durationStringToNumber,
getMatchingCategoryFiles: getMatchingCategoryFiles,
getCurrentDownloader: getCurrentDownloader,
recFindByExt: recFindByExt,
removeFileExtension: removeFileExtension,
formatDateString: formatDateString,
cropFile: cropFile,
createEdgeNGrams: createEdgeNGrams,
wait: wait,
checkExistsWithTimeout: checkExistsWithTimeout,
fetchFile: fetchFile,
restartServer: restartServer,
injectArgs: injectArgs,
filterArgs: filterArgs,
searchObjectByString: searchObjectByString,
stripPropertiesFromObject: stripPropertiesFromObject,
getArchiveFolder: getArchiveFolder,
getBaseURL: getBaseURL,
File: File
}

View File

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

View File

@@ -18,7 +18,7 @@ services:
- "8998:17442"
image: tzahi12345/youtubedl-material:latest
ytdl-mongo-db:
image: mongo
image: mongo:4
logging:
driver: "none"
container_name: mongo-db

View File

@@ -0,0 +1,53 @@
import platform
import requests
import shutil
import os
import re
from github import Github
machine = platform.machine()
def isARM():
return True if machine.startswith('arm') else False
def getLatestFileInRepo(repo, search_string):
# Create an unauthenticated instance of the Github object
g = Github(os.environ.get('GH_TOKEN'))
# Replace with the repository owner and name
repo = g.get_repo(repo)
# Get all releases of the repository
releases = repo.get_releases()
# Loop through the releases in reverse order (from latest to oldest)
for release in list(releases):
# Get the release assets (files attached to the release)
assets = release.get_assets()
# Loop through the assets
for asset in assets:
if re.search(search_string, asset.name):
print(f'Downloading: {asset.name}')
response = requests.get(asset.browser_download_url)
with open(asset.name, 'wb') as f:
f.write(response.content)
print(f'Download complete: {asset.name}. Unzipping...')
shutil.unpack_archive(asset.name, './')
print(f'Unzipping complete!')
os.remove(asset.name)
break
else:
continue
break
else:
# If no matching release is found, print a message
print(f'No release found with {search_string}')
def getLatestCLIRelease():
isArm = isARM()
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip"
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
getLatestCLIRelease()

520
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.3.0",
"version": "4.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -2142,6 +2142,12 @@
}
}
},
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"dev": true
},
"@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -3703,12 +3709,6 @@
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
"dev": true
},
"@socket.io/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==",
"dev": true
},
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@@ -3781,10 +3781,13 @@
"dev": true
},
"@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
"version": "2.8.13",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/eslint": {
"version": "8.4.10",
@@ -3851,9 +3854,9 @@
}
},
"@types/jasmine": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.2.tgz",
"integrity": "sha512-AzfesNFLvOs6Q1mHzIsVJXSeUnqVh4ZHG8ngygKJfbkcSLwzrBVm/LKa+mR8KrOfnWtUL47112gde1MC0IXqpQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.1.tgz",
"integrity": "sha512-Vu8l+UGcshYmV1VWwULgnV/2RDbBaO6i2Ptx7nd//oJPIZGhoI1YLST4VKagD2Pq/Bc2/7zvtvhM7F3p4SN7kQ==",
"dev": true
},
"@types/json-schema": {
@@ -4783,13 +4786,13 @@
}
},
"body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dev": true,
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
@@ -4797,7 +4800,7 @@
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
@@ -4808,6 +4811,12 @@
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true
},
"content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -4823,72 +4832,11 @@
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"requires": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"requires": {
"ee-first": "1.1.1"
}
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true,
"requires": {
"side-channel": "^1.0.4"
}
},
"raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dev": true,
"requires": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
}
}
},
@@ -5449,7 +5397,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
}
}
@@ -5806,7 +5754,7 @@
"custom-event": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
"integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
"integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
"dev": true
},
"damerau-levenshtein": {
@@ -5825,9 +5773,9 @@
}
},
"date-format": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.3.tgz",
"integrity": "sha512-7P3FyqDcfeznLZp2b+OMitV9Sz2lUnsT87WaTat9nVwqsBkTzPG3lPLNwW3en6F4pHUiWzr6vb8CLhjdK9bcxQ==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
"integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==",
"dev": true
},
"debug": {
@@ -5939,7 +5887,7 @@
"di": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
"integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
"integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
"dev": true
},
"diff": {
@@ -5984,7 +5932,7 @@
"dom-serialize": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
"integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
"integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==",
"dev": true,
"requires": {
"custom-event": "~1.0.0",
@@ -6124,15 +6072,47 @@
"once": "^1.4.0"
}
},
"engine.io-parser": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
"engine.io": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"dev": true,
"requires": {
"@socket.io/base64-arraybuffer": "~1.0.2"
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0"
},
"dependencies": {
"cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"dev": true
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
}
}
},
"engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"dev": true
},
"enhanced-resolve": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
@@ -6163,7 +6143,7 @@
"ent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
"integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==",
"dev": true
},
"entities": {
@@ -7158,9 +7138,9 @@
"integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
},
"filesize": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg=="
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.7.tgz",
"integrity": "sha512-iMRG7Qo9nayLoU3PNCiLizYtsy4W1ClrapeCwEgtiQelOAOuRJiw4QaLI+sSr8xr901dgHv+EYP2bCusGZgoiA=="
},
"fill-range": {
"version": "7.0.1",
@@ -7198,8 +7178,17 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
"dev": true,
"requires": {
"ee-first": "1.1.1"
}
}
}
},
@@ -7265,9 +7254,9 @@
}
},
"flatted": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"follow-redirects": {
@@ -7809,6 +7798,33 @@
"integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
"dev": true
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"requires": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"dependencies": {
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true
}
}
},
"http-parser-js": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
@@ -8263,9 +8279,9 @@
"dev": true
},
"isbinaryfile": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz",
"integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==",
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
"integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
"dev": true
},
"isexe": {
@@ -8614,15 +8630,15 @@
}
},
"karma": {
"version": "6.3.16",
"resolved": "https://registry.npmjs.org/karma/-/karma-6.3.16.tgz",
"integrity": "sha512-nEU50jLvDe5yvXqkEJRf8IuvddUkOY2x5Xc4WXHz6dxINgGDrgD2uqQWeVrJs4hbfNaotn+HQ1LZJ4yOXrL7xQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz",
"integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==",
"dev": true,
"requires": {
"@colors/colors": "1.5.0",
"body-parser": "^1.19.0",
"braces": "^3.0.2",
"chokidar": "^3.5.1",
"colors": "1.4.0",
"connect": "^3.7.0",
"di": "^0.0.1",
"dom-serialize": "^2.2.1",
@@ -8638,73 +8654,31 @@
"qjobs": "^1.2.0",
"range-parser": "^1.2.1",
"rimraf": "^3.0.2",
"socket.io": "^4.2.0",
"socket.io": "^4.4.1",
"source-map": "^0.6.1",
"tmp": "^0.2.1",
"ua-parser-js": "^0.7.30",
"yargs": "^16.1.1"
},
"dependencies": {
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"graceful-fs": {
"version": "4.2.9",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"mime": {
@@ -8714,21 +8688,12 @@
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
"minimist": "^1.2.6"
}
},
"source-map": {
@@ -8780,18 +8745,18 @@
}
},
"karma-jasmine": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz",
"integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",
"integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==",
"dev": true,
"requires": {
"jasmine-core": "^3.6.0"
"jasmine-core": "^4.1.0"
},
"dependencies": {
"jasmine-core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz",
"integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz",
"integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==",
"dev": true
}
}
@@ -9021,22 +8986,22 @@
}
},
"log4js": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.4.1.tgz",
"integrity": "sha512-iUiYnXqAmNKiIZ1XSAitQ4TmNs8CdZYTAWINARF3LjnsLN8tY5m0vRwd6uuWj/yNY0YHxeZodnbmxKFUOM2rMg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz",
"integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==",
"dev": true,
"requires": {
"date-format": "^4.0.3",
"debug": "^4.3.3",
"flatted": "^3.2.4",
"date-format": "^4.0.14",
"debug": "^4.3.4",
"flatted": "^3.2.7",
"rfdc": "^1.3.0",
"streamroller": "^3.0.2"
"streamroller": "^3.1.5"
},
"dependencies": {
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
@@ -10135,9 +10100,9 @@
"dev": true
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"requires": {
"ee-first": "1.1.1"
@@ -11231,6 +11196,15 @@
"integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
"dev": true
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true,
"requires": {
"side-channel": "^1.0.4"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -11246,6 +11220,26 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true
},
"raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"requires": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true
}
}
},
"read-package-json": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.0.tgz",
@@ -11643,9 +11637,9 @@
}
},
"rxjs-compat": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.5.4.tgz",
"integrity": "sha512-rkn+lbOHUQOurdd74J/hjmDsG9nFx0z66fvnbs8M95nrtKvNqCKdk7iZqdY51CGmDemTQk+kUPy4s8HVOHtkfA=="
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz",
"integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw=="
},
"safe-buffer": {
"version": "5.1.2",
@@ -12014,6 +12008,12 @@
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"dev": true
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true
},
"shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -12105,25 +12105,19 @@
"dev": true
},
"socket.io": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz",
"integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==",
"dev": true,
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.2.1",
"socket.io-adapter": "~2.4.0",
"engine.io": "~6.4.1",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.1"
},
"dependencies": {
"cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"dev": true
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -12132,46 +12126,36 @@
"requires": {
"ms": "2.1.2"
}
},
"engine.io": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
}
}
},
"socket.io-adapter": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
"integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
"dev": true,
"requires": {
"ws": "~8.11.0"
}
},
"socket.io-parser": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
"ms": "2.1.2"
}
},
"socket.io-adapter": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
"dev": true
},
"socket.io-parser": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
}
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"dev": true
}
}
},
@@ -12403,14 +12387,36 @@
"dev": true
},
"streamroller": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.0.2.tgz",
"integrity": "sha512-ur6y5S5dopOaRXBuRIZ1u6GC5bcEXHRZKgfBjfCglMhmIf+roVCECjvkEYzNQOXIN2/JPnkMPW/8B3CZoKaEPA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
"integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==",
"dev": true,
"requires": {
"date-format": "^4.0.3",
"debug": "^4.1.1",
"fs-extra": "^10.0.0"
"date-format": "^4.0.14",
"debug": "^4.3.4",
"fs-extra": "^8.1.0"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
}
}
},
"string-width": {
@@ -12692,6 +12698,12 @@
"is-number": "^7.0.0"
}
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@@ -12928,9 +12940,9 @@
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
},
"ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
"version": "0.7.35",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
"integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
"dev": true
},
"uglify-js": {
@@ -13102,7 +13114,7 @@
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==",
"dev": true
},
"watchpack": {
@@ -13496,9 +13508,9 @@
}
},
"yargs-parser": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true
},
"yauzl": {

View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.3.0",
"version": "4.3.1",
"license": "MIT",
"scripts": {
"ng": "ng",
@@ -39,7 +39,7 @@
"core-js": "^2.4.1",
"crypto-js": "^4.1.1",
"file-saver": "^2.0.2",
"filesize": "^6.1.0",
"filesize": "^10.0.7",
"fingerprintjs2": "^2.1.0",
"fs-extra": "^10.0.0",
"material-icons": "^1.10.8",
@@ -47,7 +47,7 @@
"ngx-avatars": "^1.4.1",
"ngx-file-drop": "^13.0.0",
"rxjs": "^6.6.3",
"rxjs-compat": "^6.0.0-rc.0",
"rxjs-compat": "^6.6.7",
"tslib": "^2.0.0",
"typescript": "~4.8.4",
"xliff-to-json": "^1.0.4",
@@ -60,7 +60,7 @@
"@angular/language-service": "^15.0.1",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0",
"@types/jasmine": "^4.3.1",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
@@ -70,11 +70,11 @@
"eslint": "^7.32.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.16",
"karma": "~6.4.2",
"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": "~5.1.0",
"karma-jasmine-html-reporter": "^1.5.0",
"openapi-typescript-codegen": "^0.23.0",
"protractor": "~7.0.0",

View File

@@ -21,7 +21,7 @@
<mat-icon>person</mat-icon>
<span i18n="Profile menu label">Profile</span>
</button>
<button *ngIf="postsService.config && postsService.config.Downloader.use_youtubedl_archive" class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
<button class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
<mat-icon>topic</mat-icon>
<span i18n="Archives menu label">Archives</span>
</button>

View File

@@ -25,7 +25,7 @@
-webkit-line-clamp: 2;
}
::ng-deep .ngx-file-drop__content {
:host ::ng-deep .ngx-file-drop__content {
width: 100%;
top: -12px;
position: relative;

View File

@@ -2,7 +2,7 @@
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>

View File

@@ -76,8 +76,9 @@ export class RecentVideosComponent implements OnInit {
constructor(public postsService: PostsService, private router: Router) {
// get cached file count
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
const sub_id_appendix = this.sub_id ? `_${this.sub_id}` : ''
if (localStorage.getItem(`cached_file_count${sub_id_appendix}`)) {
this.cached_file_count = +localStorage.getItem(`cached_file_count${sub_id_appendix}`) <= 10 ? +localStorage.getItem(`cached_file_count${sub_id_appendix}`) : 10;
this.loading_files = Array(this.cached_file_count).fill(0);
}

View File

@@ -63,7 +63,7 @@
<ng-container i18n="Update binary to">Update binary to:</ng-container>&nbsp;{{element.data}}
</ng-container>
<ng-container *ngIf="element.key == 'delete_old_files'">
<ng-container i18n="Delete old files">Delete old files:</ng-container>&nbsp;{{element.data.uids.length}}
<ng-container i18n="Delete old files">Delete old files:</ng-container>&nbsp;{{element.data.files_to_remove.length}}
</ng-container>
</button>
</ng-container>

View File

@@ -31,6 +31,6 @@ mat-header-cell, mat-cell {
border-radius: 16px 16px 16px 16px !important;
}
::ng-deep mat-row {
:host ::ng-deep mat-row {
height: fit-content !important;
}

View File

@@ -35,14 +35,9 @@
</ng-container>
</mat-menu>
<mat-divider></mat-divider>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
</button>
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete and don't download again</ng-container>
</button>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
<button *ngIf="file_obj.sub_id || use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
</ng-container>
<ng-container *ngIf="is_playlist && !loading">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>

View File

@@ -171,6 +171,6 @@
}
::ng-deep.mat-mdc-menu-panel {
:host ::ng-deep.mat-mdc-menu-panel {
max-width: none !important;
}

View File

@@ -1 +1 @@
export const CURRENT_VERSION = 'v4.3';
export const CURRENT_VERSION = 'v4.3.1';

View File

@@ -8,10 +8,10 @@
top: -12px;
}
::ng-deep.mat-menu-panel {
:host ::ng-deep.mat-menu-panel {
max-width: none !important;
}
::ng-deep.mdc-list-item__primary-text {
:host ::ng-deep.mdc-list-item__primary-text {
width: 100%;
}

View File

@@ -31,9 +31,11 @@
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
<span class="rule-buttons">
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</span>
</mat-list-item>
</mat-list>

View File

@@ -1,5 +1,5 @@
.operator-select {
width: 55px;
width: 90px;
}
.property-select {
@@ -13,4 +13,17 @@
.value-input {
margin-left: 10px;
}
:host ::ng-deep.mdc-list-item {
height: 75px !important;
}
:host ::ng-deep.mdc-list-item__content {
pointer-events: unset;
}
.rule-buttons {
position: relative;
top: 8px;
}

View File

@@ -37,7 +37,8 @@
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-label i18n="Category">Category</mat-label>
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Inject } from '@angular/core';
import filesize from 'filesize';
import { filesize } from 'filesize';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
import { Category, DatabaseFile } from 'api-types';

View File

@@ -50,7 +50,6 @@ export class MainComponent implements OnInit {
allowQualitySelect = false;
downloadOnlyMode = false;
forceAutoplay = false;
use_youtubedl_archive = false;
globalCustomArgs = null;
allowAdvancedDownload = false;
useDefaultDownloadingAgent = true;
@@ -188,7 +187,6 @@ export class MainComponent implements OnInit {
&& this.postsService.hasPermission('filemanager');
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.forceAutoplay = this.postsService.config['Extra']['force_autoplay'];
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
this.globalCustomArgs = this.postsService.config['Downloader']['custom_args'];
this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
this.postsService.config['API']['youtube_API_key'];

View File

@@ -38,7 +38,7 @@
</ng-container>
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv') && postsService['config']['API']['use_twitch_API']" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
</div>
</div>
</div>
@@ -51,7 +51,7 @@
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists'] && postsService['config']['API']['use_twitch_API']">
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
</ng-container>

View File

@@ -269,25 +269,9 @@
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
</div>
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
<div class="col-12 mt-1">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Twitch Client ID">Twitch Client ID</mat-label>
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput required>
<mat-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Twitch Client Secret">Twitch Client Secret</mat-label>
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput required>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
</div>
@@ -378,6 +362,7 @@
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<div><a target="_blank" href="https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Notifications"><ng-container i18n="Documentation">Documentation</ng-container></a></div>
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['enable_notifications']"><ng-container i18n="Enable notifications setting">Enable notifications</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-1">
@@ -393,6 +378,20 @@
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mb-2 mt-2">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="webhook URL">Webhook URL</mat-label>
<input placeholder="https://example.com/endpoint/12345" [(ngModel)]="new_config['API']['webhook_URL']" matInput>
<mat-hint>Place endpoint URL here to integrate with services like Zapier and Automatisch.</mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-2 mt-3">
<mat-form-field class="text-field" color="accent">
<mat-label i18n="Discord webhook URL">Discord Webhook URL</mat-label>
<input placeholder="https://discord.com/api/webhooks/<webhook_id>/<webhook_token>" [(ngModel)]="new_config['API']['discord_webhook_URL']" matInput>
<mat-hint><a target="_blank" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><ng-container i18n="Gotify API setting hint">See docs here.</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [disabled]="!new_config['Extra']['enable_notifications']" [(ngModel)]="new_config['API']['use_ntfy_API']"><ng-container i18n="Use ntfy API setting">Use ntfy API</ng-container></mat-checkbox>
</div>
@@ -454,6 +453,7 @@
<mat-option value="youtube-dlc">youtube-dlc</mat-option>
<mat-option value="yt-dlp">yt-dlp</mat-option>
</mat-select>
<mat-hint i18n="Restart required hint">Restart required.</mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-1">

View File

@@ -7,7 +7,7 @@
margin-top: 15px;
}
::ng-deep .mat-mdc-tab-body {
:host ::ng-deep .mat-mdc-tab-body {
margin-left: 15px;
}

View File

@@ -41,7 +41,7 @@ export class SettingsComponent implements OnInit {
latestGithubRelease = null;
CURRENT_VERSION = CURRENT_VERSION
tabs = ['main', 'downloader', 'extra', 'database', 'advanced', 'users', 'logs'];
tabs = ['main', 'downloader', 'extra', 'database', 'notifications', 'advanced', 'users', 'logs'];
tabIndex = 0;
INDEX_TO_TAB = Object.assign({}, this.tabs);

View File

@@ -1591,7 +1591,7 @@
</trans-unit>
<trans-unit id="fb35145bfb84521e21b6385363d59221f436a573" datatype="html">
<source>Kill all downloads</source>
<target>Finalitzar totes les descàrregues</target>
<target state="translated">Avortar totes les descàrregues</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">175</context>
@@ -3071,8 +3071,8 @@
<note priority="1" from="description">Installation type</note>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target>Error a <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Error a<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>
@@ -3394,6 +3394,754 @@
</context-group>
<note priority="1" from="description">Update task schedule</note>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="translated">Llista de reproducció creada correctament!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="translated">ERROR: no s'ha pogut crear la llista de reproducció!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="4250042184551786923" datatype="html">
<source>File manager</source>
<target state="translated">Gestor de fitxers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="8525406024045742391" datatype="html">
<source>Settings access</source>
<target state="translated">Configuració d'accés</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="4744991787069301975" datatype="html">
<source>Share files</source>
<target state="translated">Compartir fitxers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="5029464708330583545" datatype="html">
<source>Use advanced download mode</source>
<target state="translated">Utilitza el mode de descàrrega avançat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="3426988753455920032" datatype="html">
<source>Use tasks manager</source>
<target state="translated">Utilitza el gestor de tasques</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="ae9a5141f5c6bd62cee4ce837598ea8b0904e5cf" datatype="html">
<source>Select files</source>
<target state="translated">Seleccionar fitxers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">71</context>
</context-group>
<note priority="1" from="description">Select files</note>
</trans-unit>
<trans-unit id="4050356167294261426" datatype="html">
<source>Delete success!</source>
<target state="translated">Eliminat!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">270</context>
</context-group>
</trans-unit>
<trans-unit id="8348223454028662277" datatype="html">
<source>OK.</source>
<target state="translated">OK.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">270</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="7405156667148936748" datatype="html">
<source>Delete failed!</source>
<target state="translated">No s'ha pogut eliminar!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="8937901770314883418" datatype="html">
<source>Successfully deleted file:</source>
<target state="translated">Fitxer suprimit correctament:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">291</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="9203653061903371757" datatype="html">
<source>Playlist updated successfully.</source>
<target state="translated">La llista de reproducció s'ha actualitzat correctament.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
<context context-type="linenumber">69</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="1946323844380374711" datatype="html">
<source>Failed to restore database! See browser console for more info.</source>
<target state="translated">No s'ha pogut restaurar la base de dades! Veure la consola del navegador per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="a4a4a5f03d7d0831ccf6774094e66a9507a42b58" datatype="html">
<source>Clear downloads</source>
<target state="translated">Neteja les baixades</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">85</context>
</context-group>
<note priority="1" from="description">Clear downloads</note>
</trans-unit>
<trans-unit id="3299455901271096793" datatype="html">
<source>Clear downloads</source>
<target state="translated">Neteja les baixades</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
</trans-unit>
<trans-unit id="5456775416888155476" datatype="html">
<source>Failed to resume download! See server logs for more info.</source>
<target state="translated">No s'ha pogut reprendre la baixada! Veure els registres del servidor per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="6268791413935580107" datatype="html">
<source>Failed to resume all downloads! See server logs for more info.</source>
<target state="translated">No s'han pogut reprendre totes les baixades! Veure els registres del servidor per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="571023367671104036" datatype="html">
<source>Failed to restart download! See server logs for more info.</source>
<target state="translated">No s'ha pogut reiniciar la baixada! Veure els registres del servidor per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="4529487534884306633" datatype="html">
<source>Failed to cancel download! See server logs for more info.</source>
<target state="translated">No s'ha pogut cancel·lar la baixada! Veure els registres del servidor per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">210</context>
</context-group>
</trans-unit>
<trans-unit id="1019978815798793544" datatype="html">
<source>Failed to retrieve logs!</source>
<target state="translated">No s'han pogut recuperar els registres!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">51</context>
</context-group>
</trans-unit>
<trans-unit id="2293081271355999967" datatype="html">
<source>Logs successfully cleared!</source>
<target state="translated">Els registres s'han netejat correctament!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="52e0fa8ada52c3f29774a4508582fd98250b9f93" datatype="html">
<source>My files</source>
<target state="translated">Els meus fitxers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">My files title</note>
</trans-unit>
<trans-unit id="6827066f436adfc56a142d5816a8be6113d73b01" datatype="html">
<source>No files found.</source>
<target state="translated">No s'han trobat fitxers.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">No files found</note>
</trans-unit>
<trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
<source>Order</source>
<target state="translated">Ordenar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">53</context>
</context-group>
<note priority="1" from="description">Order</note>
</trans-unit>
<trans-unit id="820184305380634591" datatype="html">
<source>Playlist successfully removed.</source>
<target state="translated">La llista de reproducció s'ha eliminat correctament.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="5215119607776782829" datatype="html">
<source>Select downloads to clear</source>
<target state="translated">Seleccioneu les baixades a netejar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="8485375438204712002" datatype="html">
<source>Finished downloads</source>
<target state="translated">Descàrregues finalitzades</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">138</context>
</context-group>
</trans-unit>
<trans-unit id="5801924165267871854" datatype="html">
<source>Paused downloads</source>
<target state="translated">Descàrregues en pausa</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
<source>Delete and don't download again</source>
<target state="translated">Suprimeix i no el tornis a baixar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">Delete forever subscription video button</note>
</trans-unit>
<trans-unit id="1709839462010459086" datatype="html">
<source>Cookies successfully uploaded!</source>
<target state="translated">Les galetes s'han carregat correctament!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="c1b7e6d75ff4285c7636c67e5ef259629b81725b" datatype="html">
<source>Confirm Password</source>
<target state="translated">Confirmeu la contrasenya</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/login/login.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
<note priority="1" from="description">Confirm Password</note>
</trans-unit>
<trans-unit id="87406377200084623" datatype="html">
<source>Logs copied to clipboard!</source>
<target state="translated">Registres copiats al porta-retalls!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="4516710756538206828" datatype="html">
<source>Failed to clear logs!</source>
<target state="translated">No s'han pogut netejar els registres!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">77</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="1812379335568847528" datatype="html">
<source>Subscriptions</source>
<target state="translated">Subscripcions</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="4278268519633335280" datatype="html">
<source>Use downloads manager</source>
<target state="translated">Utilitza el gestor de descàrregues</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="880407735794041263" datatype="html">
<source>Download failed.</source>
<target state="translated">La baixada ha fallat.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="3481862581074838726" datatype="html">
<source>VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"</source>
<target state="translated">L'URL de VOD d'aquest vídeo no és compatible. L'identificador de VOD ha de ser posterior a "twitch.tv/videos/"</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="7157191502004604261" datatype="html">
<source>Chat could not be downloaded.</source>
<target state="translated">No s'ha pogut baixar el xat.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="2159130950882492111" datatype="html">
<source>Cancel</source>
<target state="translated">_Cancel·la</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/confirm-dialog/confirm-dialog.component.ts</context>
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="3480433876298276350" datatype="html">
<source>Database successfully restored!</source>
<target state="translated">La base de dades s'ha restaurat correctament!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="643438049907907768" datatype="html">
<source>Failed to restore database! See logs for more info.</source>
<target state="translated">No s'ha pogut restaurar la base de dades! Veure els registres per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="4960870191807928282" datatype="html">
<source>Sharing enabled.</source>
<target state="translated">Ús compartit habilitat.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="2720327817780634026" datatype="html">
<source>Failed to enable sharing.</source>
<target state="translated">No s'ha pogut habilitar l'ús compartit.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">71</context>
</context-group>
</trans-unit>
<trans-unit id="2859348955905483094" datatype="html">
<source>Failed to enable sharing - server error.</source>
<target state="translated">No s'ha pogut activar l'ús compartit: error del servidor.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="7840375760456214518" datatype="html">
<source>Failed to disable sharing.</source>
<target state="translated">No s'ha pogut desactivar l'ús compartit.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="8692976466689769553" datatype="html">
<source>Failed to disable sharing - server error.</source>
<target state="translated">No s'ha pogut desactivar l'ús compartit: error del servidor.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="3561468911579213356" datatype="html">
<source>Sharing disabled.</source>
<target state="translated">Compartició desactivada.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="3544790314111256717" datatype="html">
<source>ERROR:</source>
<target state="translated">ERROR:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="5397815846940616259" datatype="html">
<source>You must specify an amount of time</source>
<target state="translated">Heu d'especificar una quantitat de temps</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="2876893175497409225" datatype="html">
<source>Update failed. Check logs for more details.</source>
<target state="translated">L'actualització ha fallat. Comprova els registres per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="3317b8688eb2cfabc4021cd7b2926b32f3864ad2" datatype="html">
<source>Choose a date</source>
<target state="translated">Tria una data</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html</context>
<context context-type="linenumber">22</context>
</context-group>
<note priority="1" from="description">Choose a date</note>
</trans-unit>
<trans-unit id="d49d5d6786b69d140e20cfddfe29690a19641a88" datatype="html">
<source>Thumbnail path</source>
<target state="translated">Camí de la miniatura</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Thumbnail path</note>
</trans-unit>
<trans-unit id="f4003b626fcbf3a871778d4dba166e109d02f87c" datatype="html">
<source>Thumbnail URL</source>
<target state="translated">URL de les miniatures</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
<note priority="1" from="description">Thumbnail URL</note>
</trans-unit>
<trans-unit id="22bac71dbdc1ac62607135994f81cca8094cb251" datatype="html">
<source>Upload date</source>
<target state="translated">Data de càrrega</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Upload date</note>
</trans-unit>
<trans-unit id="f8c7be184fefd6750e4e5d0c7a90e74721c58f8a" datatype="html">
<source>Uploader</source>
<target state="translated">Carregador</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Uploader</note>
</trans-unit>
<trans-unit id="607de17c2a755f65775881c19e276e7c933bcf94" datatype="html">
<source>Category</source>
<target state="translated">Categoria</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
<note priority="1" from="description">Category</note>
</trans-unit>
<trans-unit id="989f5aa799ee9672675d68109bff29d1d88ebd49" datatype="html">
<source>Local view count</source>
<target state="translated">Recompte de visualitzacions locals</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Local view count</note>
</trans-unit>
<trans-unit id="3f741a2c015bb728088b630296ca401e823c6af8" datatype="html">
<source>View count</source>
<target state="translated">Recompte de visualitzacions</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<note priority="1" from="description">View count</note>
</trans-unit>
<trans-unit id="9fc54db2830fbbd332b1adebe28e9283069107ef" datatype="html">
<source>Audio bitrate:</source>
<target state="translated">Taxa de bits d'àudio:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">55</context>
</context-group>
<note priority="1" from="description">Video audio bitrate property</note>
</trans-unit>
<trans-unit id="9fa37704969eeebd496a172c5077370f569df3ae" datatype="html">
<source>Resolution:</source>
<target state="translated">Resolució:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Video resolution property</note>
</trans-unit>
<trans-unit id="2734512985872312443" datatype="html">
<source>Failed to get file information from the server.</source>
<target state="translated">No s'ha pogut obtenir informació del fitxer del servidor.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="4c9a15ab7fb3dce1002ea7aea4ecada3c1ee12e9" datatype="html">
<source>Generating an ID/secret is easy!</source>
<target state="translated">Generar un ID/secret és fàcil!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">267</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting hint</note>
</trans-unit>
<trans-unit id="5d78fe9ba69a8710613d3f7c35b22e9c8226e4dc" datatype="html">
<source>Twitch Client ID</source>
<target state="translated">ID de client de Twitch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">266</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting placeholder</note>
</trans-unit>
<trans-unit id="8506540da14d205ea092b4c856e242ed7f500643" datatype="html">
<source>Twitch Client Secret</source>
<target state="translated">Secret client de Twitch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
</context-group>
<note priority="1" from="description">Twitch Client Secret setting placeholder</note>
</trans-unit>
<trans-unit id="4604336107574138791" datatype="html">
<source>Chrome users must drag the 'Alternate URL' link to your bookmarks.</source>
<target state="translated">Els usuaris de Chrome han d'arrossegar l'enllaç "URL alternatiu" a les adreces d'interès.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">237</context>
</context-group>
</trans-unit>
<trans-unit id="9208873922277364009" datatype="html">
<source>Failed to update categories!</source>
<target state="translated">No s'han pogut actualitzar les categories!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="7180231139026789468" datatype="html">
<source>Language successfully changed! Reload to update the page.</source>
<target state="translated">L'idioma ha canviat correctament! Torna a carregar per a actualitzar la pàgina.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">209</context>
</context-group>
</trans-unit>
<trans-unit id="1942965859829798388" datatype="html">
<source>Restarting!</source>
<target state="translated">Reiniciant!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">307</context>
</context-group>
</trans-unit>
<trans-unit id="4257962986336738751" datatype="html">
<source>Successfully killed all downloads!</source>
<target state="translated">S'han avortat totes les descàrregues correctament!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">292</context>
</context-group>
</trans-unit>
<trans-unit id="4021495815084152271" datatype="html">
<source>Connection failed! Error:</source>
<target state="translated">La connexió ha fallat! Error:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="6018050954136387828" datatype="html">
<source>Connection successful!</source>
<target state="translated">Connexió correcta!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">357</context>
</context-group>
</trans-unit>
<trans-unit id="6224607866493148072" datatype="html">
<source>Failed to restart the server.</source>
<target state="translated">No s'ha pogut reiniciar el servidor.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">309</context>
</context-group>
</trans-unit>
<trans-unit id="6123898845299902958" datatype="html">
<source>Successfully transfered DB! Reloading info...</source>
<target state="translated">Base de dades transferida correctament! S'està tornant a carregar la informació...</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">340</context>
</context-group>
</trans-unit>
<trans-unit id="3961621815065792326" datatype="html">
<source>Failed to clear finished downloads!</source>
<target state="translated">No s'han pogut esborrar les descàrregues acabades!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">157</context>
</context-group>
</trans-unit>
<trans-unit id="5223827577229167333" datatype="html">
<source>Failed to pause all downloads! See server logs for more info.</source>
<target state="translated">No s'han pogut aturar totes les descàrregues. Veure els registres del servidor per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="2600933489084742998" datatype="html">
<source>Failed to kill all downloads! Check logs for details.</source>
<target state="translated">No s'han pogut avortar totes les baixades! Consulta els registres per a informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">295</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="5823550543348347814" datatype="html">
<source>Cleared downloads!</source>
<target state="translated">Descàrregues suprimides!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="8314249599019746316" datatype="html">
<source>Download failed!</source>
<target state="translated">La descàrrega ha fallat!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">387</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">775</context>
</context-group>
</trans-unit>
<trans-unit id="2723988842145709249" datatype="html">
<source>Errored downloads</source>
<target state="translated">Descàrregues amb errors</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="6789263921624845085" datatype="html">
<source>Failed to load playlist!</source>
<target state="translated">No s'ha pogut carregar la llista de reproducció!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">189</context>
</context-group>
</trans-unit>
<trans-unit id="1716030487077666916" datatype="html">
<source>Failed to transfer DB -- API call failed. See browser logs for details.</source>
<target state="translated">No s'ha pogut transferir DB -- La trucada API ha fallat. Veure els registres del navegador per a informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">347</context>
</context-group>
</trans-unit>
<trans-unit id="7114033980971410157" datatype="html">
<source>Failed to pause download! See server logs for more info.</source>
<target state="translated">No s'ha pogut posar en pausa la baixada! Veure els registres del servidor per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">170</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="5681417617361245213" datatype="html">
<source>Failed to transfer DB -- transfer was aborted. Error:</source>
<target state="translated">No s'ha pogut transferir la base de dades; la transferència s'ha avortat. Error:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">343</context>
</context-group>
</trans-unit>
<trans-unit id="6519219215739537829" datatype="html">
<source>Connection failed! Error: Server error. See logs for more info.</source>
<target state="translated">La connexió ha fallat! Error: Error del servidor. Veure registres per a més informació.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">363</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -170,19 +170,27 @@
<source>Delete selected</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">76</context>
<context context-type="linenumber">77</context>
</context-group>
<note priority="1" from="description">Delete selected</note>
</trans-unit>
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html">
<source>Download archive</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">80</context>
</context-group>
<note priority="1" from="description">Download archive</note>
</trans-unit>
<trans-unit id="47bbc861efa59ba4135e6aa8f63213420e3f3b91" datatype="html">
<source>Subscription</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">79</context>
<context context-type="linenumber">82</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">124</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
@@ -198,11 +206,11 @@
<source>None</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">81</context>
<context context-type="linenumber">84</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">123</context>
<context context-type="linenumber">126</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
@@ -218,11 +226,11 @@
<source>File type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">86</context>
<context context-type="linenumber">89</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">128</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
@@ -234,7 +242,7 @@
<source>Both</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">88</context>
<context context-type="linenumber">91</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
@@ -246,11 +254,11 @@
<source>Video</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">92</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">130</context>
<context context-type="linenumber">133</context>
</context-group>
<note priority="1" from="description">Video</note>
</trans-unit>
@@ -258,11 +266,11 @@
<source>Audio</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">90</context>
<context context-type="linenumber">93</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">134</context>
</context-group>
<note priority="1" from="description">Audio</note>
</trans-unit>
@@ -270,7 +278,7 @@
<source>Drag and Drop</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">101</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
@@ -282,7 +290,7 @@
<source>Upload</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
@@ -315,21 +323,21 @@
<source>Delete archives</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">145</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="8425787787095143143" datatype="html">
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">146</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="7022070615528435141" datatype="html">
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">147</context>
<context context-type="linenumber">154</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
@@ -340,14 +348,14 @@
<source>Successfully deleted archive items!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">165</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="8224301330941792118" datatype="html">
<source>Failed to delete archive items!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">167</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
@@ -455,7 +463,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">602</context>
<context context-type="linenumber">611</context>
</context-group>
<note priority="1" from="description">Cancel</note>
</trans-unit>
@@ -1086,7 +1094,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">599</context>
<context context-type="linenumber">608</context>
</context-group>
<note priority="1" from="description">save user edit action button tooltip</note>
</trans-unit>
@@ -1786,7 +1794,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">16</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscriptions/subscriptions.component.html</context>
@@ -2161,6 +2169,10 @@
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">127</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">381</context>
</context-group>
<note priority="1" from="description">Custom output template documentation link</note>
</trans-unit>
<trans-unit id="19d1ae64d94d28a29b2c57ae8671aace906b5401" datatype="html">
@@ -3003,7 +3015,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">775</context>
<context context-type="linenumber">788</context>
</context-group>
</trans-unit>
<trans-unit id="7220285196408439810" datatype="html">
@@ -3691,7 +3703,7 @@
<source>Enable notifications</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">381</context>
<context context-type="linenumber">382</context>
</context-group>
<note priority="1" from="description">Enable notifications setting</note>
</trans-unit>
@@ -3699,7 +3711,7 @@
<source>Enable all notifications</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">384</context>
<context context-type="linenumber">385</context>
</context-group>
<note priority="1" from="description">Enable all notifications setting</note>
</trans-unit>
@@ -3707,7 +3719,7 @@
<source>Allowed notification types</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">388</context>
<context context-type="linenumber">389</context>
</context-group>
<note priority="1" from="description">Allowed notification types</note>
</trans-unit>
@@ -3715,7 +3727,7 @@
<source>Download complete</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">390</context>
<context context-type="linenumber">391</context>
</context-group>
<note priority="1" from="description">Download complete</note>
</trans-unit>
@@ -3723,7 +3735,7 @@
<source>Download error</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">391</context>
<context context-type="linenumber">392</context>
</context-group>
<note priority="1" from="description">Download error</note>
</trans-unit>
@@ -3731,15 +3743,23 @@
<source>Task finished</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
<context context-type="linenumber">393</context>
</context-group>
<note priority="1" from="description">Task finished</note>
</trans-unit>
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html">
<source>Webhook URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">399</context>
</context-group>
<note priority="1" from="description">webhook URL</note>
</trans-unit>
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html">
<source>Use ntfy API</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">397</context>
<context context-type="linenumber">405</context>
</context-group>
<note priority="1" from="description">Use ntfy API setting</note>
</trans-unit>
@@ -3747,7 +3767,7 @@
<source>ntfy topic URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">401</context>
<context context-type="linenumber">409</context>
</context-group>
<note priority="1" from="description">ntfy topic URL</note>
</trans-unit>
@@ -3755,15 +3775,15 @@
<source>See docs here.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">403</context>
<context context-type="linenumber">411</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">413</context>
<context context-type="linenumber">421</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">420</context>
<context context-type="linenumber">428</context>
</context-group>
<note priority="1" from="description">ntfy API setting hint</note>
</trans-unit>
@@ -3771,7 +3791,7 @@
<source>Use gotify API</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">407</context>
<context context-type="linenumber">415</context>
</context-group>
<note priority="1" from="description">Use gotify API setting</note>
</trans-unit>
@@ -3779,7 +3799,7 @@
<source>Gotify server URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">411</context>
<context context-type="linenumber">419</context>
</context-group>
<note priority="1" from="description">Gotify server URL</note>
</trans-unit>
@@ -3787,7 +3807,7 @@
<source>Gotify app token</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">418</context>
<context context-type="linenumber">426</context>
</context-group>
<note priority="1" from="description">Gotify app token</note>
</trans-unit>
@@ -3795,7 +3815,7 @@
<source>Use Telegram API</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">424</context>
<context context-type="linenumber">432</context>
</context-group>
<note priority="1" from="description">Use Telegram API setting</note>
</trans-unit>
@@ -3803,7 +3823,7 @@
<source>Telegram bot token</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">428</context>
<context context-type="linenumber">436</context>
</context-group>
<note priority="1" from="description">Telegram bot token</note>
</trans-unit>
@@ -3811,7 +3831,7 @@
<source>Create bot here.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">430</context>
<context context-type="linenumber">438</context>
</context-group>
<note priority="1" from="description">Telegram bot create link</note>
</trans-unit>
@@ -3819,7 +3839,7 @@
<source>Telegram chat ID</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">435</context>
<context context-type="linenumber">443</context>
</context-group>
<note priority="1" from="description">Telegram chat ID</note>
</trans-unit>
@@ -3827,7 +3847,7 @@
<source>How do I get the chat ID?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">437</context>
<context context-type="linenumber">445</context>
</context-group>
<note priority="1" from="description">Telegram chat ID help</note>
</trans-unit>
@@ -3835,7 +3855,7 @@
<source>Advanced</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">445</context>
<context context-type="linenumber">453</context>
</context-group>
<note priority="1" from="description">Host settings label</note>
</trans-unit>
@@ -3843,15 +3863,23 @@
<source>Select a downloader</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">451</context>
<context context-type="linenumber">459</context>
</context-group>
<note priority="1" from="description">Default downloader select label</note>
</trans-unit>
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html">
<source>Restart required.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">465</context>
</context-group>
<note priority="1" from="description">Restart required hint</note>
</trans-unit>
<trans-unit id="5fab47f146b0a4b809dcebf3db9da94df6299ea1" datatype="html">
<source>Use default downloading agent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">460</context>
<context context-type="linenumber">469</context>
</context-group>
<note priority="1" from="description">Use default downloading agent setting</note>
</trans-unit>
@@ -3859,7 +3887,7 @@
<source>Select a download agent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">464</context>
<context context-type="linenumber">473</context>
</context-group>
<note priority="1" from="description">Custom downloader select label</note>
</trans-unit>
@@ -3867,7 +3895,7 @@
<source>Log Level</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">478</context>
<context context-type="linenumber">487</context>
</context-group>
<note priority="1" from="description">Log Level label</note>
</trans-unit>
@@ -3875,7 +3903,7 @@
<source>Login expiration</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">490</context>
<context context-type="linenumber">499</context>
</context-group>
<note priority="1" from="description">Login expiration select label</note>
</trans-unit>
@@ -3883,7 +3911,7 @@
<source>Allow advanced download</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">501</context>
<context context-type="linenumber">510</context>
</context-group>
<note priority="1" from="description">Allow advanced downloading setting</note>
</trans-unit>
@@ -3891,7 +3919,7 @@
<source>Use Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">509</context>
<context context-type="linenumber">518</context>
</context-group>
<note priority="1" from="description">Use cookies setting</note>
</trans-unit>
@@ -3899,7 +3927,7 @@
<source>Set Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">510</context>
<context context-type="linenumber">519</context>
</context-group>
<note priority="1" from="description">Set cookies button</note>
</trans-unit>
@@ -3907,7 +3935,7 @@
<source>Restart server</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">522</context>
<context context-type="linenumber">531</context>
</context-group>
<note priority="1" from="description">Restart server button</note>
</trans-unit>
@@ -3915,7 +3943,7 @@
<source>Users</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">531</context>
<context context-type="linenumber">540</context>
</context-group>
<note priority="1" from="description">Users settings label</note>
</trans-unit>
@@ -3923,7 +3951,7 @@
<source>Allow user registration</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">537</context>
<context context-type="linenumber">546</context>
</context-group>
<note priority="1" from="description">Allow registration setting</note>
</trans-unit>
@@ -3931,7 +3959,7 @@
<source>Auth method</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">541</context>
<context context-type="linenumber">550</context>
</context-group>
<note priority="1" from="description">Auth method</note>
</trans-unit>
@@ -3939,7 +3967,7 @@
<source>Internal</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">544</context>
<context context-type="linenumber">553</context>
</context-group>
<note priority="1" from="description">Internal auth method</note>
</trans-unit>
@@ -3947,7 +3975,7 @@
<source>LDAP</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">547</context>
<context context-type="linenumber">556</context>
</context-group>
<note priority="1" from="description">LDAP auth method</note>
</trans-unit>
@@ -3955,7 +3983,7 @@
<source>LDAP URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">554</context>
<context context-type="linenumber">563</context>
</context-group>
<note priority="1" from="description">LDAP URL</note>
</trans-unit>
@@ -3963,7 +3991,7 @@
<source>Bind DN</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">560</context>
<context context-type="linenumber">569</context>
</context-group>
<note priority="1" from="description">Bind DN</note>
</trans-unit>
@@ -3971,7 +3999,7 @@
<source>Bind Credentials</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">566</context>
<context context-type="linenumber">575</context>
</context-group>
<note priority="1" from="description">Bind Credentials</note>
</trans-unit>
@@ -3979,7 +4007,7 @@
<source>Search Base</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">572</context>
<context context-type="linenumber">581</context>
</context-group>
<note priority="1" from="description">Search Base</note>
</trans-unit>
@@ -3987,7 +4015,7 @@
<source>Search Filter</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">578</context>
<context context-type="linenumber">587</context>
</context-group>
<note priority="1" from="description">Search Filter</note>
</trans-unit>
@@ -3995,7 +4023,7 @@
<source>Logs</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">588</context>
<context context-type="linenumber">597</context>
</context-group>
<note priority="1" from="description">Logs settings label</note>
</trans-unit>
@@ -4133,7 +4161,7 @@
<source>Play all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">16</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Play all</note>
</trans-unit>
@@ -4141,7 +4169,7 @@
<source>Download zip</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">17</context>
<context context-type="linenumber">18</context>
</context-group>
<note priority="1" from="description">Download zip</note>
</trans-unit>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2961,8 +2961,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target state="translated">Erreur pour <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Erreur pour<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>
@@ -3437,6 +3437,772 @@
</context-group>
<note priority="1" from="description">Scheduled</note>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="translated">Liste de lecture créée avec succès !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="translated">ERREUR : échec de création de la liste de lecture !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="4250042184551786923" datatype="html">
<source>File manager</source>
<target state="translated">Gestionnaire de fichiers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="8525406024045742391" datatype="html">
<source>Settings access</source>
<target state="translated">Préférences d'accès</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="5029464708330583545" datatype="html">
<source>Use advanced download mode</source>
<target state="translated">Utiliser le mode de téléchargement avancé</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="820184305380634591" datatype="html">
<source>Playlist successfully removed.</source>
<target state="translated">Liste de lecture retirée avec succès.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="3426988753455920032" datatype="html">
<source>Use tasks manager</source>
<target state="translated">Utiliser le gestionnaire de tâches</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="9203653061903371757" datatype="html">
<source>Playlist updated successfully.</source>
<target state="translated">Liste de lecture mise à jour avec succès.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
<context context-type="linenumber">69</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="3317b8688eb2cfabc4021cd7b2926b32f3864ad2" datatype="html">
<source>Choose a date</source>
<target state="translated">Choisir une date</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html</context>
<context context-type="linenumber">22</context>
</context-group>
<note priority="1" from="description">Choose a date</note>
</trans-unit>
<trans-unit id="9fa37704969eeebd496a172c5077370f569df3ae" datatype="html">
<source>Resolution:</source>
<target state="translated">Résolution :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Video resolution property</note>
</trans-unit>
<trans-unit id="9fc54db2830fbbd332b1adebe28e9283069107ef" datatype="html">
<source>Audio bitrate:</source>
<target state="translated">Flux audio :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">55</context>
</context-group>
<note priority="1" from="description">Video audio bitrate property</note>
</trans-unit>
<trans-unit id="1716030487077666916" datatype="html">
<source>Failed to transfer DB -- API call failed. See browser logs for details.</source>
<target state="translated">Échec de transfert de la base de donnée -- échec d'appel de l'API. Voir les journaux du navigateur pour les détails.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">347</context>
</context-group>
</trans-unit>
<trans-unit id="6018050954136387828" datatype="html">
<source>Connection successful!</source>
<target state="translated">Connexion avec succès !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">357</context>
</context-group>
</trans-unit>
<trans-unit id="a4a4a5f03d7d0831ccf6774094e66a9507a42b58" datatype="html">
<source>Clear downloads</source>
<target state="translated">Effacer les téléchargements</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">85</context>
</context-group>
<note priority="1" from="description">Clear downloads</note>
</trans-unit>
<trans-unit id="3299455901271096793" datatype="html">
<source>Clear downloads</source>
<target state="translated">Effacer les téléchargements</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
</trans-unit>
<trans-unit id="8485375438204712002" datatype="html">
<source>Finished downloads</source>
<target state="translated">Téléchargements terminés</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">138</context>
</context-group>
</trans-unit>
<trans-unit id="5801924165267871854" datatype="html">
<source>Paused downloads</source>
<target state="translated">Téléchargements en pause</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="2723988842145709249" datatype="html">
<source>Errored downloads</source>
<target state="translated">Téléchargements avec erreur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="5215119607776782829" datatype="html">
<source>Select downloads to clear</source>
<target state="translated">Sélectionner les téléchargements à effacer</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="3961621815065792326" datatype="html">
<source>Failed to clear finished downloads!</source>
<target state="translated">Échec d'effacement des téléchargements terminés !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">157</context>
</context-group>
</trans-unit>
<trans-unit id="5456775416888155476" datatype="html">
<source>Failed to resume download! See server logs for more info.</source>
<target state="translated">Échec de reprise du téléchargement ! Voir le journal du serveur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="6268791413935580107" datatype="html">
<source>Failed to resume all downloads! See server logs for more info.</source>
<target state="translated">Échec de reprise des tous les téléchargements ! Voir le journal du serveur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="4529487534884306633" datatype="html">
<source>Failed to cancel download! See server logs for more info.</source>
<target state="translated">Échec d'annulation du téléchargement ! Voir le journal du serveur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">210</context>
</context-group>
</trans-unit>
<trans-unit id="5223827577229167333" datatype="html">
<source>Failed to pause all downloads! See server logs for more info.</source>
<target state="translated">Échec de pause de tous les téléchargements ! Voir le journal du serveur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="7114033980971410157" datatype="html">
<source>Failed to pause download! See server logs for more info.</source>
<target state="translated">Échec de pause du téléchargement ! Voir le journal du serveur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">170</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="c1b7e6d75ff4285c7636c67e5ef259629b81725b" datatype="html">
<source>Confirm Password</source>
<target state="translated">Confirmer le mot de passe</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/login/login.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
<note priority="1" from="description">Confirm Password</note>
</trans-unit>
<trans-unit id="1019978815798793544" datatype="html">
<source>Failed to retrieve logs!</source>
<target state="translated">Échec de récupération des journaux !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">51</context>
</context-group>
</trans-unit>
<trans-unit id="87406377200084623" datatype="html">
<source>Logs copied to clipboard!</source>
<target state="translated">Journaux copiés dans le presse-papiers !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2293081271355999967" datatype="html">
<source>Logs successfully cleared!</source>
<target state="translated">Journaux effacés avec succès !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="4516710756538206828" datatype="html">
<source>Failed to clear logs!</source>
<target state="translated">Échec d'effacement des journaux !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">77</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="6827066f436adfc56a142d5816a8be6113d73b01" datatype="html">
<source>No files found.</source>
<target state="translated">Aucun fichier trouvé.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">No files found</note>
</trans-unit>
<trans-unit id="52e0fa8ada52c3f29774a4508582fd98250b9f93" datatype="html">
<source>My files</source>
<target state="translated">Mes fichiers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">My files title</note>
</trans-unit>
<trans-unit id="8348223454028662277" datatype="html">
<source>OK.</source>
<target state="translated">OK.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">270</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="7405156667148936748" datatype="html">
<source>Delete failed!</source>
<target state="translated">Échec de la suppression !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="ae9a5141f5c6bd62cee4ce837598ea8b0904e5cf" datatype="html">
<source>Select files</source>
<target state="translated">Sélectionner les fichiers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">71</context>
</context-group>
<note priority="1" from="description">Select files</note>
</trans-unit>
<trans-unit id="4050356167294261426" datatype="html">
<source>Delete success!</source>
<target state="translated">Suppression avec succès !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">270</context>
</context-group>
</trans-unit>
<trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
<source>Order</source>
<target state="translated">Tri</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">53</context>
</context-group>
<note priority="1" from="description">Order</note>
</trans-unit>
<trans-unit id="8937901770314883418" datatype="html">
<source>Successfully deleted file:</source>
<target state="translated">Fichier supprimé avec succès :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">291</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="880407735794041263" datatype="html">
<source>Download failed.</source>
<target state="translated">Échec du téléchargement.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
<source>Delete and don't download again</source>
<target state="translated">Supprimer et ne pas télécharger à nouveau</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">Delete forever subscription video button</note>
</trans-unit>
<trans-unit id="3481862581074838726" datatype="html">
<source>VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"</source>
<target state="translated">L'URL de VOD pour cette vidéo n'est pas supporté. L'identifiant de VOD doit être après "twitch.tv/videos/"</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="7157191502004604261" datatype="html">
<source>Chat could not be downloaded.</source>
<target state="translated">Le chat n'a pu être téléchargé.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="2159130950882492111" datatype="html">
<source>Cancel</source>
<target state="translated">Annuler</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/confirm-dialog/confirm-dialog.component.ts</context>
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="1709839462010459086" datatype="html">
<source>Cookies successfully uploaded!</source>
<target state="translated">Envoi avec succès des cookies !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="3480433876298276350" datatype="html">
<source>Database successfully restored!</source>
<target state="translated">Base de donnée restaurée avec succès !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="1946323844380374711" datatype="html">
<source>Failed to restore database! See browser console for more info.</source>
<target state="translated">Échec de restauration de la base de données ! Voir la console du navigateur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="643438049907907768" datatype="html">
<source>Failed to restore database! See logs for more info.</source>
<target state="translated">Échec de restauration de la base de données ! Voir les journaux pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="4960870191807928282" datatype="html">
<source>Sharing enabled.</source>
<target state="translated">Partage activé.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="2720327817780634026" datatype="html">
<source>Failed to enable sharing.</source>
<target state="translated">Échec d'activation du partage.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">71</context>
</context-group>
</trans-unit>
<trans-unit id="3561468911579213356" datatype="html">
<source>Sharing disabled.</source>
<target state="translated">Partage désactivé.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="2859348955905483094" datatype="html">
<source>Failed to enable sharing - server error.</source>
<target state="translated">Échec d'activation du partage - erreur serveur.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="5397815846940616259" datatype="html">
<source>You must specify an amount of time</source>
<target state="translated">Vous devez spécifier une durée</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="7840375760456214518" datatype="html">
<source>Failed to disable sharing.</source>
<target state="translated">Échec de désactivation du partage.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="8692976466689769553" datatype="html">
<source>Failed to disable sharing - server error.</source>
<target state="translated">Échec de désactivation du partage - erreur serveur.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="3544790314111256717" datatype="html">
<source>ERROR:</source>
<target state="translated">ERREUR :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="2876893175497409225" datatype="html">
<source>Update failed. Check logs for more details.</source>
<target state="translated">Échec de l'envoi. Vérifier les journaux pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="f8c7be184fefd6750e4e5d0c7a90e74721c58f8a" datatype="html">
<source>Uploader</source>
<target state="translated">Mis en ligne par</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Uploader</note>
</trans-unit>
<trans-unit id="22bac71dbdc1ac62607135994f81cca8094cb251" datatype="html">
<source>Upload date</source>
<target state="translated">Date de mise en ligne</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Upload date</note>
</trans-unit>
<trans-unit id="d49d5d6786b69d140e20cfddfe29690a19641a88" datatype="html">
<source>Thumbnail path</source>
<target state="translated">Chemin de l'aperçu</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Thumbnail path</note>
</trans-unit>
<trans-unit id="f4003b626fcbf3a871778d4dba166e109d02f87c" datatype="html">
<source>Thumbnail URL</source>
<target state="translated">L'URL de l'aperçu</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
<note priority="1" from="description">Thumbnail URL</note>
</trans-unit>
<trans-unit id="3f741a2c015bb728088b630296ca401e823c6af8" datatype="html">
<source>View count</source>
<target state="translated">Nombre de vue</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<note priority="1" from="description">View count</note>
</trans-unit>
<trans-unit id="607de17c2a755f65775881c19e276e7c933bcf94" datatype="html">
<source>Category</source>
<target state="translated">Catégorie</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
<note priority="1" from="description">Category</note>
</trans-unit>
<trans-unit id="989f5aa799ee9672675d68109bff29d1d88ebd49" datatype="html">
<source>Local view count</source>
<target state="translated">Nombre de vues en local</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Local view count</note>
</trans-unit>
<trans-unit id="6789263921624845085" datatype="html">
<source>Failed to load playlist!</source>
<target state="translated">Échec de chargement de la liste de lecture !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">189</context>
</context-group>
</trans-unit>
<trans-unit id="8314249599019746316" datatype="html">
<source>Download failed!</source>
<target state="translated">Échec du téléchargement!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">387</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">775</context>
</context-group>
</trans-unit>
<trans-unit id="2734512985872312443" datatype="html">
<source>Failed to get file information from the server.</source>
<target state="translated">Échec de récupération des informations du fichier depuis le serveur.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="5d78fe9ba69a8710613d3f7c35b22e9c8226e4dc" datatype="html">
<source>Twitch Client ID</source>
<target state="translated">Identifiant client Twitch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">266</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting placeholder</note>
</trans-unit>
<trans-unit id="8506540da14d205ea092b4c856e242ed7f500643" datatype="html">
<source>Twitch Client Secret</source>
<target state="translated">Secret client Twitch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
</context-group>
<note priority="1" from="description">Twitch Client Secret setting placeholder</note>
</trans-unit>
<trans-unit id="4c9a15ab7fb3dce1002ea7aea4ecada3c1ee12e9" datatype="html">
<source>Generating an ID/secret is easy!</source>
<target state="translated">Générer un identifiant/secret est facile !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">267</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting hint</note>
</trans-unit>
<trans-unit id="2600933489084742998" datatype="html">
<source>Failed to kill all downloads! Check logs for details.</source>
<target state="translated">Échec d'élimination de tous les téléchargements ! Voir le journal pour plus de détails.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">295</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="4257962986336738751" datatype="html">
<source>Successfully killed all downloads!</source>
<target state="translated">Tous les téléchargements éliminés avec succès !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">292</context>
</context-group>
</trans-unit>
<trans-unit id="9208873922277364009" datatype="html">
<source>Failed to update categories!</source>
<target state="translated">Échec de mise à jour des catégories !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="7180231139026789468" datatype="html">
<source>Language successfully changed! Reload to update the page.</source>
<target state="translated">La langue a été changée avec succès ! Rechargez la page.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">209</context>
</context-group>
</trans-unit>
<trans-unit id="4604336107574138791" datatype="html">
<source>Chrome users must drag the 'Alternate URL' link to your bookmarks.</source>
<target state="translated">Les utilisateurs de Chrome doivent glisser le lien "Alternate URL" depuis leurs favoris.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">237</context>
</context-group>
</trans-unit>
<trans-unit id="5681417617361245213" datatype="html">
<source>Failed to transfer DB -- transfer was aborted. Error:</source>
<target state="translated">Échec de transfert de la base de donnée -- transfert annulé. Erreur :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">343</context>
</context-group>
</trans-unit>
<trans-unit id="1942965859829798388" datatype="html">
<source>Restarting!</source>
<target state="translated">Redémarrage !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">307</context>
</context-group>
</trans-unit>
<trans-unit id="6224607866493148072" datatype="html">
<source>Failed to restart the server.</source>
<target state="translated">Échec de redémarrage du serveur.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">309</context>
</context-group>
</trans-unit>
<trans-unit id="6123898845299902958" datatype="html">
<source>Successfully transfered DB! Reloading info...</source>
<target state="translated">Base de donnée transférée avec succès ! Rechargement des informations...</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">340</context>
</context-group>
</trans-unit>
<trans-unit id="4021495815084152271" datatype="html">
<source>Connection failed! Error:</source>
<target state="translated">Échec de connexion ! Erreur :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="6519219215739537829" datatype="html">
<source>Connection failed! Error: Server error. See logs for more info.</source>
<target state="translated">Échec de connexion ! Erreur : erreur serveur. Voir les journaux pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">363</context>
</context-group>
</trans-unit>
<trans-unit id="fd374bee82dab66055b933496860f82b7b780159" datatype="html">
<source>Global custom args for downloads on the home page. (Set args for subscriptions for each subscriptions separately!) Args are delimited using two commas like so: ,,</source>
<target state="translated">Les arguments personnalisés globaux pour les téléchargement de la page d'accueil. (Définir ces arguments pour chaque souscription séparément !). Les arguments sont séparés par deux virgules : ",,"</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">128</context>
</context-group>
<note priority="1" from="description">Custom args setting input hint</note>
</trans-unit>
<trans-unit id="4744991787069301975" datatype="html">
<source>Share files</source>
<target state="translated">Partager les fichiers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="1812379335568847528" datatype="html">
<source>Subscriptions</source>
<target state="translated">Souscriptions</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="4278268519633335280" datatype="html">
<source>Use downloads manager</source>
<target state="translated">Utiliser le gestionnaire de téléchargements</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="571023367671104036" datatype="html">
<source>Failed to restart download! See server logs for more info.</source>
<target state="translated">Échec de redémarrage du téléchargement ! Voir le journal du serveur pour plus d'informations.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="998793c78895d53576f462054f13d6e515e923d9" datatype="html">
<source>Update task schedule</source>
<target state="translated">Mise à jour du planificateur de tâches</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Update task schedule</note>
</trans-unit>
<trans-unit id="5823550543348347814" datatype="html">
<source>Cleared downloads!</source>
<target state="translated">Téléchargements effacés !</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2787,8 +2787,8 @@
<note priority="1" from="description">Confirm Password</note>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target state="translated">kesalahan untuk <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">kesalahan untuk<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">259</context>
@@ -4053,6 +4053,86 @@
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="translated">berhasil membuat paftar putar!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="translated">KESALAHAN: gagal membuat daftar putar!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="1812379335568847528" datatype="html">
<source>Subscriptions</source>
<target state="translated">berlangganan</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="4744991787069301975" datatype="html">
<source>Share files</source>
<target state="translated">berbagi file</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="4278268519633335280" datatype="html">
<source>Use downloads manager</source>
<target state="translated">gunakan manager unduhan</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="3426988753455920032" datatype="html">
<source>Use tasks manager</source>
<target state="translated">gunakan manager tugas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="5029464708330583545" datatype="html">
<source>Use advanced download mode</source>
<target state="translated">gunakan mode unduh lanjutan</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="820184305380634591" datatype="html">
<source>Playlist successfully removed.</source>
<target state="translated">daftar putar berhasil dihapus.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="4250042184551786923" datatype="html">
<source>File manager</source>
<target state="translated">manager file</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="8525406024045742391" datatype="html">
<source>Settings access</source>
<target state="translated">akses pengaturan</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2656,8 +2656,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target state="translated">Errore per <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Errore per<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>
@@ -4039,6 +4039,86 @@
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="translated">Playlist creata con successo!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="820184305380634591" datatype="html">
<source>Playlist successfully removed.</source>
<target state="translated">Playlist rimossa con successo.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="4250042184551786923" datatype="html">
<source>File manager</source>
<target state="translated">Gestore di file</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5029464708330583545" datatype="html">
<source>Use advanced download mode</source>
<target state="translated">Usa la modalità di download avanzata</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="4278268519633335280" datatype="html">
<source>Use downloads manager</source>
<target state="translated">Usa il gestore dei download</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="3426988753455920032" datatype="html">
<source>Use tasks manager</source>
<target state="translated">Usa il gestore delle attività</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="1812379335568847528" datatype="html">
<source>Subscriptions</source>
<target state="translated">Iscrizioni</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="4744991787069301975" datatype="html">
<source>Share files</source>
<target state="translated">Condividi file</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="8525406024045742391" datatype="html">
<source>Settings access</source>
<target state="translated">Accesso alle impostazioni</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="translated">ERRORE: fallita la creazione della playlist!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en-US" datatype="plaintext" original="ng2.template" target-language="ko">
<body>
@@ -2836,8 +2836,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="}); retu"/></source>
<target><x id="url" equiv-text="}); retu"/> 에 대한 에러</target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated"><x id="url" equiv-text="download['url']"/>에 대한 오류</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>
@@ -3077,6 +3077,128 @@
<context context-type="linenumber">469</context>
</context-group>
</trans-unit>
<trans-unit id="8485375438204712002" datatype="html">
<source>Finished downloads</source>
<target state="needs-translation">완료된 다운로드</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">138</context>
</context-group>
</trans-unit>
<trans-unit id="5801924165267871854" datatype="html">
<source>Paused downloads</source>
<target state="needs-translation">일시중지된 다운로드</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="2723988842145709249" datatype="html">
<source>Errored downloads</source>
<target state="needs-translation">다운로드 오류</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="7114033980971410157" datatype="html">
<source>Failed to pause download! See server logs for more info.</source>
<target state="needs-translation">다운로드를 일시 중지하지 못했습니다! 자세한 내용은 서버 로그를 참조하세요.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">170</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="5223827577229167333" datatype="html">
<source>Failed to pause all downloads! See server logs for more info.</source>
<target state="needs-translation">모든 다운로드를 일시 중지하지 못했습니다! 자세한 내용은 서버 로그를 참조하세요.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="5456775416888155476" datatype="html">
<source>Failed to resume download! See server logs for more info.</source>
<target state="needs-translation">다운로드를 재개하지 못했습니다! 자세한 내용은 서버 로그를 참조하세요.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="6268791413935580107" datatype="html">
<source>Failed to resume all downloads! See server logs for more info.</source>
<target state="needs-translation">모든 다운로드를 다시 시작 실패! 더 많은 정보를 위해 서버 로그를 참조하십시오.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="c1b7e6d75ff4285c7636c67e5ef259629b81725b" datatype="html">
<source>Confirm Password</source>
<target state="needs-translation">비밀번호 확인</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/login/login.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
<note priority="1" from="description">Confirm Password</note>
</trans-unit>
<trans-unit id="1019978815798793544" datatype="html">
<source>Failed to retrieve logs!</source>
<target state="needs-translation">로그를 검색하지 못했습니다!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">51</context>
</context-group>
</trans-unit>
<trans-unit id="87406377200084623" datatype="html">
<source>Logs copied to clipboard!</source>
<target state="needs-translation">로그가 클립보드에 복사되었습니다!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="586a5fd72602b5b14ec0c55f84814de47bb21e3a" datatype="html">
<source>Tasks</source>
<target state="needs-translation">작업</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<note priority="1" from="description">Navigation menu Tasks Page title</note>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="needs-translation">재생 목록을 성공적으로 만들었습니다!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="needs-translation">오류: 재생 목록을 만들지 못했습니다!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="820184305380634591" datatype="html">
<source>Playlist successfully removed.</source>
<target state="translated">재생목록이 성공적으로 삭제되었습니다.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -2496,8 +2496,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target>Грешка за <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Грешка за<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>

View File

@@ -2884,6 +2884,31 @@
</context-group>
<note priority="1" from="description">Restart</note>
</trans-unit>
<trans-unit id="586a5fd72602b5b14ec0c55f84814de47bb21e3a" datatype="html">
<source>Tasks</source>
<target state="translated">Gjøremål</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<note priority="1" from="description">Navigation menu Tasks Page title</note>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Playlist created.</source>
<target state="needs-translation">Spilleliste opprettet.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="needs-translation">Feil: Klarte ikke å opprette spilleliste.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1681,7 +1681,7 @@
</trans-unit>
<trans-unit id="00a94f58d9eb2e3aa561440eabea616d0c937fa2" datatype="html">
<source>This will delete your old API key!</source>
<target>Let op: hiermee verwijder je je oude api-sleutel!</target>
<target state="translated">Hiermee verwijder je je oude API-sleutel!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">223</context>
@@ -1717,7 +1717,7 @@
</trans-unit>
<trans-unit id="8602e313cdfa7c4cc475ccbe86459fce3c3fd986" datatype="html">
<source>Generating a key is easy!</source>
<target>Het genereren van een sleutel is eenvoudig.</target>
<target state="translated">Een sleutel genereren is eenvoudig!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">237</context>
@@ -2854,8 +2854,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target>Foutmelding bij <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Foutmelding bij<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>
@@ -2895,7 +2895,7 @@
</trans-unit>
<trans-unit id="7724483709075923163" datatype="html">
<source>Downloading file</source>
<target>Bezig met downloaden…</target>
<target state="translated">Bestand wordt gedownload</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">60</context>
@@ -2903,7 +2903,7 @@
</trans-unit>
<trans-unit id="4027175717527633324" datatype="html">
<source>Getting info</source>
<target>Bezig met ophalen van informatie…</target>
<target state="translated">Informatie wordt opgehaald</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">59</context>
@@ -2911,7 +2911,7 @@
</trans-unit>
<trans-unit id="2827589726081052618" datatype="html">
<source>Creating download</source>
<target>Bezig met samenstellen…</target>
<target state="translated">Download wordt gegenereerd</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">58</context>
@@ -3128,7 +3128,7 @@
</trans-unit>
<trans-unit id="698f716f927f633b16cbe2308d90b2dba51f7556" datatype="html">
<source>Last confirmed</source>
<target>Laatst bevestigd:</target>
<target state="translated">Laatst bevestigd</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">25</context>
@@ -3182,7 +3182,7 @@
</trans-unit>
<trans-unit id="9562dd1e902acc4cc6a6e09cba66a2775daf7794" datatype="html">
<source>Update binary to:</source>
<target>Uitvoerbaar bestand bijwerken naar</target>
<target state="translated">Binaries bijwerken naar:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">63</context>
@@ -3314,7 +3314,7 @@
</trans-unit>
<trans-unit id="e4c3cd97461b220b9cea67cfe08c5399799cfeed" datatype="html">
<source>Last ran</source>
<target>Laatst uitgevoerd:</target>
<target state="translated">Laatst uitgevoerd</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">16</context>
@@ -3394,6 +3394,754 @@
</context-group>
<note priority="1" from="description">Custom args setting input hint</note>
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="translated">Afspeellijst succesvol aangemaakt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2070856663109337061" datatype="html">
<source>ERROR: failed to create playlist!</source>
<target state="translated">ERROR: afspeellijst maken mislukt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="820184305380634591" datatype="html">
<source>Playlist successfully removed.</source>
<target state="translated">Afspeellijst succesvol verwijderd.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="4250042184551786923" datatype="html">
<source>File manager</source>
<target state="translated">Bestandsbeheer</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="8525406024045742391" datatype="html">
<source>Settings access</source>
<target state="translated">Toegang tot instellingen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="1812379335568847528" datatype="html">
<source>Subscriptions</source>
<target state="translated">Abonnementen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="4744991787069301975" datatype="html">
<source>Share files</source>
<target state="translated">Bestanden delen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="5029464708330583545" datatype="html">
<source>Use advanced download mode</source>
<target state="translated">Gebruik geavanceerde download modus</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="3426988753455920032" datatype="html">
<source>Use tasks manager</source>
<target state="translated">Gebruik taak manager</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="52e0fa8ada52c3f29774a4508582fd98250b9f93" datatype="html">
<source>My files</source>
<target state="translated">Mijn bestanden</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">My files title</note>
</trans-unit>
<trans-unit id="8937901770314883418" datatype="html">
<source>Successfully deleted file:</source>
<target state="translated">Bestand succesvol verwijderd:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">291</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="3481862581074838726" datatype="html">
<source>VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"</source>
<target state="translated">VOD url voor deze video wordt niet ondersteund. VOD ID moet achter "twitch.tv/videos/"</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="2159130950882492111" datatype="html">
<source>Cancel</source>
<target state="translated">Annuleren</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/confirm-dialog/confirm-dialog.component.ts</context>
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="1709839462010459086" datatype="html">
<source>Cookies successfully uploaded!</source>
<target state="translated">Cookies succesvol geüpload!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="a4a4a5f03d7d0831ccf6774094e66a9507a42b58" datatype="html">
<source>Clear downloads</source>
<target state="translated">Downloads wissen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">85</context>
</context-group>
<note priority="1" from="description">Clear downloads</note>
</trans-unit>
<trans-unit id="3299455901271096793" datatype="html">
<source>Clear downloads</source>
<target state="translated">Downloads wissen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">131</context>
</context-group>
</trans-unit>
<trans-unit id="5215119607776782829" datatype="html">
<source>Select downloads to clear</source>
<target state="translated">Selecteer downloads om te wissen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="8485375438204712002" datatype="html">
<source>Finished downloads</source>
<target state="translated">Afgeronde downloads</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">138</context>
</context-group>
</trans-unit>
<trans-unit id="5801924165267871854" datatype="html">
<source>Paused downloads</source>
<target state="translated">Gepauzeerde downloads</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="2723988842145709249" datatype="html">
<source>Errored downloads</source>
<target state="translated">Mislukte downloads</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="3961621815065792326" datatype="html">
<source>Failed to clear finished downloads!</source>
<target state="translated">Afgeronde downloads wissen is mislukt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">157</context>
</context-group>
</trans-unit>
<trans-unit id="5823550543348347814" datatype="html">
<source>Cleared downloads!</source>
<target state="translated">Downloads gewist!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="7114033980971410157" datatype="html">
<source>Failed to pause download! See server logs for more info.</source>
<target state="translated">Download pauzeren is mislukt! Zie de server logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">170</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="5456775416888155476" datatype="html">
<source>Failed to resume download! See server logs for more info.</source>
<target state="translated">Download hervatten is mislukt! Zie de server logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="5223827577229167333" datatype="html">
<source>Failed to pause all downloads! See server logs for more info.</source>
<target state="translated">Alle downloads pauzeren is mislukt! Zie de server logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="6268791413935580107" datatype="html">
<source>Failed to resume all downloads! See server logs for more info.</source>
<target state="translated">Alle downloads hervatten is mislukt! Zie de server logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="4529487534884306633" datatype="html">
<source>Failed to cancel download! See server logs for more info.</source>
<target state="translated">Het stoppen van de download is mislukt! Zie de server logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">210</context>
</context-group>
</trans-unit>
<trans-unit id="571023367671104036" datatype="html">
<source>Failed to restart download! See server logs for more info.</source>
<target state="translated">Het herstarten van de download is mislukt! Zie de server logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="c1b7e6d75ff4285c7636c67e5ef259629b81725b" datatype="html">
<source>Confirm Password</source>
<target state="translated">Bevestig wachtwoord</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/login/login.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
<note priority="1" from="description">Confirm Password</note>
</trans-unit>
<trans-unit id="1019978815798793544" datatype="html">
<source>Failed to retrieve logs!</source>
<target state="translated">Het verkrijgen van de logs is mislukt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">51</context>
</context-group>
</trans-unit>
<trans-unit id="87406377200084623" datatype="html">
<source>Logs copied to clipboard!</source>
<target state="translated">Logs gekopieerd naar het klembord!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="2293081271355999967" datatype="html">
<source>Logs successfully cleared!</source>
<target state="translated">Logs succesvol gewist!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="4516710756538206828" datatype="html">
<source>Failed to clear logs!</source>
<target state="translated">Logs wissen mislukt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">77</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/logs-viewer/logs-viewer.component.ts</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="5704ec2049d007c5f5fb495a5d8b607e68d58081" datatype="html">
<source>Order</source>
<target state="translated">Volgorde</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">53</context>
</context-group>
<note priority="1" from="description">Order</note>
</trans-unit>
<trans-unit id="4050356167294261426" datatype="html">
<source>Delete success!</source>
<target state="translated">Succesvol verwijderd!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">270</context>
</context-group>
</trans-unit>
<trans-unit id="8348223454028662277" datatype="html">
<source>OK.</source>
<target state="translated">Oke.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">270</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="7405156667148936748" datatype="html">
<source>Delete failed!</source>
<target state="translated">Verwijderen mislukt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">273</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="880407735794041263" datatype="html">
<source>Download failed.</source>
<target state="translated">Download mislukt.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="7157191502004604261" datatype="html">
<source>Chat could not be downloaded.</source>
<target state="translated">Chat kon niet worden gedownload.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.ts</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="e58f5716d6c08b6a841eb003c9f9774b5c5d34a9" datatype="html">
<source>Delete and don't download again</source>
<target state="translated">Verwijder en download niet weer</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">Delete forever subscription video button</note>
</trans-unit>
<trans-unit id="9203653061903371757" datatype="html">
<source>Playlist updated successfully.</source>
<target state="translated">Afspeellijst succesvol geüpdatet.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
<context context-type="linenumber">69</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/create-playlist/create-playlist.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="4278268519633335280" datatype="html">
<source>Use downloads manager</source>
<target state="translated">Gebruik download manager</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage-role/manage-role.component.ts</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="6827066f436adfc56a142d5816a8be6113d73b01" datatype="html">
<source>No files found.</source>
<target state="translated">Geen bestanden gevonden.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">No files found</note>
</trans-unit>
<trans-unit id="ae9a5141f5c6bd62cee4ce837598ea8b0904e5cf" datatype="html">
<source>Select files</source>
<target state="translated">Selecteer bestanden</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">71</context>
</context-group>
<note priority="1" from="description">Select files</note>
</trans-unit>
<trans-unit id="3480433876298276350" datatype="html">
<source>Database successfully restored!</source>
<target state="translated">Database succesvol hersteld!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="1946323844380374711" datatype="html">
<source>Failed to restore database! See browser console for more info.</source>
<target state="translated">Database kon niet worden hersteld! Zie browser console voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="4960870191807928282" datatype="html">
<source>Sharing enabled.</source>
<target state="translated">Delen geactiveerd.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="2720327817780634026" datatype="html">
<source>Failed to enable sharing.</source>
<target state="translated">Delen kon niet worden geactiveerd.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">71</context>
</context-group>
</trans-unit>
<trans-unit id="3561468911579213356" datatype="html">
<source>Sharing disabled.</source>
<target state="translated">Delen gedeactiveerd.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="7840375760456214518" datatype="html">
<source>Failed to disable sharing.</source>
<target state="translated">Delen kon niet worden gedeactiveerd.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="8692976466689769553" datatype="html">
<source>Failed to disable sharing - server error.</source>
<target state="translated">Delen kon niet worden gedeactiveerd - serverfout.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="5397815846940616259" datatype="html">
<source>You must specify an amount of time</source>
<target state="translated">Je moet een tijdsduur invoeren</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="3544790314111256717" datatype="html">
<source>ERROR:</source>
<target state="translated">FOUT:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.ts</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="f8c7be184fefd6750e4e5d0c7a90e74721c58f8a" datatype="html">
<source>Uploader</source>
<target state="translated">Uploader</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Uploader</note>
</trans-unit>
<trans-unit id="989f5aa799ee9672675d68109bff29d1d88ebd49" datatype="html">
<source>Local view count</source>
<target state="translated">Aantal keer lokaal bekeken</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Local view count</note>
</trans-unit>
<trans-unit id="1942965859829798388" datatype="html">
<source>Restarting!</source>
<target state="translated">Aan het herstarten!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">307</context>
</context-group>
</trans-unit>
<trans-unit id="5681417617361245213" datatype="html">
<source>Failed to transfer DB -- transfer was aborted. Error:</source>
<target state="translated">Kon niet naar DB worden overgedragen -- overdracht afgebroken. Fout:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">343</context>
</context-group>
</trans-unit>
<trans-unit id="4021495815084152271" datatype="html">
<source>Connection failed! Error:</source>
<target state="translated">Verbinding mislukt! Fout:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="6519219215739537829" datatype="html">
<source>Connection failed! Error: Server error. See logs for more info.</source>
<target state="translated">Verbinding mislukt! Fout: Serverfout. Zie logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">363</context>
</context-group>
</trans-unit>
<trans-unit id="643438049907907768" datatype="html">
<source>Failed to restore database! See logs for more info.</source>
<target state="translated">Database kon niet worden hersteld! Zie logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/restore-db-dialog/restore-db-dialog.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="2859348955905483094" datatype="html">
<source>Failed to enable sharing - server error.</source>
<target state="translated">Delen kon niet worden geactiveerd - serverfout.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.ts</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="2876893175497409225" datatype="html">
<source>Update failed. Check logs for more details.</source>
<target state="translated">Update mislukt. Zie logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-progress-dialog/update-progress-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="3317b8688eb2cfabc4021cd7b2926b32f3864ad2" datatype="html">
<source>Choose a date</source>
<target state="translated">Kies een datum</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html</context>
<context context-type="linenumber">22</context>
</context-group>
<note priority="1" from="description">Choose a date</note>
</trans-unit>
<trans-unit id="d49d5d6786b69d140e20cfddfe29690a19641a88" datatype="html">
<source>Thumbnail path</source>
<target state="translated">Thumbnail-pad</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Thumbnail path</note>
</trans-unit>
<trans-unit id="f4003b626fcbf3a871778d4dba166e109d02f87c" datatype="html">
<source>Thumbnail URL</source>
<target state="translated">Thumbnail-url</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
<note priority="1" from="description">Thumbnail URL</note>
</trans-unit>
<trans-unit id="9fc54db2830fbbd332b1adebe28e9283069107ef" datatype="html">
<source>Audio bitrate:</source>
<target state="translated">Audio bitrate:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">55</context>
</context-group>
<note priority="1" from="description">Video audio bitrate property</note>
</trans-unit>
<trans-unit id="2734512985872312443" datatype="html">
<source>Failed to get file information from the server.</source>
<target state="translated">Bestandsinformatie kon niet van de server worden verkregen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="6789263921624845085" datatype="html">
<source>Failed to load playlist!</source>
<target state="translated">Speellijst kon niet worden geladen!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">186</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.ts</context>
<context context-type="linenumber">189</context>
</context-group>
</trans-unit>
<trans-unit id="8314249599019746316" datatype="html">
<source>Download failed!</source>
<target state="translated">Download mislukt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">387</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">775</context>
</context-group>
</trans-unit>
<trans-unit id="4c9a15ab7fb3dce1002ea7aea4ecada3c1ee12e9" datatype="html">
<source>Generating an ID/secret is easy!</source>
<target state="translated">Een ID/geheim genereren is eenvoudig!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">267</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting hint</note>
</trans-unit>
<trans-unit id="8506540da14d205ea092b4c856e242ed7f500643" datatype="html">
<source>Twitch Client Secret</source>
<target state="translated">Twitch Client geheim</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
</context-group>
<note priority="1" from="description">Twitch Client Secret setting placeholder</note>
</trans-unit>
<trans-unit id="5d78fe9ba69a8710613d3f7c35b22e9c8226e4dc" datatype="html">
<source>Twitch Client ID</source>
<target state="translated">Twitch Client ID</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">266</context>
</context-group>
<note priority="1" from="description">Twitch Client ID setting placeholder</note>
</trans-unit>
<trans-unit id="9208873922277364009" datatype="html">
<source>Failed to update categories!</source>
<target state="translated">Categorieën konden niet worden vernieuwd!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="4604336107574138791" datatype="html">
<source>Chrome users must drag the 'Alternate URL' link to your bookmarks.</source>
<target state="translated">Chrome-gebruikers moeten de "Alternate URL"-link naar hun bladwijzers slepen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">237</context>
</context-group>
</trans-unit>
<trans-unit id="4257962986336738751" datatype="html">
<source>Successfully killed all downloads!</source>
<target state="translated">Alle downloads succesvol gestopt!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">292</context>
</context-group>
</trans-unit>
<trans-unit id="7180231139026789468" datatype="html">
<source>Language successfully changed! Reload to update the page.</source>
<target state="translated">Taal succesvol gewijzigd! Herlaad de pagina om toe te passen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">209</context>
</context-group>
</trans-unit>
<trans-unit id="2600933489084742998" datatype="html">
<source>Failed to kill all downloads! Check logs for details.</source>
<target state="translated">Niet alle downloads konden worden gestopt! Zie logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">295</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="6123898845299902958" datatype="html">
<source>Successfully transfered DB! Reloading info...</source>
<target state="translated">DB succesvol overgezet! Info wordt herladen...</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">340</context>
</context-group>
</trans-unit>
<trans-unit id="6224607866493148072" datatype="html">
<source>Failed to restart the server.</source>
<target state="translated">De server kon niet worden herstart.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">309</context>
</context-group>
</trans-unit>
<trans-unit id="1716030487077666916" datatype="html">
<source>Failed to transfer DB -- API call failed. See browser logs for details.</source>
<target state="translated">Kon niet naar DB worden overgedragen -- API call mislukt. Zie browser logs voor meer informatie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">347</context>
</context-group>
</trans-unit>
<trans-unit id="6018050954136387828" datatype="html">
<source>Connection successful!</source>
<target state="translated">Verbinding geslaagd!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">357</context>
</context-group>
</trans-unit>
<trans-unit id="3f741a2c015bb728088b630296ca401e823c6af8" datatype="html">
<source>View count</source>
<target state="translated">Aantal keer bekeken</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
<note priority="1" from="description">View count</note>
</trans-unit>
<trans-unit id="22bac71dbdc1ac62607135994f81cca8094cb251" datatype="html">
<source>Upload date</source>
<target state="translated">Uploaddatum</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Upload date</note>
</trans-unit>
<trans-unit id="607de17c2a755f65775881c19e276e7c933bcf94" datatype="html">
<source>Category</source>
<target state="translated">Categorie</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
<note priority="1" from="description">Category</note>
</trans-unit>
<trans-unit id="9fa37704969eeebd496a172c5077370f569df3ae" datatype="html">
<source>Resolution:</source>
<target state="translated">Resolutie:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Video resolution property</note>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

View File

@@ -487,7 +487,7 @@
</trans-unit>
<trans-unit id="98b6ec9ec138186d663e64770267b67334353d63" datatype="html">
<source>Custom file output</source>
<target>Saída de arquivo personalizado</target>
<target state="translated">Saída de ficheiro personalizado</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">66</context>
@@ -709,7 +709,7 @@
</trans-unit>
<trans-unit id="109c6f4a5e46efb933612ededfaf52a13178b7e0" datatype="html">
<source>File size:</source>
<target>Tamanho do arquivo:</target>
<target state="translated">Tamanho do ficheiro:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">17</context>
@@ -1519,7 +1519,7 @@
</trans-unit>
<trans-unit id="cfe829634b1144bc44b6d38cf5584ea65db9804f" datatype="html">
<source>Default file output</source>
<target>Arquivo de destino padrão</target>
<target state="translated">Ficheiro de destino padrão</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">123</context>
@@ -1609,7 +1609,7 @@
</trans-unit>
<trans-unit id="78d3531417c0d4ba4c90f0d4ae741edc261ec8df" datatype="html">
<source>File manager enabled</source>
<target>Habilitar gerenciador de arquivos</target>
<target state="translated">Gestor de ficheiros ativado</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">193</context>
@@ -2008,7 +2008,7 @@
</trans-unit>
<trans-unit id="199c17e5d6a419313af3c325f06dcbb9645ca618" datatype="html">
<source>is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos.</source>
<target>é um downloader open-source para o Youtube feito nas especificações do Mateerial Design da Google. Você pode baixar seus vídeos favoritos como arquivos de vídeo ou áudio, e até se inscrever nos seus canais ou playlists favoritos para ficar atualizado com os novos vídeos publicados.</target>
<target state="translated">é um descarregador open-source para o Youtube feito nas especificações do Mateerial Design da Google. Pode descarregar os seus vídeos favoritos como ficheiros de vídeo ou áudio e até inscrever-se nos seus canais ou playlists favoritos para ficar atualizado com os novos vídeos publicados.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">12</context>
@@ -2434,7 +2434,7 @@
</trans-unit>
<trans-unit id="ccf5ea825526ac490974336cb5c24352886abc07" datatype="html">
<source>Open file</source>
<target>Abrir arquivo</target>
<target state="translated">Abrir ficheiro</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">18</context>
@@ -2443,7 +2443,7 @@
</trans-unit>
<trans-unit id="5656a06f17c24b2d7eae9c221567b209743829a9" datatype="html">
<source>Open file in new tab</source>
<target>Abrir arquivo em uma nova aba</target>
<target state="translated">Abrir ficheiro numa nova aba</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">19</context>
@@ -2532,7 +2532,7 @@
</trans-unit>
<trans-unit id="b4e61d531b8db72449f043f122119da964f4fc54" datatype="html">
<source>File type</source>
<target>Tipo do arquivo</target>
<target state="translated">Tipo do ficheiro</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.html</context>
<context context-type="linenumber">52</context>
@@ -2603,7 +2603,7 @@
</trans-unit>
<trans-unit id="e243d8408e1a3d2ef6501b85899d02374ea34880" datatype="html">
<source>Generates NFO files with every download, primarily used by Kodi.</source>
<target>Gera arquivos NFO em todos os downloads, usado primariamente pelo Kodi.</target>
<target state="translated">Gera ficheiros NFO em todas as descargas, usado primariamente pelo Kodi.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
@@ -2726,7 +2726,7 @@
</trans-unit>
<trans-unit id="4e1291cb1d579e7b7a1b802e6a8fd16ef7a557fa" datatype="html">
<source>Crop file</source>
<target>Cortar arquivo</target>
<target state="translated">Cortar ficheiro</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">160,161</context>
@@ -2824,7 +2824,7 @@
</trans-unit>
<trans-unit id="e65fce85d6177d3582b376f9d622daebca5ee0da" datatype="html">
<source>Generate NFO files</source>
<target>Gerar arquivos NFO</target>
<target state="translated">Gerar ficheiros NFO</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
@@ -2878,7 +2878,7 @@
</trans-unit>
<trans-unit id="3913164a51898aac444bf6c7150e46ad5a8a18ad" datatype="html">
<source>MongoDB Connection String</source>
<target>String de conexão do MongoDB</target>
<target state="translated">Cadeia de conexão do MongoDB</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">318</context>
@@ -2896,7 +2896,7 @@
</trans-unit>
<trans-unit id="d54142de169844b014ae913a4056c31495f4a305" datatype="html">
<source>Test connection string</source>
<target>Testar string de conexão</target>
<target state="translated">Testar cadeia de conexão</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">323</context>
@@ -2923,7 +2923,7 @@
</trans-unit>
<trans-unit id="94e2674467c7a08a291f9bd97ce694d4e47ffd62" datatype="html">
<source>Share file</source>
<target>Compartilhar arquivo</target>
<target state="translated">Compartilhar ficheiro</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/share-media-dialog/share-media-dialog.component.html</context>
<context context-type="linenumber">3</context>
@@ -2948,7 +2948,7 @@
</trans-unit>
<trans-unit id="7724483709075923163" datatype="html">
<source>Downloading file</source>
<target>Baixando arquivo</target>
<target state="translated">A descarregar ficheiro</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">60</context>
@@ -2987,8 +2987,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="}); retu"/></source>
<target>Erro para <x id="url" equiv-text="}); retu"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Erro para<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>

View File

@@ -3021,8 +3021,8 @@
<note priority="1" from="description">Progress</note>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target>Ошибка с <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Ошибка с<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>

View File

@@ -350,8 +350,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="download['url']"/></source>
<target state="translated">Fel för <x id="url" equiv-text="download['url']"/></target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated">Fel för<x id="url" equiv-text="download['url']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>

View File

@@ -2496,8 +2496,8 @@
</context-group>
</trans-unit>
<trans-unit id="2560364143605631750" datatype="html">
<source>Error for <x id="url" equiv-text="}); retu"/></source>
<target><x id="url" equiv-text="}); retu"/> కోసం లోపం</target>
<source>Error for<x id="url" equiv-text="download['url']"/></source>
<target state="translated"><x id="url" equiv-text="download['url']"/>కోసం లోపం</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">238</context>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff