Compare commits

...

68 Commits

Author SHA1 Message Date
semantic-release-bot
7e0e2a92e3 chore(release): 1.24.0 2021-05-05 08:57:48 +00:00
sct
e840489e57 Merge branch 'develop' 2021-05-05 17:50:27 +09:00
sct
800f4668ec chore: fix gitattributes for images [skip ci] 2021-05-05 17:49:55 +09:00
Weblate (bot)
93c441ef66 feat(lang): translations update from Weblate (#1566)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 95.4% (824 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.4% (824 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Dabu <clarissy.andrea@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (863 of 863 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 99.0% (855 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 99.0% (851 of 859 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 97.3% (840 of 863 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 97.2% (839 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (863 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (859 of 859 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (863 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Dabu <clarissy.andrea@gmail.com>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
2021-05-05 01:00:38 -04:00
TheCatLady
549103f6f6 fix(locale): default user locale should be the server setting (#1574)
* fix(locale): default user locale should be the server setting

* fix(db): fix user settings migration

* fix(db): set default locale to empty string

* revert: revert change to usersettings.ts
2021-05-05 13:33:43 +09:00
TheCatLady
e2acf8887c fix(ui): default to text input type for SensitiveInputs (#1568) 2021-05-05 00:24:28 +09:00
TheCatLady
69ab7cc660 feat(email): replace 'Enable SSL' setting with more descriptive/clear 'Encryption Method' setting (#1549)
* feat(email): replace 'Enable SSL' setting with more descriptive/clear 'Encryption Method' setting

* fix: clarify settings & add true 'none' option
2021-05-04 10:31:33 +00:00
sct
11dc8ff92a build(deps): revert to next 10.1.3 2021-05-04 19:22:58 +09:00
TheCatLady
e912a00880 fix(ui): correct RegionSelector z-index (#1567) 2021-05-04 18:45:33 +09:00
TheCatLady
f8b2c3c1b1 build(deps): bump openpgp from 5.0.0-1 to 5.0.0-2 (#1565) 2021-05-04 09:23:20 +00:00
TheCatLady
26580eaa21 fix(ui): improve form usability (#1563)
* fix(ui): improve form usability

* refactor: remove unnecessary <> and </> tags

* fix(ui): set url inputmode for *arr URL base fields
2021-05-04 17:42:27 +09:00
TheCatLady
b05b177776 fix(webpush): only prompt user to allow notifications if enabled in user settings (#1552) 2021-05-04 03:40:32 +00:00
Weblate (bot)
6c9991d474 feat(lang): translations update from Weblate (#1558)
* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (866 of 866 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (865 of 865 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (866 of 866 strings)

Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 97.6% (846 of 866 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (866 of 866 strings)

Co-authored-by: Kobe <kobaubarr@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
2021-05-03 23:14:39 -04:00
TheCatLady
6c34bb2908 refactor(svg): inline *arr logos and optimize SVG assets (#1562) 2021-05-04 02:26:40 +00:00
sct
5b2300c28c build(deps): bump dependencies 2021-05-04 11:14:40 +09:00
TheCatLady
1123fce089 fix(ui): apply rounded-l-only to SensitiveInput textareas and increase visible text input area (#1561) 2021-05-04 09:37:51 +09:00
TheCatLady
e4d0029f7b fix(ui): add user profile links to RequestBlock and change 'ETA' string in DownloadBlock (#1551) 2021-05-03 13:21:45 +00:00
TheCatLady
2fc9835a6a build(docker): refactor Dockerfile to enable caching of node_modules (#1550)
* ci: revert actions/cache version change and install GNU tar

* ci: do not potentially destroy Docker cache while building preview images

* build(docker): refactor Dockerfile to cache node_modules

* ci: remove Next.js caching

* build(docker): don't break arm builds
2021-05-03 22:13:23 +09:00
TheCatLady
f256a444c5 feat: add server default locale setting (#1536)
* feat: add server default locale setting

* fix: do not modify defaultLocale property of IntlProvider
2021-05-03 13:11:28 +00:00
Jakob Ankarhem
4fd452dd18 feat(inputs): add support for toggling security on input fields (#1404) 2021-05-03 21:59:52 +09:00
Weblate (bot)
e3d5e33ec3 feat(lang): translated using Weblate (Spanish) (#1553)
Currently translated at 100.0% (863 of 863 strings)

Co-authored-by: Ricardo González <notorius28@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Ricardo González <notorius28@gmail.com>
2021-05-02 15:00:31 -04:00
TheCatLady
189313e94a fix: correctly fall back to English name in LanguageSelector (#1537)
* fix: correctly fall back to English name in LanguageSelector

* refactor: clean up language sort & name logic

* refactor: also clean up region sort & name logic

* refactor: use arrow functions
2021-04-30 18:02:59 +00:00
TheCatLady
b7b55e275c fix(backend): properly set request media status (#1541) 2021-05-01 00:43:42 +09:00
TheCatLady
ad7375ec50 ci: use actions/cache@v2.1.0 instead of v2.1.5 (#1502) 2021-04-30 22:54:56 +09:00
TheCatLady
5e588be812 fix(ui): add missing margins on button SVGs on Plex Settings page (#1546) 2021-04-30 17:38:47 +09:00
Weblate (bot)
1a6d4bddc0 feat(lang): translations update from Weblate (#1527)
* feat(lang): translated using Weblate (French)

Currently translated at 97.3% (840 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 96.4% (832 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 96.4% (832 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 96.1% (830 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 96.1% (830 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 96.0% (829 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 96.0% (829 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.7% (826 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.7% (826 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.3% (823 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.3% (823 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.2% (822 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.2% (822 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.1% (821 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.1% (821 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.0% (820 of 863 strings)

feat(lang): translated using Weblate (French)

Currently translated at 95.0% (820 of 863 strings)

Co-authored-by: Dabu <clarissy.andrea@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 95.8% (827 of 863 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (863 of 863 strings)

Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 39.6% (342 of 863 strings)

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Dabu <clarissy.andrea@gmail.com>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2021-04-29 09:56:23 -04:00
TheCatLady
b2e37955ac refactor(pwa): modify color of shortcut icons (#1528)
* refactor(pwa): add gradient bg to shortcut icons

* refactor(pwa): remove bg in favor of colored icons
2021-04-29 19:36:24 +09:00
TheCatLady
e1dc62b0a5 fix(pwa): add Discover shortcut and fix/optimize icons (#1525) 2021-04-28 17:42:33 +09:00
TheCatLady
26ddc03b2c fix(css): fix cog icon size on media detail pages (#1520) 2021-04-28 12:21:22 +09:00
TheCatLady
85899ab49a fix(radarr): only process Radarr movies which are either monitored or downloaded (#1511)
* fix(radarr): only process Radarr movies which are either monitored or downloaded

* refactor: move monitored check to processRadarrMovie
2021-04-28 09:53:52 +09:00
Weblate (bot)
9a95a07391 feat(lang): translations update from Weblate (#1497)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (840 of 840 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (863 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (861 of 861 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (860 of 860 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 97.4% (838 of 860 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 99.6% (841 of 844 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 99.7% (838 of 840 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (832 of 832 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (824 of 824 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (844 of 844 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (832 of 832 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (863 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (861 of 861 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (844 of 844 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 99.2% (834 of 840 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (832 of 832 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 98.9% (823 of 832 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (824 of 824 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (840 of 840 strings)

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (832 of 832 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 39.5% (341 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (863 of 863 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (861 of 861 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (860 of 860 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 96.6% (831 of 860 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (840 of 840 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (824 of 824 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (824 of 824 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Ricardo González <notorius28@gmail.com>
2021-04-27 19:32:48 -04:00
TheCatLady
3baa55c690 fix(email): parse sender hostname from application URL (#1518) 2021-04-27 23:23:51 +00:00
TheCatLady
802e40a5df fix(ui): explicitly specify width/height of Listbox dropdown icon (#1514) 2021-04-28 08:15:30 +09:00
TheCatLady
f78b9c1ca9 fix(css): don't target button globally (#1510)
* fix(css): don't target button globally

* fix(css): revert toast change
2021-04-27 15:23:36 +00:00
sct
2a912180b8 build: lets try tailwind jit again 2021-04-27 18:38:51 +09:00
TheCatLady
91b9e0f679 feat(perms): add separate REQUEST_MOVIE and REQUEST_TV permissions (#1474)
* feat(perms): add separate REQUEST_MOVIE and REQUEST_TV permissions

* fix(perms): do not require regular request perms for 4K requests
2021-04-27 18:28:07 +09:00
Danshil Kokil Mungur
ed99e4976d feat(pwa): add shortcuts to PWA (#1509)
* feat(pwa): add shortcuts to PWA

* feat(pwa): suggested changes

use 192x192 px icons and replace links
2021-04-27 11:20:31 +09:00
TheCatLady
db49b2024d feat: add option to only allow Plex sign-in from existing users (#1496)
* feat: add option to only allow Plex login from existing users

* fix: remove newPlexLogin from public settings
2021-04-26 21:06:54 +09:00
TheCatLady
3e5e9c0ad1 refactor(css): add styles targeting SVG button/modal icons (#1464)
* refactor(css): add global classes for common icon types

* refactor(css): target child SVGs instead of creating new icon classes

* fix: fix ButtonWithDropdown style class spacing
2021-04-26 10:09:58 +00:00
TheCatLady
079645c2c7 feat(notif): show success/failure toast for test notifications (#1442)
* feat(notif): show success/failure toast for test notifications

* fix(lang): remove 'successfully' from test notif success strings
2021-04-26 10:01:31 +00:00
TheCatLady
4faddf3810 fix(ui): show warning if user has both a default non-4K server and a non-default 4K server (#1478) 2021-04-26 17:13:05 +09:00
sct
ef3f9778aa fix(api): do not try to transform empty values passed to user notificationTypes
fixes #1501
2021-04-26 08:22:50 +09:00
TheCatLady
48587719e9 fix: do not set locale when modifying other users (#1499) 2021-04-26 07:39:19 +09:00
sct
2ecd9d7b13 fix(ui): correct toasts being in the wrong position on smaller screens 2021-04-26 01:29:36 +09:00
sct
11a5e8d95b fix(lang): correct overwritten email toast strings 2021-04-26 01:23:29 +09:00
TheCatLady
4e6fb00a4a feat(notif): add LunaSea agent (#1495)
* feat(notif): add LunaSea agent

* feat(notif): change LunaSea 'Authorization Header' input field to 'Profile Name'
2021-04-26 01:22:54 +09:00
imgbot[bot]
aa96e809bf chore: [ImgBot] Optimize images (#1498)
*Total -- 222.59kb -> 203.90kb (8.4%)

/public/android-chrome-512x512_maskable.png -- 80.66kb -> 70.39kb (12.73%)
/public/android-chrome-192x192.png -- 28.24kb -> 26.01kb (7.9%)
/public/android-chrome-192x192_maskable.png -- 25.87kb -> 24.12kb (6.77%)
/public/android-chrome-512x512.png -- 87.82kb -> 83.38kb (5.06%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: sct <ryan@sct.dev>
2021-04-25 15:05:31 +00:00
dependabot[bot]
25b5187f31 build(deps): bump ssri from 6.0.1 to 6.0.2 (#1473)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 15:00:32 +00:00
sct
dbac048431 build(deps): bump dependencies 2021-04-25 23:54:10 +09:00
TheCatLady
f1e8250740 ci(nextjs): configure Next.js build cache (#1463) [skip ci]
https://nextjs.org/docs/messages/no-cache#github-actions

Co-authored-by: sct <ryan@sct.dev>
2021-04-25 23:18:31 +09:00
sct
28830d4ef8 feat: PWA Support (#1488) 2021-04-25 20:44:12 +09:00
Weblate (bot)
e6e5ad221a Translations update from Weblate (#1451)
* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (822 of 822 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (820 of 820 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (822 of 822 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (820 of 820 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (820 of 820 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (822 of 822 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (820 of 820 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (819 of 819 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 99.7% (822 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (822 of 822 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Norwegian Bokmål)

Currently translated at 74.6% (612 of 820 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Torkil <torkil.liseth@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (824 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (820 of 820 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 99.5% (820 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 99.7% (822 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (822 of 822 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (822 of 822 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (822 of 822 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (822 of 822 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (822 of 822 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (819 of 819 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (819 of 819 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (819 of 819 strings)

Co-authored-by: Dabu <clarissy.andrea@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (820 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (820 of 824 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 98.0% (808 of 824 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (822 of 822 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (819 of 819 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Co-authored-by: Torkil <torkil.liseth@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Dabu <clarissy.andrea@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Ricardo González <notorius28@gmail.com>
2021-04-24 16:11:43 -04:00
sct
6a4ea1b0c5 chore: fix gitattributes for images [skip ci] 2021-04-24 09:10:36 +09:00
semantic-release-bot
f2c37babeb chore(release): 1.23.2 2021-04-21 12:28:28 +00:00
sct
24095eac03 Merge branch 'develop' 2021-04-21 21:20:07 +09:00
TheCatLady
1a311d211d fix(ui): change 'Disable Auto-Search' checkbox to 'Enable Automatic Search' (#1476)
* fix(ui): change 'Disable Auto-Search' checkbox to 'Enable Automatic Search'

* docs: update *arr setting documentation to reflect changes

* fix: apply form-input class
2021-04-21 09:21:25 +09:00
TheCatLady
a822b01922 fix(lang): add missing '4K' from singular case of approve/deny 4K request strings (#1481) 2021-04-20 23:03:05 +00:00
TheCatLady
89455ad9b7 fix: set editRequest attribute as necessary, allow users to edit their own pending requests, and show 'View Request' button on series pages (#1446)
* fix: set editRequest attribute for RequestModal

* fix: remove now-unneeded conditional

* fix(ui): only show 'View Request' for user's own requests if they don't have MANAGE_REQUESTS perm

* fix(ui): show edit button on request list for own requests & 'View Request' button on series pages

* fix(ui): do not show 'Request More' if user already has a pending request

* fix: address PR comments

* fix(lang): edit usercreatedfaileexisting string & generate translation key

* fix: users should always be able to view/edit their own requests even if their perms have changed

also fixed capitalization of 'Signing In...' string
2021-04-19 12:12:05 +09:00
sct
f13f1c9451 fix: better error message when creating a user with an existing email
fixes #1441
2021-04-18 19:53:55 +09:00
sct
eb5d152886 fix(ui): align icons in user dropdown
also changed icons on the jobs & cache page to be outlined (and switched to trash icon for flush
cache)
2021-04-18 19:39:24 +09:00
TheCatLady
4e98f56753 fix(notif): include year in notifications (#1439)
* fix(notif): include year in notifications

* fix(lang): fix shared overwritten strings

* fix: check if email agent is actually enabled when generating passwords
2021-04-18 10:11:56 +00:00
TheCatLady
3ae7d0098b fix(radarr): search in addition to monitoring existing movies (#1449) 2021-04-18 10:05:02 +00:00
TheCatLady
1702acf61c refactor(ui): add icons to PlayButton dropdown (#1457) 2021-04-18 09:56:07 +00:00
TheCatLady
5d1b741f55 fix(ui): adjust user list buttons on mobile (#1452) 2021-04-18 17:41:02 +09:00
TheCatLady
4449241a8f fix(api): add check for 4K request perms to request creation endpoint (#1450) 2021-04-17 15:07:38 +00:00
TheCatLady
db077700e4 fix(plex): add support for plex.direct URLs (#1437)
* fix(plex): add support for plex.direct URLs

* fix(ui): mark HTTPS Plex connections as secure
2021-04-17 10:07:37 +00:00
TheCatLady
8fc71bea08 refactor(icons): install and use @heroicons/react (#1438) 2021-04-17 18:07:23 +09:00
allcontributors[bot]
d0cc0a8e7a docs: add Dabu-dot as a contributor (#1447) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-04-17 09:32:16 +09:00
229 changed files with 7068 additions and 5410 deletions

View File

@@ -458,6 +458,15 @@
"contributions": [
"translation"
]
},
{
"login": "Dabu-dot",
"name": "Dabu-dot",
"avatar_url": "https://avatars.githubusercontent.com/u/52525576?v=4",
"profile": "https://github.com/Dabu-dot",
"contributions": [
"translation"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

25
.gitattributes vendored
View File

@@ -1 +1,26 @@
* text eol=lf
#
## These files are binary and should be left untouched
#
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary

View File

@@ -14,15 +14,15 @@ jobs:
runs-on: ubuntu-20.04
container: node:14.16-alpine
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: install dependencies
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
- name: Lint
run: yarn lint
- name: build
- name: Build
run: yarn build
build_and_push:
@@ -70,7 +70,7 @@ jobs:
ghcr.io/sct/overseerr:develop
ghcr.io/sct/overseerr:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temporary fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896

44
.github/workflows/preview.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Overseerr Preview
on:
push:
tags:
- 'preview-*'
jobs:
build_and_push:
name: Build & Publish Docker Preview Images
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
build-args: |
COMMIT_TAG=${{ github.sha }}
tags: |
sctx/overseerr:${{ steps.get_version.outputs.VERSION }}
ghcr.io/sct/overseerr:${{ steps.get_version.outputs.VERSION }}

View File

@@ -11,16 +11,17 @@ jobs:
runs-on: ubuntu-20.04
container: node:14.16-alpine
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: install dependencies
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
- name: Lint
run: yarn lint
- name: build
- name: Build
run: yarn build
semantic-release:
name: Tag and release latest version
needs: test
@@ -57,6 +58,7 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: npx semantic-release
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: semantic-release
@@ -77,7 +79,6 @@ jobs:
run: git checkout master
- name: Pull latest changes
run: git pull
- name: Prepare
id: prepare
run: |
@@ -87,35 +88,31 @@ jobs:
else
echo ::set-output name=RELEASE::edge
fi
- name: Set Up QEMU
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v2
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1.2.0
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: semantic-release
@@ -124,7 +121,6 @@ jobs:
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2.1.5
- name: Combine Job Status
id: status
run: |
@@ -134,7 +130,6 @@ jobs:
else
echo ::set-output name=status::$WORKFLOW_CONCLUSION
fi
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
with:

View File

@@ -22,15 +22,15 @@ jobs:
runs-on: ubuntu-20.04
container: node:14.16-alpine
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: install dependencies
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
- name: Lint
run: yarn lint
- name: build
- name: Build
run: yarn build
build-snap:

View File

@@ -1,3 +1,64 @@
# [1.24.0](https://github.com/sct/overseerr/compare/v1.23.2...v1.24.0) (2021-05-05)
### Bug Fixes
* **api:** do not try to transform empty values passed to user notificationTypes ([ef3f977](https://github.com/sct/overseerr/commit/ef3f9778aa81f8ed39dcd835d63d94f2248e0204)), closes [#1501](https://github.com/sct/overseerr/issues/1501)
* **backend:** properly set request media status ([#1541](https://github.com/sct/overseerr/issues/1541)) ([b7b55e2](https://github.com/sct/overseerr/commit/b7b55e275cb2f1f61c3057cb8ab4cb1027f6356d))
* **css:** don't target button globally ([#1510](https://github.com/sct/overseerr/issues/1510)) ([f78b9c1](https://github.com/sct/overseerr/commit/f78b9c1ca9648eb10b010e526d9b9db09648b154))
* **css:** fix cog icon size on media detail pages ([#1520](https://github.com/sct/overseerr/issues/1520)) ([26ddc03](https://github.com/sct/overseerr/commit/26ddc03b2c01b343c24f1c359b78c587310cc747))
* **email:** parse sender hostname from application URL ([#1518](https://github.com/sct/overseerr/issues/1518)) ([3baa55c](https://github.com/sct/overseerr/commit/3baa55c690dd9ba39768b8b271595cb6b09fe6da))
* **lang:** correct overwritten email toast strings ([11a5e8d](https://github.com/sct/overseerr/commit/11a5e8d95bc2a2f16adf1e48d2ef38b508a6ace5))
* **locale:** default user locale should be the server setting ([#1574](https://github.com/sct/overseerr/issues/1574)) ([549103f](https://github.com/sct/overseerr/commit/549103f6f6d5624201e425df7d7814f0f67863b9))
* **pwa:** add Discover shortcut and fix/optimize icons ([#1525](https://github.com/sct/overseerr/issues/1525)) ([e1dc62b](https://github.com/sct/overseerr/commit/e1dc62b0a5b64202701aff821837ed11dd3f12db))
* **radarr:** only process Radarr movies which are either monitored or downloaded ([#1511](https://github.com/sct/overseerr/issues/1511)) ([85899ab](https://github.com/sct/overseerr/commit/85899ab49a27542390e91443531905737224338d))
* **ui:** add missing margins on button SVGs on Plex Settings page ([#1546](https://github.com/sct/overseerr/issues/1546)) ([5e588be](https://github.com/sct/overseerr/commit/5e588be8127b50dd83477f7f3a65f18de774e8af))
* **ui:** add user profile links to RequestBlock and change 'ETA' string in DownloadBlock ([#1551](https://github.com/sct/overseerr/issues/1551)) ([e4d0029](https://github.com/sct/overseerr/commit/e4d0029f7b4245b8606e2447c54629def40c7761))
* **ui:** apply rounded-l-only to SensitiveInput textareas and increase visible text input area ([#1561](https://github.com/sct/overseerr/issues/1561)) ([1123fce](https://github.com/sct/overseerr/commit/1123fce089b86251dcafebf77743d60a6e396bee))
* **ui:** correct RegionSelector z-index ([#1567](https://github.com/sct/overseerr/issues/1567)) ([e912a00](https://github.com/sct/overseerr/commit/e912a00880f856fa9621e8587ef1cc6513a3d49c))
* **ui:** correct toasts being in the wrong position on smaller screens ([2ecd9d7](https://github.com/sct/overseerr/commit/2ecd9d7b1391b8fc83e9c12a18bab105e7148f0f))
* **ui:** default to text input type for SensitiveInputs ([#1568](https://github.com/sct/overseerr/issues/1568)) ([e2acf88](https://github.com/sct/overseerr/commit/e2acf8887cb0456c80308bd1b7f3bbe1930e8cff))
* **ui:** explicitly specify width/height of Listbox dropdown icon ([#1514](https://github.com/sct/overseerr/issues/1514)) ([802e40a](https://github.com/sct/overseerr/commit/802e40a5dfa00f897f9d5a741718a319f74ff030))
* **ui:** improve form usability ([#1563](https://github.com/sct/overseerr/issues/1563)) ([26580ea](https://github.com/sct/overseerr/commit/26580eaa218702bc5841718310e340d049c50332))
* **ui:** show warning if user has both a default non-4K server and a non-default 4K server ([#1478](https://github.com/sct/overseerr/issues/1478)) ([4faddf3](https://github.com/sct/overseerr/commit/4faddf3810e20851c7ae1251ff0187fa13d7b0f6))
* **webpush:** only prompt user to allow notifications if enabled in user settings ([#1552](https://github.com/sct/overseerr/issues/1552)) ([b05b177](https://github.com/sct/overseerr/commit/b05b177776a5d22bf3b5e93bad4358f4007b879a))
* correctly fall back to English name in LanguageSelector ([#1537](https://github.com/sct/overseerr/issues/1537)) ([189313e](https://github.com/sct/overseerr/commit/189313e94a16e694d192d157642d77f664fd709b))
* do not set locale when modifying other users ([#1499](https://github.com/sct/overseerr/issues/1499)) ([4858771](https://github.com/sct/overseerr/commit/48587719e9474139c7bbc2970b1c7d1d17b78a81))
### Features
* **email:** replace 'Enable SSL' setting with more descriptive/clear 'Encryption Method' setting ([#1549](https://github.com/sct/overseerr/issues/1549)) ([69ab7cc](https://github.com/sct/overseerr/commit/69ab7cc660bea43b70bdb646eabd3866c1b5a90f))
* **inputs:** add support for toggling security on input fields ([#1404](https://github.com/sct/overseerr/issues/1404)) ([4fd452d](https://github.com/sct/overseerr/commit/4fd452dd1880f597a0acda812d567e7cb6c16d83))
* **lang:** translated using Weblate (Spanish) ([#1553](https://github.com/sct/overseerr/issues/1553)) ([e3d5e33](https://github.com/sct/overseerr/commit/e3d5e33ec3e43d36ec832d6ca47f330fc7675088))
* **lang:** translations update from Weblate ([#1497](https://github.com/sct/overseerr/issues/1497)) ([9a95a07](https://github.com/sct/overseerr/commit/9a95a073916c9968b8ef348d0805d77400ea203a))
* **lang:** translations update from Weblate ([#1527](https://github.com/sct/overseerr/issues/1527)) ([1a6d4bd](https://github.com/sct/overseerr/commit/1a6d4bddc016f4aaad83b945e103b19be4d0da31))
* **lang:** translations update from Weblate ([#1558](https://github.com/sct/overseerr/issues/1558)) ([6c9991d](https://github.com/sct/overseerr/commit/6c9991d474a5cd95d9a0a10104bd79d8a9f3ada9))
* **lang:** translations update from Weblate ([#1566](https://github.com/sct/overseerr/issues/1566)) ([93c441e](https://github.com/sct/overseerr/commit/93c441ef6665291ca3698368e4b093c843726036))
* add server default locale setting ([#1536](https://github.com/sct/overseerr/issues/1536)) ([f256a44](https://github.com/sct/overseerr/commit/f256a444c57f2d92c1c4918d4ff6e223ef85ecd2))
* **notif:** add LunaSea agent ([#1495](https://github.com/sct/overseerr/issues/1495)) ([4e6fb00](https://github.com/sct/overseerr/commit/4e6fb00a4a59545817add1544c0b1555078809a4))
* **notif:** show success/failure toast for test notifications ([#1442](https://github.com/sct/overseerr/issues/1442)) ([079645c](https://github.com/sct/overseerr/commit/079645c2c74edfb7e4f583de2ac72bb9824f6524))
* **perms:** add separate REQUEST_MOVIE and REQUEST_TV permissions ([#1474](https://github.com/sct/overseerr/issues/1474)) ([91b9e0f](https://github.com/sct/overseerr/commit/91b9e0f67996a442b5c0117fe09e2d69c163fafb))
* **pwa:** add shortcuts to PWA ([#1509](https://github.com/sct/overseerr/issues/1509)) ([ed99e49](https://github.com/sct/overseerr/commit/ed99e4976dc2700fe84c70af4887c1a431bba92c))
* add option to only allow Plex sign-in from existing users ([#1496](https://github.com/sct/overseerr/issues/1496)) ([db49b20](https://github.com/sct/overseerr/commit/db49b2024d399d90f2d1500b262374efc42f333c))
* PWA Support ([#1488](https://github.com/sct/overseerr/issues/1488)) ([28830d4](https://github.com/sct/overseerr/commit/28830d4ef809efa92a5879a81cac11ff52ea3d1f))
## [1.23.2](https://github.com/sct/overseerr/compare/v1.23.1...v1.23.2) (2021-04-21)
### Bug Fixes
* **lang:** add missing '4K' from singular case of approve/deny 4K request strings ([#1481](https://github.com/sct/overseerr/issues/1481)) ([a822b01](https://github.com/sct/overseerr/commit/a822b019220e86e362a2570e7024289450b4ed46))
* **ui:** change 'Disable Auto-Search' checkbox to 'Enable Automatic Search' ([#1476](https://github.com/sct/overseerr/issues/1476)) ([1a311d2](https://github.com/sct/overseerr/commit/1a311d211d78731c9089e66ed5387c1b5afe33c0))
* better error message when creating a user with an existing email ([f13f1c9](https://github.com/sct/overseerr/commit/f13f1c94515b5bd51382fa18ad96a2ccfd06e50d)), closes [#1441](https://github.com/sct/overseerr/issues/1441)
* set editRequest attribute as necessary, allow users to edit their own pending requests, and show 'View Request' button on series pages ([#1446](https://github.com/sct/overseerr/issues/1446)) ([89455ad](https://github.com/sct/overseerr/commit/89455ad9b783d04d993a0009c351b1096f2b222e))
* **api:** add check for 4K request perms to request creation endpoint ([#1450](https://github.com/sct/overseerr/issues/1450)) ([4449241](https://github.com/sct/overseerr/commit/4449241a8f63fdaeaa4995aa7ec34127c322b9dd))
* **notif:** include year in notifications ([#1439](https://github.com/sct/overseerr/issues/1439)) ([4e98f56](https://github.com/sct/overseerr/commit/4e98f567534a650e26b0244990b7ca549cecbe89))
* **plex:** add support for plex.direct URLs ([#1437](https://github.com/sct/overseerr/issues/1437)) ([db07770](https://github.com/sct/overseerr/commit/db077700e42ab1d2c870213fd55bbdee74002775))
* **radarr:** search in addition to monitoring existing movies ([#1449](https://github.com/sct/overseerr/issues/1449)) ([3ae7d00](https://github.com/sct/overseerr/commit/3ae7d0098b225562499d7c8a74b8b6c3e8893ad9))
* **ui:** adjust user list buttons on mobile ([#1452](https://github.com/sct/overseerr/issues/1452)) ([5d1b741](https://github.com/sct/overseerr/commit/5d1b741f55665c528e299a09464dff6d66f72666))
* **ui:** align icons in user dropdown ([eb5d152](https://github.com/sct/overseerr/commit/eb5d1528869959cdf642e6fefc1a8f4dcf51b84e))
## [1.23.1](https://github.com/sct/overseerr/compare/v1.23.0...v1.23.1) (2021-04-16)

View File

@@ -1,28 +1,30 @@
FROM node:14.16-alpine AS BUILD_IMAGE
WORKDIR /app
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app
WORKDIR /app
RUN \
case "${TARGETPLATFORM}" in \
'linux/arm64') apk add --no-cache python make g++ ;; \
'linux/arm/v7') apk add --no-cache python make g++ ;; \
esac
RUN yarn --frozen-lockfile --network-timeout 1000000 && \
yarn build
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --network-timeout 1000000
COPY . ./
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
RUN yarn build
# remove development dependencies
RUN yarn install --production --ignore-scripts --prefer-offline
RUN rm -rf src && \
rm -rf server
RUN rm -rf src server
RUN touch config/DOCKER
@@ -31,11 +33,12 @@ RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:14.16-alpine
WORKDIR /app
RUN apk add --no-cache tzdata tini
# copy from build image
COPY --from=BUILD_IMAGE /app /app
WORKDIR /app
COPY --from=BUILD_IMAGE /app ./
ENTRYPOINT [ "/sbin/tini", "--" ]
CMD [ "yarn", "start" ]

View File

@@ -12,7 +12,7 @@
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-49-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-50-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
@@ -137,6 +137,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://nz.linkedin.com/in/jonocairns"><img src="https://avatars.githubusercontent.com/u/182836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jono Cairns</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jonocairns" title="Code">💻</a></td>
<td align="center"><a href="https://scias.net/"><img src="https://avatars.githubusercontent.com/u/439655?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DJScias</b></sub></a><br /><a href="#translation-DJScias" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Dabu-dot"><img src="https://avatars.githubusercontent.com/u/52525576?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dabu-dot</b></sub></a><br /><a href="#translation-Dabu-dot" title="Translation">🌍</a></td>
</tr>
</table>
<!-- markdownlint-restore -->

View File

@@ -178,11 +178,11 @@ If the hostname or IP address you configured above is not accessible outside you
#### Enable Scan
Tick this box if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available.
Enable this setting if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available.
#### Disable Auto-Search
#### Enable Automatic Search
If you do not want Radarr/Sonarr to automatically search for media upon submission of a request, you can disable this setting.
Enable this setting to have Radarr/Sonarr to automatically search for media upon approval of a request.
## Notifications

View File

@@ -103,8 +103,10 @@ components:
properties:
apiKey:
type: string
example: 'anapikey'
readOnly: true
appLanguage:
type: string
example: en
applicationTitle:
type: string
example: Overseerr
@@ -126,6 +128,9 @@ components:
localLogin:
type: boolean
example: true
newPlexLogin:
type: boolean
example: true
defaultPermissions:
type: number
example: 32
@@ -1128,6 +1133,15 @@ components:
properties:
webhookUrl:
type: string
WebPushSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
WebhookSettings:
type: object
properties:
@@ -1142,6 +1156,8 @@ components:
properties:
webhookUrl:
type: string
authHeader:
type: string
jsonPayload:
type: string
TelegramSettings:
@@ -1196,6 +1212,22 @@ components:
type: string
priority:
type: number
LunaSeaSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
webhookUrl:
type: string
profileName:
type: string
NotificationEmailSettings:
type: object
properties:
@@ -1223,6 +1255,12 @@ components:
secure:
type: boolean
example: false
ignoreTls:
type: boolean
example: false
requireTls:
type: boolean
example: false
authUser:
type: string
nullable: true
@@ -2397,22 +2435,22 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/telegram:
/settings/notifications/lunasea:
get:
summary: Get Telegram notification settings
description: Returns current Telegram notification settings in a JSON object.
summary: Get LunaSea notification settings
description: Returns current LunaSea notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Telegram settings
description: Returned LunaSea settings
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
$ref: '#/components/schemas/LunaSeaSettings'
post:
summary: Update Telegram notification settings
description: Update Telegram notification settings with the provided values.
summary: Update LunaSea notification settings
description: Updates LunaSea notification settings with the provided values.
tags:
- settings
requestBody:
@@ -2420,18 +2458,18 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
$ref: '#/components/schemas/LunaSeaSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
/settings/notifications/telegram/test:
$ref: '#/components/schemas/LunaSeaSettings'
/settings/notifications/lunasea/test:
post:
summary: Test Telegram settings
description: Sends a test notification to the Telegram agent.
summary: Test LunaSea settings
description: Sends a test notification to the LunaSea agent.
tags:
- settings
requestBody:
@@ -2439,7 +2477,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
$ref: '#/components/schemas/LunaSeaSettings'
responses:
'204':
description: Test notification attempted
@@ -2581,6 +2619,98 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/telegram:
get:
summary: Get Telegram notification settings
description: Returns current Telegram notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Telegram settings
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
post:
summary: Update Telegram notification settings
description: Update Telegram notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
/settings/notifications/telegram/test:
post:
summary: Test Telegram settings
description: Sends a test notification to the Telegram agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/webpush:
get:
summary: Get Web Push notification settings
description: Returns current Web Push notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned web push settings
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
post:
summary: Update Web Push notification settings
description: Updates Web Push notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
/settings/notifications/webpush/test:
post:
summary: Test Web Push settings
description: Sends a test notification to the Web Push agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/webhook:
get:
summary: Get webhook notification settings
@@ -2903,6 +3033,32 @@ paths:
type: array
items:
$ref: '#/components/schemas/User'
/user/registerPushSubscription:
post:
summary: Register a web push /user/registerPushSubscription
description: Registers a web push subscription for the logged-in user
tags:
- users
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
endpoint:
type: string
auth:
type: string
p256dh:
type: string
required:
- endpoint
- auth
- p256dh
responses:
'204':
description: Successfully registered push subscription
/user/{userId}:
get:
summary: Get user by ID

View File

@@ -1,6 +1,6 @@
{
"name": "overseerr",
"version": "1.23.1",
"version": "1.24.0",
"private": true,
"scripts": {
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts",
@@ -17,10 +17,11 @@
},
"license": "MIT",
"dependencies": {
"@headlessui/react": "^1.0.0",
"@headlessui/react": "^1.1.1",
"@heroicons/react": "^1.0.1",
"@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.62",
"@tanem/react-nprogress": "^3.0.64",
"ace-builds": "^1.4.12",
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
@@ -29,35 +30,34 @@
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.5",
"copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.2.9",
"country-flag-icons": "^1.2.10",
"csurf": "^1.11.0",
"email-templates": "^8.0.4",
"express": "^4.17.1",
"express-openapi-validator": "^4.12.7",
"express-openapi-validator": "^4.12.9",
"express-rate-limit": "^5.2.6",
"express-session": "^1.17.1",
"formik": "^2.2.6",
"gravatar-url": "^3.1.0",
"gravatar-url": "3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.21",
"next": "10.1.3",
"node-cache": "^5.1.2",
"node-schedule": "^2.0.0",
"nodemailer": "^6.5.0",
"nookies": "^2.5.2",
"openpgp": "^5.0.0-1",
"nodemailer": "^6.6.0",
"openpgp": "^5.0.0-2",
"plex-api": "^5.3.1",
"pug": "^3.0.2",
"react": "17.0.2",
"react-ace": "^9.3.0",
"react-animate-height": "^2.0.23",
"react-dom": "17.0.2",
"react-intersection-observer": "^8.31.0",
"react-intl": "5.15.8",
"react-markdown": "^6.0.0",
"react-intersection-observer": "^8.31.1",
"react-intl": "5.17.4",
"react-markdown": "^6.0.1",
"react-select": "^4.3.0",
"react-spring": "^8.0.27",
"react-toast-notifications": "^2.4.3",
"react-toast-notifications": "^2.4.4",
"react-transition-group": "^4.4.1",
"react-truncate-markup": "^5.1.0",
"react-use-clipboard": "1.0.7",
@@ -65,17 +65,18 @@
"secure-random-password": "^0.2.2",
"sqlite3": "^5.0.2",
"swagger-ui-express": "^4.1.6",
"swr": "^0.5.5",
"swr": "^0.5.6",
"typeorm": "^0.2.32",
"uuid": "^8.3.2",
"web-push": "^3.4.4",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.2",
"winston-daily-rotate-file": "^4.5.3",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.32.9"
},
"devDependencies": {
"@babel/cli": "^7.13.14",
"@babel/cli": "^7.13.16",
"@commitlint/cli": "^12.1.1",
"@commitlint/config-conventional": "^12.1.1",
"@semantic-release/changelog": "^5.0.1",
@@ -95,10 +96,10 @@
"@types/express-rate-limit": "^5.1.1",
"@types/express-session": "^1.17.3",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.41",
"@types/node": "^15.0.1",
"@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.1",
"@types/react": "^17.0.3",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/react-select": "^4.0.15",
"@types/react-toast-notifications": "^2.4.0",
@@ -106,6 +107,7 @@
"@types/secure-random-password": "^0.2.0",
"@types/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0",
"@types/web-push": "^3.3.0",
"@types/xml2js": "^0.4.8",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.11",
@@ -117,9 +119,9 @@
"commitizen": "^4.2.3",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.2.0",
"eslint-plugin-formatjs": "^2.14.6",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-formatjs": "^2.14.10",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.2",
@@ -128,11 +130,11 @@
"husky": "4.3.8",
"lint-staged": "^10.5.4",
"nodemon": "^2.0.7",
"postcss": "^8.2.10",
"postcss": "^8.2.13",
"prettier": "^2.2.1",
"semantic-release": "^17.4.2",
"semantic-release-docker-buildx": "^1.0.1",
"tailwindcss": "^2.1.1",
"tailwindcss": "^2.1.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/cog-icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1115.2"><metadata/><defs><path id="a" d="m0 0h1024v1024h-1024z"/></defs><use width="100%" height="100%" fill="#fff" fill-opacity="0" transform="rotate(.704 1914.4 -5491.6)" xlink:href="#a"/><g><path fill="#fff" d="m105.76 154.15-1.263 714.59c-60.261 6.782-105.02-23.858-104.28-84.025l-0.216-594.25c2.312-188.02 175.85-231.02 280.22-154.52l530.2 314.93c74.563 53.572 88.403 151.53 49.965 218.76-6.873-52.739-29.067-83.101-73.823-113.74l-597.52-345.84c-44.756-30.639-82.453-23.58-83.286 44.109zm-54.377 751.54c44.941 15.597 90.16 8.63 128.04-13.47l621.16-353.43c36.958 53.109 28.79 105.66-16.706 135.19l-522.65 294.46c-75.673 36.68-172.98-2.127-209.85-62.757z" transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)"/><path fill="#ffc230" d="m240.52 702.59 365.02-216.68-364.35-197.07z" transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)"/></g></svg>

Before

Width:  |  Height:  |  Size: 969 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="216.7" height="216.9" viewBox="0 0 216.7 216.9"><path fill="#EEE" fill-rule="evenodd" d="M216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95C53.05 10.517 78.617 0 108.45 0c29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" clip-rule="evenodd"/><path fill="#3A3F51" fill-rule="evenodd" d="M194.65 42.5l-22.4 22.4C159.152 77.998 158 89.4 158 109.5c0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05L61.1 43.85C74.102 56.852 90.767 60.2 108.7 60.2c18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" clip-rule="evenodd"/><g clip-rule="evenodd"><path fill="#0CF" fill-rule="evenodd" d="M78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3 0 8.267-2.916 15.35-8.75 21.25-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z"/><path fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="2" d="M157.8 59.75l-15 14.65M30.785 32.526L71.65 73.25m84.6 84.25l27.808 28.78m1.855-153.894L157.8 59.75m-125.45 126l27.35-27.4"/><path fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="7" d="M157.8 59.75l-16.95 17.2M58.97 60.604l17.2 17.15M59.623 158.43l16.75-17.4m61.928-1.396l18.028 17.945"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

69
public/offline.html Normal file
View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>You are offline</title>
<!-- Inline the page's stylesheet. -->
<style>
body {
font-family: helvetica, arial, sans-serif;
margin: 2em;
background-color: #111827;
}
h1 {
color: #6366F1;
}
p {
margin-block: 1rem;
}
button {
display: block;
}
</style>
</head>
<body>
<h1>You are offline</h1>
<button type="button">⤾ Reload</button>
<!-- Inline the page's JavaScript file. -->
<script>
// Manual reload feature.
document.querySelector("button").addEventListener("click", () => {
window.location.reload();
});
// Listen to changes in the network state, reload when online.
// This handles the case when the device is completely offline.
window.addEventListener('online', () => {
window.location.reload();
});
// Check if the server is responding and reload the page if it is.
// This handles the case when the device is online, but the server
// is offline or misbehaving.
async function checkNetworkAndReload() {
try {
const response = await fetch('.');
// Verify we get a valid response from the server
if (response.status >= 200 && response.status < 500) {
window.location.reload();
return;
}
} catch {
// Unable to connect to the server, ignore.
}
window.setTimeout(checkNetworkAndReload, 2500);
}
checkNetworkAndReload();
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 455 KiB

View File

@@ -4,19 +4,77 @@
"start_url": "./",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-192x192_maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/android-chrome-512x512.png",
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-512x512_maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#2d3748",
"background_color": "#2d3748",
"display": "standalone"
"theme_color": "#1f2937",
"background_color": "#1f2937",
"display": "standalone",
"shortcuts": [
{
"name": "Discover",
"url": "./",
"icons": [
{
"src": "./sparkles-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "Requests",
"url": "./requests",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "Profile",
"url": "./profile",
"icons": [
{
"src": "./user-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "Settings",
"url": "./profile/settings",
"icons": [
{
"src": "./cog-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

136
public/sw.js Normal file
View File

@@ -0,0 +1,136 @@
// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const OFFLINE_VERSION = 3;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "/offline.html";
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the
// response isn't fulfilled from the HTTP cache; i.e., it will be from
// the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})()
);
// Force the waiting service worker to become the active service worker.
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === "navigate") {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
});
self.addEventListener('push', (event) => {
const payload = event.data ? event.data.json() : {};
const options = {
body: payload.message,
icon: payload.image ? payload.image : 'android-chrome-192x192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: '2',
actionUrl: payload.actionUrl,
requestId: payload.requestId,
},
actions: [],
}
if (payload.actionUrl){
options.actions.push(
{
action: 'viewmedia',
title: 'View Media',
}
);
}
if (payload.notificationType === 'MEDIA_PENDING') {
options.actions.push(
{
action: 'approve',
title: 'Approve',
},
{
action: 'decline',
title: 'Decline',
}
);
}
event.waitUntil(
self.registration.showNotification(payload.subject, options)
);
})
self.addEventListener('notificationclick', (event) => {
const notificationData = event.notification.data;
event.notification.close();
if (event.action === 'viewmedia') {
self.clients.openWindow(notificationData.actionUrl);
} else if (event.action === 'approve') {
fetch(`/api/v1/request/${notificationData.requestId}/approve`, {
method: 'POST',
});
self.clients.openWindow(notificationData.actionUrl);
} else if (event.action === 'decline') {
fetch(`/api/v1/request/${notificationData.requestId}/decline`, {
method: 'POST',
});
self.clients.openWindow(notificationData.actionUrl);
} else if (notificationData.actionUrl) {
self.clients.openWindow(notificationData.actionUrl);
}
}, false);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -72,7 +72,8 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
} catch (e) {
logger.error('Error retrieving movie by TMDb ID', {
label: 'Radarr API',
message: e.message,
errorMessage: e.message,
tmdbId: id,
});
throw new Error('Movie not found');
}
@@ -89,12 +90,13 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
'Title already exists and is available. Skipping add and returning success',
{
label: 'Radarr',
movie,
}
);
return movie;
}
// movie exists in radarr but is neither downloaded nor monitored
// movie exists in Radarr but is neither downloaded nor monitored
if (movie.id && !movie.monitored) {
const response = await this.axios.put<RadarrMovie>(`/movie`, {
...movie,
@@ -115,16 +117,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
if (response.data.monitored) {
logger.info(
'Found existing title in Radarr and set it to monitored. Returning success',
{ label: 'Radarr' }
'Found existing title in Radarr and set it to monitored.',
{
label: 'Radarr',
movieId: response.data.id,
movieTitle: response.data.title,
}
);
logger.debug('Radarr update details', {
label: 'Radarr',
movie: response.data,
});
if (options.searchNow) {
this.searchMovie(response.data.id);
}
return response.data;
} else {
logger.error('Failed to update existing movie in Radarr', {
logger.error('Failed to update existing movie in Radarr.', {
label: 'Radarr',
options,
});
@@ -183,6 +194,26 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
throw new Error('Failed to add movie to Radarr');
}
};
public async searchMovie(movieId: number): Promise<void> {
logger.info('Executing movie search command', {
label: 'Radarr API',
movieId,
});
try {
await this.runCommand('MoviesSearch', { movieIds: [movieId] });
} catch (e) {
logger.error(
'Something went wrong while executing Radarr movie search.',
{
label: 'Radarr API',
errorMessage: e.message,
movieId,
}
);
}
}
}
export default RadarrAPI;

View File

@@ -113,7 +113,8 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
} catch (e) {
logger.error('Error retrieving series by series title', {
label: 'Sonarr API',
message: e.message,
errorMessage: e.message,
title,
});
throw new Error('No series found');
}
@@ -135,7 +136,8 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
} catch (e) {
logger.error('Error retrieving series by tvdb ID', {
label: 'Sonarr API',
message: e.message,
errorMessage: e.message,
tvdbId: id,
});
throw new Error('Series not found');
}
@@ -156,16 +158,21 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
);
if (newSeriesResponse.data.id) {
logger.info('Sonarr accepted request. Updated existing series', {
logger.info('Updated existing series in Sonarr.', {
label: 'Sonarr',
seriesId: newSeriesResponse.data.id,
seriesTitle: newSeriesResponse.data.title,
});
logger.debug('Sonarr update details', {
label: 'Sonarr',
movie: newSeriesResponse.data,
});
if (options.searchNow) {
this.searchSeries(newSeriesResponse.data.id);
}
return newSeriesResponse.data;
} else {
logger.error('Failed to update series in Sonarr', {
label: 'Sonarr',
@@ -173,8 +180,6 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
});
throw new Error('Failed to update series in Sonarr');
}
return newSeriesResponse.data;
}
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
@@ -223,7 +228,7 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
logger.error('Something went wrong while adding a series to Sonarr.', {
label: 'Sonarr API',
errorMessage: e.message,
error: e,
options,
response: e?.response?.data,
});
throw new Error('Failed to add series');
@@ -244,7 +249,7 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
'Something went wrong while retrieving Sonarr language profiles.',
{
label: 'Sonarr API',
message: e.message,
errorMessage: e.message,
}
);
@@ -253,11 +258,23 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
}
public async searchSeries(seriesId: number): Promise<void> {
logger.info('Executing series search command', {
logger.info('Executing series search command.', {
label: 'Sonarr API',
seriesId,
});
await this.runCommand('SeriesSearch', { seriesId });
try {
await this.runCommand('SeriesSearch', { seriesId });
} catch (e) {
logger.error(
'Something went wrong while executing Sonarr series search.',
{
label: 'Sonarr API',
errorMessage: e.message,
seriesId,
}
);
}
}
private buildSeasonList(

View File

@@ -142,7 +142,9 @@ export class MediaRequest {
if (this.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, {
subject: movie.title,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
media,
@@ -153,7 +155,9 @@ export class MediaRequest {
if (this.type === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, {
subject: tv.name,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media,
@@ -210,7 +214,9 @@ export class MediaRequest {
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
subject: movie.title,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy,
@@ -227,7 +233,9 @@ export class MediaRequest {
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
subject: tv.name,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy,
@@ -274,11 +282,7 @@ export class MediaRequest {
media[this.is4k ? 'status4k' : 'status'] !==
MediaStatus.PARTIALLY_AVAILABLE
) {
if (this.is4k) {
media.status4k = MediaStatus.PROCESSING;
} else {
media.status = MediaStatus.PROCESSING;
}
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
mediaRepository.save(media);
}
@@ -286,11 +290,7 @@ export class MediaRequest {
media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED
) {
if (this.is4k) {
media.status4k = MediaStatus.UNKNOWN;
} else {
media.status = MediaStatus.UNKNOWN;
}
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
@@ -306,9 +306,9 @@ export class MediaRequest {
media.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING
).length === 0 &&
media.status === MediaStatus.PENDING
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
) {
media.status = MediaStatus.UNKNOWN;
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
@@ -482,7 +482,7 @@ export class MediaRequest {
await mediaRepository.save(media);
})
.catch(async () => {
media.status = MediaStatus.UNKNOWN;
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added movie request failed to add to Radarr, marking as unknown',
@@ -492,7 +492,9 @@ export class MediaRequest {
);
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: movie.title,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
@@ -690,7 +692,7 @@ export class MediaRequest {
await mediaRepository.save(media);
})
.catch(async () => {
media.status = MediaStatus.UNKNOWN;
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added series request failed to add to Sonarr, marking as unknown',
@@ -700,7 +702,11 @@ export class MediaRequest {
);
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: series.name,
subject: `${series.name}${
series.first_air_date
? ` (${series.first_air_date.slice(0, 4)})`
: ''
}`,
message: series.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media,

View File

@@ -29,6 +29,7 @@ import { getSettings } from '../lib/settings';
import logger from '../logger';
import { MediaRequest } from './MediaRequest';
import SeasonRequest from './SeasonRequest';
import { UserPushSubscription } from './UserPushSubscription';
import { UserSettings } from './UserSettings';
@Entity()
@@ -105,6 +106,9 @@ export class User {
})
public settings?: UserSettings;
@OneToMany(() => UserPushSubscription, (pushSub) => pushSub.user)
public pushSubscriptions: UserPushSubscription[];
@CreateDateColumn()
public createdAt: Date;

View File

@@ -0,0 +1,27 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from './User';
@Entity()
export class UserPushSubscription {
@PrimaryGeneratedColumn()
public id: number;
@ManyToOne(() => User, (user) => user.pushSubscriptions, {
eager: true,
onDelete: 'CASCADE',
})
public user: User;
@Column()
public endpoint: string;
@Column()
public p256dh: string;
@Column({ unique: true })
public auth: string;
constructor(init?: Partial<UserPushSubscription>) {
Object.assign(this, init);
}
}

View File

@@ -5,12 +5,15 @@ import {
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import {
hasNotificationAgentEnabled,
NotificationAgentType,
} from '../lib/notifications/agenttypes';
import { NotificationAgentTypes } from '../interfaces/api/userSettingsInterfaces';
import { hasNotificationType, Notification } from '../lib/notifications';
import { NotificationAgentKey } from '../lib/settings';
import { User } from './User';
export const ALL_NOTIFICATIONS = Object.values(Notification)
.filter((v) => !isNaN(Number(v)))
.reduce((a, v) => a + Number(v), 0);
@Entity()
export class UserSettings {
constructor(init?: Partial<UserSettings>) {
@@ -24,15 +27,15 @@ export class UserSettings {
@JoinColumn()
public user: User;
@Column({ default: '' })
public locale?: string;
@Column({ nullable: true })
public region?: string;
@Column({ nullable: true })
public originalLanguage?: string;
@Column({ type: 'integer', default: NotificationAgentType.EMAIL })
public notificationAgents = NotificationAgentType.EMAIL;
@Column({ nullable: true })
public pgpKey?: string;
@@ -45,7 +48,67 @@ export class UserSettings {
@Column({ nullable: true })
public telegramSendSilently?: boolean;
public hasNotificationAgentEnabled(agent: NotificationAgentType): boolean {
return !!hasNotificationAgentEnabled(agent, this.notificationAgents);
@Column({
type: 'text',
nullable: true,
transformer: {
from: (value: string | null): Partial<NotificationAgentTypes> => {
const defaultTypes = {
email: ALL_NOTIFICATIONS,
discord: 0,
pushbullet: 0,
pushover: 0,
slack: 0,
telegram: 0,
webhook: 0,
webpush: ALL_NOTIFICATIONS,
};
if (!value) {
return defaultTypes;
}
const values = JSON.parse(value) as Partial<NotificationAgentTypes>;
// Something with the migration to this field has caused some issue where
// the value pre-populates with just a raw "2"? Here we check if that's the case
// and return the default notification types if so
if (typeof values !== 'object') {
return defaultTypes;
}
if (values.email == null) {
values.email = ALL_NOTIFICATIONS;
}
if (values.webpush == null) {
values.webpush = ALL_NOTIFICATIONS;
}
return values;
},
to: (value: Partial<NotificationAgentTypes>): string | null => {
if (!value || typeof value !== 'object') {
return null;
}
const allowedKeys = Object.values(NotificationAgentKey);
// Remove any unknown notification agent keys before saving to db
(Object.keys(value) as (keyof NotificationAgentTypes)[]).forEach(
(key) => {
if (!allowedKeys.includes(key)) {
delete value[key];
}
}
);
return JSON.stringify(value);
},
},
})
public notificationTypes: Partial<NotificationAgentTypes>;
public hasNotificationType(key: NotificationAgentKey, type: Notification) {
return hasNotificationType(type, this.notificationTypes[key] ?? 0);
}
}

View File

@@ -1,30 +1,32 @@
import express, { Request, Response, NextFunction } from 'express';
import next from 'next';
import path from 'path';
import { createConnection, getRepository } from 'typeorm';
import routes from './routes';
import { getClientIp } from '@supercharge/request-ip';
import bodyParser from 'body-parser';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import session, { Store } from 'express-session';
import { TypeormStore } from 'connect-typeorm/out';
import YAML from 'yamljs';
import swaggerUi from 'swagger-ui-express';
import express, { NextFunction, Request, Response } from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import session, { Store } from 'express-session';
import next from 'next';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
import { createConnection, getRepository } from 'typeorm';
import YAML from 'yamljs';
import { Session } from './entity/Session';
import { getSettings } from './lib/settings';
import logger from './logger';
import { startJobs } from './job/schedule';
import notificationManager from './lib/notifications';
import DiscordAgent from './lib/notifications/agents/discord';
import EmailAgent from './lib/notifications/agents/email';
import TelegramAgent from './lib/notifications/agents/telegram';
import { getAppVersion } from './utils/appVersion';
import SlackAgent from './lib/notifications/agents/slack';
import PushoverAgent from './lib/notifications/agents/pushover';
import WebhookAgent from './lib/notifications/agents/webhook';
import { getClientIp } from '@supercharge/request-ip';
import LunaSeaAgent from './lib/notifications/agents/lunasea';
import PushbulletAgent from './lib/notifications/agents/pushbullet';
import PushoverAgent from './lib/notifications/agents/pushover';
import SlackAgent from './lib/notifications/agents/slack';
import TelegramAgent from './lib/notifications/agents/telegram';
import WebhookAgent from './lib/notifications/agents/webhook';
import WebPushAgent from './lib/notifications/agents/webpush';
import { getSettings } from './lib/settings';
import logger from './logger';
import routes from './routes';
import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
@@ -52,11 +54,13 @@ app
notificationManager.registerAgents([
new DiscordAgent(),
new EmailAgent(),
new LunaSeaAgent(),
new PushbulletAgent(),
new PushoverAgent(),
new SlackAgent(),
new TelegramAgent(),
new WebhookAgent(),
new WebPushAgent(),
]);
// Start Jobs

View File

@@ -30,6 +30,9 @@ export interface PublicSettingsResponse {
originalLanguage: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
locale: string;
}
export interface CacheItem {

View File

@@ -1,5 +1,8 @@
import { NotificationAgentKey } from '../../lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
locale?: string;
region?: string;
originalLanguage?: string;
movieQuotaLimit?: number;
@@ -12,8 +15,8 @@ export interface UserSettingsGeneralResponse {
globalTvQuotaDays?: number;
}
export type NotificationAgentTypes = Record<NotificationAgentKey, number>;
export interface UserSettingsNotificationsResponse {
notificationAgents: number;
emailEnabled?: boolean;
pgpKey?: string;
discordEnabled?: boolean;
@@ -22,4 +25,6 @@ export interface UserSettingsNotificationsResponse {
telegramBotUsername?: string;
telegramChatId?: string;
telegramSendSilently?: boolean;
webPushEnabled?: boolean;
notificationTypes: Partial<NotificationAgentTypes>;
}

View File

@@ -1,14 +1,20 @@
import Email from 'email-templates';
import nodemailer from 'nodemailer';
import { NotificationAgentEmail } from '../settings';
import { URL } from 'url';
import { getSettings, NotificationAgentEmail } from '../settings';
import { openpgpEncrypt } from './openpgpEncrypt';
class PreparedEmail extends Email {
public constructor(settings: NotificationAgentEmail, pgpKey?: string) {
const { applicationUrl } = getSettings().main;
const transport = nodemailer.createTransport({
name: applicationUrl ? new URL(applicationUrl).hostname : undefined,
host: settings.options.smtpHost,
port: settings.options.smtpPort,
secure: settings.options.secure,
ignoreTLS: settings.options.ignoreTls,
requireTLS: settings.options.requireTls,
tls: settings.options.allowSelfSigned
? {
rejectUnauthorized: false,
@@ -22,6 +28,7 @@ class PreparedEmail extends Email {
}
: undefined,
});
if (pgpKey) {
transport.use(
'stream',
@@ -32,6 +39,7 @@ class PreparedEmail extends Email {
})
);
}
super({
message: {
from: {

View File

@@ -1,6 +1,6 @@
import crypto from 'crypto';
import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream';
import crypto from 'crypto';
interface EncryptorOptions {
signingKey?: string;
@@ -54,7 +54,8 @@ class PGPEncryptor extends Transform {
privateKey = await openpgp.readKey({
armoredKey: this._signingKey,
});
await privateKey.decrypt(this._password);
await openpgp.decryptKey({ privateKey, passphrase: this._password });
}
const emailPartDelimiter = '\r\n\r\n';
@@ -128,11 +129,12 @@ class PGPEncryptor extends Transform {
.join('\r\n');
const encryptedMessage = await openpgp.encrypt({
message: openpgp.Message.fromText(
contentHeadersRaw +
message: await openpgp.createMessage({
text:
contentHeadersRaw +
emailPartDelimiter +
messageParts.join(emailPartDelimiter)
),
messageParts.join(emailPartDelimiter),
}),
publicKeys: validPublicKeys,
privateKeys: privateKey,
});

View File

@@ -4,8 +4,11 @@ import { hasNotificationType, Notification } from '..';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import { getSettings, NotificationAgentDiscord } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentDiscord,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
enum EmbedColors {
@@ -227,8 +230,9 @@ class DiscordAgent
if (payload.notifyUser) {
// Mention user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationAgentEnabled(
NotificationAgentType.DISCORD
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
payload.notifyUser.settings?.discordId
) {
@@ -243,8 +247,9 @@ class DiscordAgent
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
user.settings?.hasNotificationAgentEnabled(
NotificationAgentType.DISCORD
user.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
user.settings?.discordId
)
@@ -267,7 +272,7 @@ class DiscordAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -7,8 +7,11 @@ import { User } from '../../../entity/User';
import logger from '../../../logger';
import PreparedEmail from '../../email';
import { Permission } from '../../permissions';
import { getSettings, NotificationAgentEmail } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentEmail,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class EmailAgent
@@ -152,9 +155,13 @@ class EmailAgent
// Send notification to the user who submitted the request
if (
!payload.notifyUser.settings ||
payload.notifyUser.settings.hasNotificationAgentEnabled(
NotificationAgentType.EMAIL
)
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(payload.notifyUser.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true)
) {
logger.debug('Sending email notification', {
label: 'Notifications',
@@ -194,9 +201,13 @@ class EmailAgent
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
(!user.settings ||
user.settings.hasNotificationAgentEnabled(
NotificationAgentType.EMAIL
))
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(user.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true))
)
.map(async (user) => {
logger.debug('Sending email notification', {

View File

@@ -0,0 +1,104 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaStatus } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentLunaSea } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class LunaSeaAgent
extends BaseAgent<NotificationAgentLunaSea>
implements NotificationAgent {
protected getSettings(): NotificationAgentLunaSea {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.lunasea;
}
private buildPayload(type: Notification, payload: NotificationPayload) {
return {
notification_type: Notification[type],
subject: payload.subject,
message: payload.message,
image: payload.image ?? null,
email: payload.notifyUser?.email,
username: payload.notifyUser?.username,
avatar: payload.notifyUser?.avatar,
media: payload.media
? {
media_type: payload.media.mediaType,
tmdbId: payload.media.tmdbId,
imdbId: payload.media.imdbId,
tvdbId: payload.media.tvdbId,
status: MediaStatus[payload.media.status],
status4k: MediaStatus[payload.media.status4k],
}
: null,
extra: payload.extra ?? [],
request: payload.request
? {
request_id: payload.request.id,
requestedBy_email: payload.request.requestedBy.email,
requestedBy_username: payload.request.requestedBy.displayName,
requestedBy_avatar: payload.request.requestedBy.avatar,
}
: null,
};
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const { webhookUrl, profileName } = this.getSettings().options;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildPayload(type, payload), {
headers: {
Authorization: `Basic ${Buffer.from(`${profileName}:`).toString(
'base64'
)}`,
},
});
return true;
} catch (e) {
logger.error('Error sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
});
return false;
}
}
}
export default LunaSeaAgent;

View File

@@ -170,7 +170,7 @@ class PushbulletAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -196,7 +196,7 @@ class PushoverAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -254,7 +254,7 @@ class SlackAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -2,8 +2,11 @@ import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentTelegram } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentKey,
NotificationAgentTelegram,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface TelegramMessagePayload {
@@ -198,8 +201,9 @@ class TelegramAgent
if (
payload.notifyUser &&
payload.notifyUser.settings?.hasNotificationAgentEnabled(
NotificationAgentType.TELEGRAM
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
payload.notifyUser.settings?.telegramChatId &&
payload.notifyUser.settings?.telegramChatId !==
@@ -240,7 +244,7 @@ class TelegramAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -154,7 +154,7 @@ class WebhookAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -0,0 +1,234 @@
import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentConfig,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushNotificationPayload {
notificationType: string;
mediaType?: 'movie' | 'tv';
tmdbId?: number;
subject: string;
message?: string;
image?: string;
actionUrl?: string;
requestId?: number;
}
class WebPushAgent
extends BaseAgent<NotificationAgentConfig>
implements NotificationAgent {
protected getSettings(): NotificationAgentConfig {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.webpush;
}
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): PushNotificationPayload {
switch (type) {
case Notification.TEST_NOTIFICATION:
return {
notificationType: Notification[type],
subject: payload.subject,
message: payload.message,
};
case Notification.MEDIA_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request has been approved.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AUTO_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Automatically approved a new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AVAILABLE:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request is now available!`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_DECLINED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request was declined.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_FAILED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Failed to process ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_PENDING:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Approval required for new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
}
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending web push notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
const userRepository = getRepository(User);
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
let pushSubs: UserPushSubscription[] = [];
const mainUser = await userRepository.findOne({ where: { id: 1 } });
if (
payload.notifyUser &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true)
) {
const notifySubs = await userPushSubRepository.find({
where: { user: payload.notifyUser.id },
});
pushSubs = notifySubs;
} else if (!payload.notifyUser) {
const users = await userRepository.find();
const manageUsers = users.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(user.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true)
);
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id),
})
.getMany();
pushSubs = allSubs;
}
if (mainUser && pushSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
Promise.all(
pushSubs.map(async (sub) => {
try {
await webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
)
);
} catch (e) {
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
})
);
}
return true;
}
}
export default WebPushAgent;

View File

@@ -1,16 +0,0 @@
export enum NotificationAgentType {
NONE = 0,
EMAIL = 2,
DISCORD = 4,
TELEGRAM = 8,
PUSHOVER = 16,
PUSHBULLET = 32,
SLACK = 64,
}
export const hasNotificationAgentEnabled = (
agent: NotificationAgentType,
value: number
): boolean => {
return !!(value & agent);
};

View File

@@ -1,5 +1,4 @@
import logger from '../../logger';
import { getSettings } from '../settings';
import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification {
@@ -45,13 +44,13 @@ class NotificationManager {
type: Notification,
payload: NotificationPayload
): void {
const settings = getSettings().notifications;
logger.info(`Sending notification(s) for ${Notification[type]}`, {
label: 'Notifications',
subject: payload.subject,
});
this.activeAgents.forEach((agent) => {
if (settings.enabled && agent.shouldSend(type)) {
if (agent.shouldSend(type)) {
agent.send(type, payload);
}
});

View File

@@ -17,6 +17,8 @@ export enum Permission {
AUTO_APPROVE_4K = 32768,
AUTO_APPROVE_4K_MOVIE = 65536,
AUTO_APPROVE_4K_TV = 131072,
REQUEST_MOVIE = 262144,
REQUEST_TV = 524288,
}
export interface PermissionCheckOptions {

View File

@@ -72,6 +72,17 @@ class RadarrScanner
}
private async processRadarrMovie(radarrMovie: RadarrMovie): Promise<void> {
if (!radarrMovie.monitored && !radarrMovie.downloaded) {
this.log(
'Title is unmonitored and has not been downloaded. Skipping item.',
'debug',
{
title: radarrMovie.title,
}
);
return;
}
try {
const server4k = this.enable4kMovie && this.currentServer.is4k;
await this.processMovie(radarrMovie.tmdbId, {

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import { merge } from 'lodash';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import webpush from 'web-push';
import { Permission } from './permissions';
export interface Library {
@@ -13,6 +14,7 @@ export interface Library {
export interface Region {
iso_3166_1: string;
english_name: string;
name?: string;
}
export interface Language {
@@ -81,10 +83,12 @@ export interface MainSettings {
};
hideAvailable: boolean;
localLogin: boolean;
newPlexLogin: boolean;
region: string;
originalLanguage: string;
trustProxy: boolean;
partialRequestsEnabled: boolean;
locale: string;
}
interface PublicSettings {
@@ -101,6 +105,9 @@ interface FullPublicSettings extends PublicSettings {
originalLanguage: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
locale: string;
}
export interface NotificationAgentConfig {
@@ -128,6 +135,8 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
smtpHost: string;
smtpPort: number;
secure: boolean;
ignoreTls: boolean;
requireTls: boolean;
authUser?: string;
authPass?: string;
allowSelfSigned: boolean;
@@ -137,6 +146,13 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
};
}
export interface NotificationAgentLunaSea extends NotificationAgentConfig {
options: {
webhookUrl: string;
profileName: string;
};
}
export interface NotificationAgentTelegram extends NotificationAgentConfig {
options: {
botUsername?: string;
@@ -168,23 +184,37 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
};
}
export enum NotificationAgentKey {
DISCORD = 'discord',
EMAIL = 'email',
PUSHBULLET = 'pushbullet',
PUSHOVER = 'pushover',
SLACK = 'slack',
TELEGRAM = 'telegram',
WEBHOOK = 'webhook',
WEBPUSH = 'webpush',
}
interface NotificationAgents {
discord: NotificationAgentDiscord;
email: NotificationAgentEmail;
lunasea: NotificationAgentLunaSea;
pushbullet: NotificationAgentPushbullet;
pushover: NotificationAgentPushover;
slack: NotificationAgentSlack;
telegram: NotificationAgentTelegram;
webhook: NotificationAgentWebhook;
webpush: NotificationAgentConfig;
}
interface NotificationSettings {
enabled: boolean;
agents: NotificationAgents;
}
interface AllSettings {
clientId: string;
vapidPublic: string;
vapidPrivate: string;
main: MainSettings;
plex: PlexSettings;
radarr: RadarrSettings[];
@@ -203,6 +233,8 @@ class Settings {
constructor(initialSettings?: AllSettings) {
this.data = {
clientId: uuidv4(),
vapidPrivate: '',
vapidPublic: '',
main: {
apiKey: '',
applicationTitle: 'Overseerr',
@@ -216,14 +248,16 @@ class Settings {
},
hideAvailable: false,
localLogin: true,
newPlexLogin: true,
region: '',
originalLanguage: '',
trustProxy: false,
partialRequestsEnabled: true,
locale: 'en',
},
plex: {
name: '',
ip: '127.0.0.1',
ip: '',
port: 32400,
useSsl: false,
libraries: [],
@@ -234,16 +268,17 @@ class Settings {
initialized: false,
},
notifications: {
enabled: true,
agents: {
email: {
enabled: false,
types: 0,
options: {
emailFrom: '',
smtpHost: '127.0.0.1',
smtpHost: '',
smtpPort: 587,
secure: false,
ignoreTls: false,
requireTls: false,
allowSelfSigned: false,
senderName: 'Overseerr',
},
@@ -257,6 +292,14 @@ class Settings {
webhookUrl: '',
},
},
lunasea: {
enabled: false,
types: 0,
options: {
webhookUrl: '',
profileName: '',
},
},
slack: {
enabled: false,
types: 0,
@@ -300,6 +343,11 @@ class Settings {
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
},
},
webpush: {
enabled: false,
types: 0,
options: {},
},
},
},
};
@@ -368,6 +416,9 @@ class Settings {
originalLanguage: this.data.main.originalLanguage,
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
cacheImages: this.data.main.cacheImages,
vapidPublic: this.vapidPublic,
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
locale: this.data.main.locale,
};
}
@@ -388,6 +439,18 @@ class Settings {
return this.data.clientId;
}
get vapidPublic(): string {
this.generateVapidKeys();
return this.data.vapidPublic;
}
get vapidPrivate(): string {
this.generateVapidKeys();
return this.data.vapidPrivate;
}
public regenerateApiKey(): MainSettings {
this.main.apiKey = this.generateApiKey();
this.save();
@@ -398,6 +461,15 @@ class Settings {
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
}
private generateVapidKeys(force = false): void {
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
const vapidKeys = webpush.generateVAPIDKeys();
this.data.vapidPrivate = vapidKeys.privateKey;
this.data.vapidPublic = vapidKeys.publicKey;
this.save();
}
}
/**
* Settings Load
*

View File

@@ -5,6 +5,7 @@ import { getSettings } from '../lib/settings';
export const checkUser: Middleware = async (req, _res, next) => {
const settings = getSettings();
if (req.header('X-API-Key') === settings.main.apiKey) {
const userRepository = getRepository(User);
@@ -28,8 +29,12 @@ export const checkUser: Middleware = async (req, _res, next) => {
if (user) {
req.user = user;
req.locale = user.settings?.locale
? user.settings?.locale
: settings.main.locale;
}
}
next();
};

View File

@@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserPushSubscriptions1618912653565
implements MigrationInterface {
name = 'CreateUserPushSubscriptions1618912653565';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"))`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"))`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
}
}

View File

@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsLocale1619239659754 implements MigrationInterface {
name = 'AddUserSettingsLocale1619239659754';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsNotificationTypes1619339817343
implements MigrationInterface {
name = 'AddUserSettingsNotificationTypes1619339817343';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

View File

@@ -1,12 +1,12 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import PlexTvAPI from '../api/plextv';
import { isAuthenticated } from '../middleware/auth';
import { Permission } from '../lib/permissions';
import logger from '../logger';
import { getSettings } from '../lib/settings';
import { UserType } from '../constants/user';
import { User } from '../entity/User';
import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { isAuthenticated } from '../middleware/auth';
const authRoutes = Router();
@@ -79,6 +79,24 @@ authRoutes.post('/plex', async (req, res, next) => {
// Double check that we didn't create the first admin user before running this
if (!user) {
if (!settings.main.newPlexLogin) {
logger.info(
'Failed sign-in attempt from user who has not been imported to Overseerr.',
{
label: 'Auth',
account: {
...account,
authentication_token: '__REDACTED__',
authToken: '__REDACTED__',
},
}
);
return next({
status: 403,
message: 'Access denied.',
});
}
// If we get to this point, the user does not already exist so we need to create the
// user _assuming_ they have access to the Plex server
const mainUser = await userRepository.findOneOrFail({
@@ -112,7 +130,7 @@ authRoutes.post('/plex', async (req, res, next) => {
);
return next({
status: 403,
message: 'You do not have access to this Plex server.',
message: 'Access denied.',
});
}
}
@@ -128,7 +146,7 @@ authRoutes.post('/plex', async (req, res, next) => {
logger.error(e.message, { label: 'Auth' });
return next({
status: 500,
message: 'Something went wrong. Is your auth token valid?',
message: 'Something went wrong.',
});
}
});

View File

@@ -11,7 +11,7 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -1,16 +1,16 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
import Media from '../entity/Media';
import { isMovie, isPerson } from '../utils/typeHelpers';
import { MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import { User } from '../entity/User';
import { mapProductionCompany } from '../models/Movie';
import { mapNetwork } from '../models/Tv';
import logger from '../logger';
import { sortBy } from 'lodash';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import { User } from '../entity/User';
import { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { mapProductionCompany } from '../models/Movie';
import { mapMovieResult, mapPersonResult, mapTvResult } from '../models/Search';
import { mapNetwork } from '../models/Tv';
import { isMovie, isPerson } from '../utils/typeHelpers';
const createTmdbWithRegionLanaguage = (user?: User): TheMovieDb => {
const settings = getSettings();
@@ -42,7 +42,7 @@ discoverRoutes.get('/movies', async (req, res) => {
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
studio: req.query.studio ? Number(req.query.studio) : undefined,
});
@@ -83,7 +83,7 @@ discoverRoutes.get<{ language: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
@@ -115,7 +115,7 @@ discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const genre = genres.find(
@@ -128,7 +128,7 @@ discoverRoutes.get<{ genreId: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
@@ -164,7 +164,7 @@ discoverRoutes.get<{ studioId: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
studio: Number(req.params.studioId),
});
@@ -204,7 +204,7 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
primaryReleaseDateGte: date,
});
@@ -232,7 +232,7 @@ discoverRoutes.get('/tv', async (req, res) => {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
network: req.query.network ? Number(req.query.network) : undefined,
});
@@ -273,7 +273,7 @@ discoverRoutes.get<{ language: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
@@ -304,7 +304,7 @@ discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const genre = genres.find(
@@ -317,7 +317,7 @@ discoverRoutes.get<{ genreId: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
@@ -352,7 +352,7 @@ discoverRoutes.get<{ networkId: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
network: Number(req.params.networkId),
});
@@ -392,7 +392,7 @@ discoverRoutes.get('/tv/upcoming', async (req, res) => {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
firstAirDateGte: date,
});
@@ -420,7 +420,7 @@ discoverRoutes.get('/trending', async (req, res) => {
const data = await tmdb.getAllTrending({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -461,7 +461,7 @@ discoverRoutes.get<{ keywordId: string }>(
const data = await tmdb.getMoviesByKeyword({
keywordId: Number(req.params.keywordId),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -494,7 +494,7 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
const mappedGenres: GenreSliderItem[] = [];
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
await Promise.all(
@@ -535,7 +535,7 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
const mappedGenres: GenreSliderItem[] = [];
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
await Promise.all(

View File

@@ -81,10 +81,16 @@ router.get('/status/appdata', (_req, res) => {
});
router.use('/user', isAuthenticated(), user);
router.get('/settings/public', async (_req, res) => {
router.get('/settings/public', async (req, res) => {
const settings = getSettings();
return res.status(200).json(settings.fullPublicSettings);
if (!req.user?.settings?.notificationTypes.webpush) {
return res
.status(200)
.json({ ...settings.fullPublicSettings, enablePushRegistration: false });
} else {
return res.status(200).json(settings.fullPublicSettings);
}
});
router.use(
'/settings',
@@ -138,7 +144,7 @@ router.get('/genres/movie', isAuthenticated(), async (req, res) => {
const tmdb = new TheMovieDb();
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);
@@ -148,7 +154,7 @@ router.get('/genres/tv', isAuthenticated(), async (req, res) => {
const tmdb = new TheMovieDb();
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);

View File

@@ -1,11 +1,11 @@
import { Router } from 'express';
import RottenTomatoes from '../api/rottentomatoes';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import logger from '../logger';
import { mapMovieDetails } from '../models/Movie';
import { mapMovieResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import { MediaType } from '../constants/media';
const movieRoutes = Router();
@@ -15,7 +15,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
try {
const tmdbMovie = await tmdb.getMovie({
movieId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
@@ -36,7 +36,7 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
const results = await tmdb.getMovieRecommendations({
movieId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -64,7 +64,7 @@ movieRoutes.get('/:id/similar', async (req, res) => {
const results = await tmdb.getMovieSimilar({
movieId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -16,7 +16,7 @@ personRoutes.get('/:id', async (req, res, next) => {
try {
const person = await tmdb.getPerson({
personId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapPersonDetails(person));
} catch (e) {
@@ -30,7 +30,7 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
const combinedCredits = await tmdb.getPersonCombinedCredits({
personId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const castMedia = await Media.getRelatedMedia(

View File

@@ -139,257 +139,289 @@ requestRoutes.get('/', async (req, res, next) => {
}
});
requestRoutes.post(
'/',
isAuthenticated(Permission.REQUEST),
async (req, res, next) => {
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
requestRoutes.post('/', async (req, res, next) => {
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
try {
let requestUser = req.user;
try {
let requestUser = req.user;
if (
req.body.userId &&
!req.user?.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
return next({
status: 403,
message: 'You do not have permission to modify the request user.',
});
} else if (req.body.userId) {
requestUser = await userRepository.findOneOrFail({
where: { id: req.body.userId },
});
if (
req.body.userId &&
!req.user?.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
return next({
status: 403,
message: 'You do not have permission to modify the request user.',
});
} else if (req.body.userId) {
requestUser = await userRepository.findOneOrFail({
where: { id: req.body.userId },
});
}
if (!requestUser) {
return next({
status: 500,
message: 'User missing from request context.',
});
}
if (
req.body.mediaType === MediaType.MOVIE &&
!req.user?.hasPermission(
req.body.is4k
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: `You do not have permission to make ${
req.body.is4k ? '4K ' : ''
}movie requests.`,
});
} else if (
req.body.mediaType === MediaType.TV &&
!req.user?.hasPermission(
req.body.is4k
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
: [Permission.REQUEST, Permission.REQUEST_TV],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: `You do not have permission to make ${
req.body.is4k ? '4K ' : ''
}series requests.`,
});
}
const quotas = await requestUser.getQuota();
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
return next({
status: 403,
message: 'Movie Quota Exceeded',
});
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
return next({
status: 403,
message: 'Series Quota Exceeded',
});
}
const tmdbMedia =
req.body.mediaType === MediaType.MOVIE
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
relations: ['requests'],
});
if (!media) {
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
mediaType: req.body.mediaType,
});
} else {
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
media.status = MediaStatus.PENDING;
}
if (!requestUser) {
return next({
status: 500,
message: 'User missing from request context.',
});
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
media.status4k = MediaStatus.PENDING;
}
}
const quotas = await requestUser.getQuota();
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
return next({
status: 403,
message: 'Movie Quota Exceeded',
});
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
return next({
status: 403,
message: 'Series Quota Exceeded',
});
}
const tmdbMedia =
req.body.mediaType === MediaType.MOVIE
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
relations: ['requests'],
if (req.body.mediaType === MediaType.MOVIE) {
const existing = await requestRepository.findOne({
where: {
media: {
tmdbId: tmdbMedia.id,
},
requestedBy: req.user,
is4k: req.body.is4k,
},
});
if (!media) {
media = new Media({
if (existing) {
logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id,
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
mediaType: req.body.mediaType,
});
} else {
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
media.status = MediaStatus.PENDING;
}
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
media.status4k = MediaStatus.PENDING;
}
return next({
status: 409,
message: 'Request for this media already exists.',
});
}
if (req.body.mediaType === MediaType.MOVIE) {
const existing = await requestRepository.findOne({
where: {
media: {
tmdbId: tmdbMedia.id,
},
requestedBy: req.user,
is4k: req.body.is4k,
},
});
await mediaRepository.save(media);
if (existing) {
logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id,
mediaType: req.body.mediaType,
});
return next({
status: 409,
message: 'Request for this media already exists.',
});
}
const request = new MediaRequest({
type: MediaType.MOVIE,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
tags: req.body.tags,
});
await mediaRepository.save(media);
await requestRepository.save(request);
return res.status(201).json(request);
} else if (req.body.mediaType === MediaType.TV) {
const requestedSeasons = req.body.seasons as number[];
let existingSeasons: number[] = [];
const request = new MediaRequest({
type: MediaType.MOVIE,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
// We need to check existing requests on this title to make sure we don't double up on seasons that were
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
// (Unless there are no seasons, in which case we abort)
if (media.requests) {
existingSeasons = media.requests
.filter(
(request) =>
request.is4k === req.body.is4k &&
request.status !== MediaRequestStatus.DECLINED
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
tags: req.body.tags,
});
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
await requestRepository.save(request);
return res.status(201).json(request);
} else if (req.body.mediaType === MediaType.TV) {
const requestedSeasons = req.body.seasons as number[];
let existingSeasons: number[] = [];
// We need to check existing requests on this title to make sure we don't double up on seasons that were
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
// (Unless there are no seasons, in which case we abort)
if (media.requests) {
existingSeasons = media.requests
.filter(
(request) =>
request.is4k === req.body.is4k &&
request.status !== MediaRequestStatus.DECLINED
)
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
const finalSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
if (finalSeasons.length === 0) {
return next({
status: 202,
message: 'No seasons available to request',
});
}
await mediaRepository.save(media);
const request = new MediaRequest({
type: MediaType.TV,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
languageProfileId: req.body.languageProfileId,
tags: req.body.tags,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
seasonNumber: sn,
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
})
),
});
await requestRepository.save(request);
return res.status(201).json(request);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
next({ status: 500, message: 'Invalid media type' });
} catch (e) {
next({ status: 500, message: e.message });
const finalSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
if (finalSeasons.length === 0) {
return next({
status: 202,
message: 'No seasons available to request',
});
}
await mediaRepository.save(media);
const request = new MediaRequest({
type: MediaType.TV,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
languageProfileId: req.body.languageProfileId,
tags: req.body.tags,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
seasonNumber: sn,
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
})
),
});
await requestRepository.save(request);
return res.status(201).json(request);
}
next({ status: 500, message: 'Invalid media type' });
} catch (e) {
next({ status: 500, message: e.message });
}
);
});
requestRoutes.get('/count', async (_req, res, next) => {
const requestRepository = getRepository(MediaRequest);
@@ -463,7 +495,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
requestRoutes.put<{ requestId: string }>(
'/:requestId',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
@@ -473,17 +504,30 @@ requestRoutes.put<{ requestId: string }>(
);
if (!request) {
return next({ status: 404, message: 'Request not found' });
return next({ status: 404, message: 'Request not found.' });
}
if (
(request.requestedBy.id !== req.user?.id ||
(req.body.mediaType !== 'tv' &&
!req.user?.hasPermission(Permission.REQUEST_ADVANCED))) &&
!req.user?.hasPermission(Permission.MANAGE_REQUESTS)
) {
return next({
status: 403,
message: 'You do not have permission to modify this request.',
});
}
let requestUser = req.user;
if (
req.body.userId &&
!(
req.user?.hasPermission(Permission.MANAGE_USERS) &&
req.user?.hasPermission(Permission.MANAGE_REQUESTS)
)
req.body.userId !== req.user?.id &&
!req.user?.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
return next({
status: 403,
@@ -516,7 +560,7 @@ requestRoutes.put<{ requestId: string }>(
if (!requestedSeasons || requestedSeasons.length === 0) {
throw new Error(
'Missing seasons. If you want to cancel a tv request, use the DELETE method.'
'Missing seasons. If you want to cancel a series request, use the DELETE method.'
);
}
@@ -603,7 +647,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
) {
return next({
status: 401,
message: 'You do not have permission to remove this request',
message: 'You do not have permission to delete this request.',
});
}
@@ -612,7 +656,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
return res.status(204).send();
} catch (e) {
logger.error(e.message);
next({ status: 404, message: 'Request not found' });
next({ status: 404, message: 'Request not found.' });
}
});
@@ -638,7 +682,7 @@ requestRoutes.post<{
label: 'Media Request',
message: e.message,
});
next({ status: 404, message: 'Request not found' });
next({ status: 404, message: 'Request not found.' });
}
}
);
@@ -682,7 +726,7 @@ requestRoutes.post<{
label: 'Media Request',
message: e.message,
});
next({ status: 404, message: 'Request not found' });
next({ status: 404, message: 'Request not found.' });
}
}
);

View File

@@ -1,7 +1,7 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapSearchResults } from '../models/Search';
import Media from '../entity/Media';
import { mapSearchResults } from '../models/Search';
const searchRoutes = Router();
@@ -11,7 +11,7 @@ searchRoutes.get('/', async (req, res) => {
const results = await tmdb.searchMulti({
query: req.query.query as string,
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -191,7 +191,7 @@ serviceRoutes.get<{ tmdbId: string }>(
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.tmdbId),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const response = await sonarr.getSeriesByTitle(tv.name);

View File

@@ -4,11 +4,13 @@ import fs from 'fs';
import { merge, omit } from 'lodash';
import path from 'path';
import { getRepository } from 'typeorm';
import { URL } from 'url';
import PlexAPI from '../../api/plexapi';
import PlexTvAPI from '../../api/plextv';
import Media from '../../entity/Media';
import { MediaRequest } from '../../entity/MediaRequest';
import { User } from '../../entity/User';
import { PlexConnection } from '../../interfaces/api/plexInterfaces';
import {
LogMessage,
LogsResultsResponse,
@@ -129,13 +131,32 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
if (devices) {
await Promise.all(
devices.map(async (device) => {
const plexDirectConnections: PlexConnection[] = [];
device.connection.forEach((connection) => {
const url = new URL(connection.uri);
if (url.hostname !== connection.address) {
const plexDirectConnection = { ...connection };
plexDirectConnection.address = url.hostname;
plexDirectConnections.push(plexDirectConnection);
// Connect to IP addresses over HTTP
connection.protocol = 'http';
}
});
plexDirectConnections.forEach((plexDirectConnection) => {
device.connection.push(plexDirectConnection);
});
await Promise.all(
device.connection.map(async (connection) => {
const plexDeviceSettings = {
...settings.plex,
ip: connection.address,
port: connection.port,
useSsl: !connection.local && connection.protocol === 'https',
useSsl: connection.protocol === 'https',
};
const plexClient = new PlexAPI({
plexToken: admin.plexToken,
@@ -149,7 +170,7 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
connection.message = 'OK';
} catch (e) {
connection.status = 500;
connection.message = e.message;
connection.message = e.message.split(':')[0];
}
})
);

View File

@@ -2,11 +2,13 @@ import { Router } from 'express';
import { Notification } from '../../lib/notifications';
import DiscordAgent from '../../lib/notifications/agents/discord';
import EmailAgent from '../../lib/notifications/agents/email';
import LunaSeaAgent from '../../lib/notifications/agents/lunasea';
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
import PushoverAgent from '../../lib/notifications/agents/pushover';
import SlackAgent from '../../lib/notifications/agents/slack';
import TelegramAgent from '../../lib/notifications/agents/telegram';
import WebhookAgent from '../../lib/notifications/agents/webhook';
import WebPushAgent from '../../lib/notifications/agents/webpush';
import { getSettings } from '../../lib/settings';
const notificationRoutes = Router();
@@ -26,23 +28,30 @@ notificationRoutes.post('/discord', (req, res) => {
res.status(200).json(settings.notifications.agents.discord);
});
notificationRoutes.post('/discord/test', (req, res, next) => {
notificationRoutes.post('/discord/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const discordAgent = new DiscordAgent(req.body);
discordAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await discordAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Discord notification.',
});
}
});
notificationRoutes.get('/slack', (_req, res) => {
@@ -60,23 +69,30 @@ notificationRoutes.post('/slack', (req, res) => {
res.status(200).json(settings.notifications.agents.slack);
});
notificationRoutes.post('/slack/test', (req, res, next) => {
notificationRoutes.post('/slack/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const slackAgent = new SlackAgent(req.body);
slackAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await slackAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Slack notification.',
});
}
});
notificationRoutes.get('/telegram', (_req, res) => {
@@ -94,23 +110,30 @@ notificationRoutes.post('/telegram', (req, res) => {
res.status(200).json(settings.notifications.agents.telegram);
});
notificationRoutes.post('/telegram/test', (req, res, next) => {
notificationRoutes.post('/telegram/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const telegramAgent = new TelegramAgent(req.body);
telegramAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await telegramAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Telegram notification.',
});
}
});
notificationRoutes.get('/pushbullet', (_req, res) => {
@@ -128,23 +151,30 @@ notificationRoutes.post('/pushbullet', (req, res) => {
res.status(200).json(settings.notifications.agents.pushbullet);
});
notificationRoutes.post('/pushbullet/test', (req, res, next) => {
notificationRoutes.post('/pushbullet/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const pushbulletAgent = new PushbulletAgent(req.body);
pushbulletAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await pushbulletAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Pushbullet notification.',
});
}
});
notificationRoutes.get('/pushover', (_req, res) => {
@@ -162,23 +192,30 @@ notificationRoutes.post('/pushover', (req, res) => {
res.status(200).json(settings.notifications.agents.pushover);
});
notificationRoutes.post('/pushover/test', (req, res, next) => {
notificationRoutes.post('/pushover/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const pushoverAgent = new PushoverAgent(req.body);
pushoverAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await pushoverAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Pushover notification.',
});
}
});
notificationRoutes.get('/email', (_req, res) => {
@@ -196,7 +233,48 @@ notificationRoutes.post('/email', (req, res) => {
res.status(200).json(settings.notifications.agents.email);
});
notificationRoutes.post('/email/test', (req, res, next) => {
notificationRoutes.post('/email/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information is missing from the request.',
});
}
const emailAgent = new EmailAgent(req.body);
if (
await emailAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send email notification.',
});
}
});
notificationRoutes.get('/webpush', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush', (req, res) => {
const settings = getSettings();
settings.notifications.agents.webpush = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
@@ -204,15 +282,22 @@ notificationRoutes.post('/email/test', (req, res, next) => {
});
}
const emailAgent = new EmailAgent(req.body);
emailAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
const webpushAgent = new WebPushAgent(req.body);
if (
await webpushAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send web push notification.',
});
}
});
notificationRoutes.get('/webhook', (_req, res) => {
@@ -260,11 +345,11 @@ notificationRoutes.post('/webhook', (req, res, next) => {
}
});
notificationRoutes.post('/webhook/test', (req, res, next) => {
notificationRoutes.post('/webhook/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
@@ -284,17 +369,65 @@ notificationRoutes.post('/webhook/test', (req, res, next) => {
};
const webhookAgent = new WebhookAgent(testBody);
webhookAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await webhookAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send webhook notification.',
});
}
} catch (e) {
next({ status: 500, message: e.message });
}
});
notificationRoutes.get('/lunasea', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.lunasea);
});
notificationRoutes.post('/lunasea', (req, res) => {
const settings = getSettings();
settings.notifications.agents.lunasea = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.lunasea);
});
notificationRoutes.post('/lunasea/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
});
}
const lunaseaAgent = new LunaSeaAgent(req.body);
if (
await lunaseaAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send web push notification.',
});
}
});
export default notificationRoutes;

View File

@@ -1,11 +1,11 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
import { mapTvResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import logger from '../logger';
import { mapTvResult } from '../models/Search';
import { mapSeasonWithEpisodes, mapTvDetails } from '../models/Tv';
const tvRoutes = Router();
@@ -14,7 +14,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getMedia(tv.id, MediaType.TV);
@@ -35,7 +35,7 @@ tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => {
const season = await tmdb.getTvSeason({
tvId: Number(req.params.id),
seasonNumber: Number(req.params.seasonNumber),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapSeasonWithEpisodes(season));
@@ -47,7 +47,7 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
const results = await tmdb.getTvRecommendations({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -75,7 +75,7 @@ tvRoutes.get('/:id/similar', async (req, res) => {
const results = await tmdb.getTvSimilar({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -5,6 +5,7 @@ import PlexTvAPI from '../../api/plextv';
import { UserType } from '../../constants/user';
import { MediaRequest } from '../../entity/MediaRequest';
import { User } from '../../entity/User';
import { UserPushSubscription } from '../../entity/UserPushSubscription';
import {
QuotaResponse,
UserRequestsResponse,
@@ -81,10 +82,25 @@ router.post(
const body = req.body;
const userRepository = getRepository(User);
const existingUser = await userRepository.findOne({
where: { email: body.email },
});
if (existingUser) {
return next({
status: 409,
message: 'User already exists with submitted email.',
errors: ['USER_EXISTS'],
});
}
const passedExplicitPassword = body.password && body.password.length > 0;
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
if (!passedExplicitPassword && !settings.notifications.agents.email) {
if (
!passedExplicitPassword &&
!settings.notifications.agents.email.enabled
) {
throw new Error('Email notifications must be enabled');
}
@@ -112,6 +128,48 @@ router.post(
}
);
router.post<
never,
unknown,
{
endpoint: string;
p256dh: string;
auth: string;
}
>('/registerPushSubscription', async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const existingSubs = await userPushSubRepository.find({
where: { auth: req.body.auth },
});
if (existingSubs.length > 0) {
logger.debug(
'User push subscription already exists. Skipping registration.',
{ label: 'API' }
);
return res.status(204).send();
}
const userPushSubscription = new UserPushSubscription({
auth: req.body.auth,
endpoint: req.body.endpoint,
p256dh: req.body.p256dh,
user: req.user,
});
userPushSubRepository.save(userPushSubscription);
return res.status(204).send();
} catch (e) {
logger.error('Failed to register user push subscription', {
label: 'API',
});
next({ status: 500, message: 'Failed to register subscription.' });
}
});
router.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const userRepository = getRepository(User);

View File

@@ -7,7 +7,6 @@ import {
UserSettingsGeneralResponse,
UserSettingsNotificationsResponse,
} from '../../interfaces/api/userSettingsInterfaces';
import { NotificationAgentType } from '../../lib/notifications/agenttypes';
import { Permission } from '../../lib/permissions';
import { getSettings } from '../../lib/settings';
import logger from '../../logger';
@@ -52,6 +51,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
return res.status(200).json({
username: user.username,
locale: user.settings?.locale,
region: user.settings?.region,
originalLanguage: user.settings?.originalLanguage,
movieQuotaLimit: user.movieQuotaLimit,
@@ -109,17 +109,24 @@ userSettingsRoutes.post<
if (!user.settings) {
user.settings = new UserSettings({
user: req.user,
locale: req.body.locale,
region: req.body.region,
originalLanguage: req.body.originalLanguage,
});
} else {
user.settings.locale = req.body.locale;
user.settings.region = req.body.region;
user.settings.originalLanguage = req.body.originalLanguage;
}
await userRepository.save(user);
return res.status(200).json({ username: user.username });
return res.status(200).json({
username: user.username,
region: user.settings.region,
locale: user.settings.locale,
originalLanguage: user.settings.originalLanguage,
});
} catch (e) {
next({ status: 500, message: e.message });
}
@@ -243,8 +250,6 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
}
return res.status(200).json({
notificationAgents:
user.settings?.notificationAgents ?? NotificationAgentType.EMAIL,
emailEnabled: settings?.notifications.agents.email.enabled,
pgpKey: user.settings?.pgpKey,
discordEnabled: settings?.notifications.agents.discord.enabled,
@@ -254,6 +259,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
settings?.notifications.agents.telegram.options.botUsername,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
webPushEnabled: settings?.notifications.agents.webpush.enabled,
notificationTypes: user.settings?.notificationTypes ?? {},
});
} catch (e) {
next({ status: 500, message: e.message });
@@ -287,30 +294,32 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
if (!user.settings) {
user.settings = new UserSettings({
user: req.user,
notificationAgents:
req.body.notificationAgents ?? NotificationAgentType.EMAIL,
pgpKey: req.body.pgpKey,
discordId: req.body.discordId,
telegramChatId: req.body.telegramChatId,
telegramSendSilently: req.body.telegramSendSilently,
notificationTypes: req.body.notificationTypes,
});
} else {
user.settings.notificationAgents =
req.body.notificationAgents ?? NotificationAgentType.EMAIL;
user.settings.pgpKey = req.body.pgpKey;
user.settings.discordId = req.body.discordId;
user.settings.telegramChatId = req.body.telegramChatId;
user.settings.telegramSendSilently = req.body.telegramSendSilently;
user.settings.notificationTypes = Object.assign(
{},
user.settings.notificationTypes,
req.body.notificationTypes
);
}
userRepository.save(user);
return res.status(200).json({
notificationAgents: user.settings?.notificationAgents,
pgpKey: user.settings?.pgpKey,
discordId: user.settings?.discordId,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
notificationTypes: user.settings.notificationTypes,
});
} catch (e) {
next({ status: 500, message: e.message });

View File

@@ -6,6 +6,7 @@ declare global {
namespace Express {
export interface Request {
user?: User;
locale?: string;
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44"><path fill="#31C48D" d="M43.5 0H0L43.5 43.5V0Z"/><path fill="#F7FAFC" fill-rule="evenodd" d="M36.707 9.29303C36.8945 9.48056 36.9998 9.73487 36.9998 10C36.9998 10.2652 36.8945 10.5195 36.707 10.707L28.707 18.707C28.5195 18.8945 28.2652 18.9998 28 18.9998C27.7348 18.9998 27.4805 18.8945 27.293 18.707L23.293 14.707C23.1108 14.5184 23.0101 14.2658 23.0123 14.0036C23.0146 13.7414 23.1198 13.4906 23.3052 13.3052C23.4906 13.1198 23.7414 13.0146 24.0036 13.0124C24.2658 13.0101 24.5184 13.1109 24.707 13.293L28 16.586L35.293 9.29303C35.4805 9.10556 35.7348 9.00024 36 9.00024C36.2652 9.00024 36.5195 9.10556 36.707 9.29303Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 744 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 250 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 319 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-icon="discord" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="currentColor" d="M297.216 243.2c0 15.616-11.52 28.416-26.112 28.416-14.336 0-26.112-12.8-26.112-28.416s11.52-28.416 26.112-28.416c14.592 0 26.112 12.8 26.112 28.416zm-119.552-28.416c-14.592 0-26.112 12.8-26.112 28.416s11.776 28.416 26.112 28.416c14.592 0 26.112-12.8 26.112-28.416.256-15.616-11.52-28.416-26.112-28.416zM448 52.736V512c-64.494-56.994-43.868-38.128-118.784-107.776l13.568 47.36H52.48C23.552 451.584 0 428.032 0 398.848V52.736C0 23.552 23.552 0 52.48 0h343.04C424.448 0 448 23.552 448 52.736zm-72.96 242.688c0-82.432-36.864-149.248-36.864-149.248-36.864-27.648-71.936-26.88-71.936-26.88l-3.584 4.096c43.52 13.312 63.744 32.512 63.744 32.512-60.811-33.329-132.244-33.335-191.232-7.424-9.472 4.352-15.104 7.424-15.104 7.424s21.248-20.224 67.328-33.536l-2.56-3.072s-35.072-.768-71.936 26.88c0 0-36.864 66.816-36.864 149.248 0 0 21.504 37.12 78.08 38.912 0 0 9.472-11.52 17.152-21.248-32.512-9.728-44.8-30.208-44.8-30.208 3.766 2.636 9.976 6.053 10.496 6.4 43.21 24.198 104.588 32.126 159.744 8.96 8.96-3.328 18.944-8.192 29.44-15.104 0 0-12.8 20.992-46.336 30.464 7.68 9.728 16.896 20.736 16.896 20.736 56.576-1.792 78.336-38.912 78.336-38.912z"/></svg>
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path d="m81.9 83.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1 0.1-6.1-4.5-11.1-10.2-11.1zm36.5 0c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z" fill="currentColor"/><path d="m167 0h-134c-11.3 0-20.5 9.2-20.5 20.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19v-179.4c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-0.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-0.9-4.8-2-7.3-3.4-0.3-0.2-0.6-0.3-0.9-0.5-0.2-0.1-0.3-0.2-0.4-0.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-0.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3 0.6-0.1 1.1-0.2 1.7-0.2 6.1-0.8 13-1 20.2-0.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-0.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 750 750" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="m554.69 180.46c-333.63 0-452.75 389.23-556.05 389.23 185.37 0 237.85-247.18 419.12-247.18l47.24-102.05z"/><path d="m749.31 375.08c0 107.48-87.14 194.61-194.62 194.61s-194.62-87.13-194.62-194.61 87.13-194.62 194.62-194.62c7.391-2e-3 14.776 0.412 22.12 1.24-78.731 10.172-136.59 78.893-133.2 158.2 3.393 79.313 66.907 142.84 146.22 146.25 79.311 3.411 148.05-54.43 158.24-133.16 0.826 7.331 1.24 14.703 1.24 22.08z"/></g></svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 256 256"><metadata/><path fill="#fff" d="m128 0c-70.692 0-128 57.308-128 128s57.308 128 128 128 128-57.308 128-128-57.308-128-128-128zm-51 64.268h3.334c16.733 0 16.732-5.78e-4 16.732 16.732v91.867c-1e-6 16.733 5.77e-4 16.732-16.732 16.732h-3.334c-16.733 0-16.732 5.8e-4 -16.732-16.732v-91.867c0-16.733-5.78e-4 -16.732 16.732-16.732zm44.041 0h37.537c32.178 0 52.627 32.273 52.627 63.025 0 30.752-20.627 62.307-52.627 62.307h-37.537c-5.699 0-8.5078-2.8088-8.5078-8.5078v-108.32c0-5.698 2.8088-8.5059 8.5078-8.5059z" mask="url(#mask-2)"/></svg>
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="m128 0c-70.692 0-128 57.308-128 128s57.308 128 128 128 128-57.308 128-128-57.308-128-128-128zm-51 64.268h3.334c16.733 0 16.732-5.78e-4 16.732 16.732v91.867c-1e-6 16.733 5.77e-4 16.732-16.732 16.732h-3.334c-16.733 0-16.732 5.8e-4 -16.732-16.732v-91.867c0-16.733-5.78e-4 -16.732 16.732-16.732zm44.041 0h37.537c32.178 0 52.627 32.273 52.627 63.025s-20.627 62.307-52.627 62.307h-37.537c-5.699 0-8.5078-2.8088-8.5078-8.5078v-108.32c0-5.698 2.8088-8.5059 8.5078-8.5059z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 563 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="57 57 602 602"><g id="layer1" fill="#1F2937" fill-rule="evenodd" stroke="none" stroke-width="1" opacity=".91" transform="translate(58.964119, 58.887520)"><ellipse style="fill:#fff;fill-rule:evenodd;stroke:#fff;stroke-width:0" cx="216.308" cy="152.076" rx="296.855" ry="296.855" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)"/><path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill-rule:nonzero;white-space:pre"/></g></svg>
<svg viewBox="57 57 593.71002 593.71002" xmlns="http://www.w3.org/2000/svg"><g transform="translate(55.201 55.147)"><circle transform="rotate(132.42 225.16 243.54)" cx="216.31" cy="152.08" r="296.86" fill="currentColor"/><path d="m280.95 172.51 74.48-9.8-72.52 163.66c12.74-0.98 25.233-5.307 37.48-12.98 12.253-7.68 23.527-17.317 33.82-28.91 10.287-11.6 19.187-24.503 26.7-38.71 7.513-14.213 12.903-28.18 16.17-41.9 1.96-8.493 2.86-16.66 2.7-24.5-0.167-7.84-2.21-14.7-6.13-20.58s-9.883-10.617-17.89-14.21c-8-3.593-18.86-5.39-32.58-5.39-16.007 0-31.77 2.613-47.29 7.84-15.513 5.227-29.887 12.823-43.12 22.79-13.227 9.96-24.74 22.373-34.54 37.24-9.8 14.86-16.823 31.763-21.07 50.71-1.633 6.207-2.613 11.187-2.94 14.94-0.327 3.76-0.407 6.863-0.24 9.31 0.16 2.453 0.483 4.333 0.97 5.64 0.493 1.307 0.903 2.613 1.23 3.92-16.66 0-28.83-3.35-36.51-10.05-7.673-6.693-9.55-18.37-5.63-35.03 3.92-17.313 12.823-33.81 26.71-49.49 13.88-15.68 30.373-29.483 49.48-41.41 19.113-11.92 40.02-21.39 62.72-28.41 22.707-7.027 44.84-10.54 66.4-10.54 18.947 0 34.87 2.693 47.77 8.08 12.907 5.393 22.953 12.5 30.14 21.32s11.677 19.11 13.47 30.87c1.8 11.76 1.23 24.01-1.71 36.75-3.593 15.353-10.373 30.79-20.34 46.31-9.96 15.513-22.453 29.56-37.48 42.14-15.027 12.573-32.26 22.78-51.7 30.62-19.433 7.84-40.093 11.76-61.98 11.76h-2.45l-62.23 139.65h-70.56z"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-icon="slack" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="currentColor" d="M94.12 315.1c0 25.9-21.16 47.06-47.06 47.06S0 341 0 315.1c0-25.9 21.16-47.06 47.06-47.06h47.06v47.06zm23.72 0c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06v117.84c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06V315.1zm47.06-188.98c-25.9 0-47.06-21.16-47.06-47.06S139 32 164.9 32s47.06 21.16 47.06 47.06v47.06H164.9zm0 23.72c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06H47.06C21.16 243.96 0 222.8 0 196.9s21.16-47.06 47.06-47.06H164.9zm188.98 47.06c0-25.9 21.16-47.06 47.06-47.06 25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06h-47.06V196.9zm-23.72 0c0 25.9-21.16 47.06-47.06 47.06-25.9 0-47.06-21.16-47.06-47.06V79.06c0-25.9 21.16-47.06 47.06-47.06 25.9 0 47.06 21.16 47.06 47.06V196.9zM283.1 385.88c25.9 0 47.06 21.16 47.06 47.06 0 25.9-21.16 47.06-47.06 47.06-25.9 0-47.06-21.16-47.06-47.06v-47.06h47.06zm0-23.72c-25.9 0-47.06-21.16-47.06-47.06 0-25.9 21.16-47.06 47.06-47.06h117.84c25.9 0 47.06 21.16 47.06 47.06 0 25.9-21.16 47.06-47.06 47.06H283.1z"/></svg>
<svg viewBox="0 0 448 448" xmlns="http://www.w3.org/2000/svg"><path d="m94.12 283.1c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06 21.16-47.06 47.06-47.06h47.06zm23.72 0c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06v117.84c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06zm47.06-188.98c-25.9 0-47.06-21.16-47.06-47.06s21.16-47.06 47.06-47.06 47.06 21.16 47.06 47.06v47.06zm0 23.72c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06h-117.84c-25.9 0-47.06-21.16-47.06-47.06s21.16-47.06 47.06-47.06zm188.98 47.06c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06-21.16 47.06-47.06 47.06h-47.06zm-23.72 0c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06v-117.84c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06zm-47.06 188.98c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06-47.06-21.16-47.06-47.06v-47.06zm0-23.72c-25.9 0-47.06-21.16-47.06-47.06s21.16-47.06 47.06-47.06h117.84c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1014 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="currentColor" fill-rule="evenodd" d="M0 24C0 37.2548 10.7452 48 24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24ZM19.6 35L20.0083 28.8823L20.008 28.882L31.1369 18.839C31.6253 18.4055 31.0303 18.1941 30.3819 18.5873L16.6473 27.2523L10.7147 25.4007C9.4335 25.0084 9.4243 24.128 11.0023 23.4951L34.1203 14.5809C35.1762 14.1015 36.1953 14.8345 35.7922 16.4505L31.8552 35.0031C31.5803 36.3215 30.7837 36.6368 29.68 36.0278L23.6827 31.5969L20.8 34.4C20.7909 34.4088 20.7819 34.4176 20.7729 34.4264C20.4505 34.7403 20.1837 35 19.6 35Z" clip-rule="evenodd"/></svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="m0 24c0 13.255 10.745 24 24 24s24-10.745 24-24-10.745-24-24-24-24 10.745-24 24zm19.6 11l0.4083-6.1177-3e-4 -3e-4 11.129-10.043c0.4884-0.4335-0.1066-0.6449-0.755-0.2517l-13.735 8.665-5.9326-1.8516c-1.2812-0.3923-1.2904-1.2727 0.2876-1.9056l23.118-8.9142c1.0559-0.4794 2.075 0.2536 1.6719 1.8696l-3.937 18.553c-0.2749 1.3184-1.0715 1.6337-2.1752 1.0247l-5.9973-4.4309-2.8827 2.8031c-0.0091 0.0088-0.0181 0.0176-0.0271 0.0264-0.3224 0.3139-0.5892 0.5736-1.1729 0.5736z" clip-rule="evenodd" fill="currentColor" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44"><path fill="#ED8936" d="M43.5 0H0L43.5 43.5V0Z"/><path fill="#fff" d="M31 5.79999C29.5678 5.79999 28.1943 6.36891 27.1816 7.38161C26.1689 8.39431 25.6 9.76782 25.6 11.2V14.4274L24.9637 15.0637C24.8379 15.1896 24.7522 15.3499 24.7175 15.5245C24.6828 15.699 24.7006 15.8799 24.7687 16.0444C24.8368 16.2088 24.9521 16.3494 25.1001 16.4482C25.2481 16.5471 25.422 16.5999 25.6 16.6H36.4C36.578 16.5999 36.7519 16.5471 36.8999 16.4482C37.0479 16.3494 37.1632 16.2088 37.2313 16.0444C37.2994 15.8799 37.3172 15.699 37.2825 15.5245C37.2478 15.3499 37.1621 15.1896 37.0363 15.0637L36.4 14.4274V11.2C36.4 9.76782 35.8311 8.39431 34.8184 7.38161C33.8057 6.36891 32.4322 5.79999 31 5.79999ZM31 20.2C30.2839 20.2 29.5972 19.9155 29.0908 19.4092C28.5845 18.9028 28.3 18.2161 28.3 17.5H33.7C33.7 18.2161 33.4155 18.9028 32.9092 19.4092C32.4028 19.9155 31.7161 20.2 31 20.2Z"/></svg>

Before

Width:  |  Height:  |  Size: 962 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"><g fill="none"><path fill="#FFF" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></g></svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,8 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560">
<g fill="none" transform="translate(33 140)">
<path fill="#FFF" d="M43.8020066,267.3152 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.3152 Z M266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 C26.3585902,221.21181 24.03,207.991848 22.1723803,189.066133 L266.684852,192.017143 Z M275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.325333 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 L275.03101,89.3083429 Z M302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 Z"/>
<path fill="#00641E" d="M303.565869,264.667352 C306.720846,256.846476 317.903331,252.081752 325.93259,252.63901 C334.515108,253.234819 343.631626,262.264838 345.224872,271.145905 C345.520761,270.823467 345.830656,270.518552 346.145803,270.218895 C348.901593,267.583314 352.35421,265.834438 356.132479,265.352533 C355.554708,262.790552 355.416393,260.048076 355.812079,257.233752 C357.207482,247.3328 365.145698,239.907962 374.234203,239.981562 C380.099449,240.028876 385.245108,243.032457 388.597928,247.646476 C388.897318,247.271467 389.228223,246.928 389.550374,246.577524 C393.384669,226.586362 395.814807,203.999924 396.368066,180.030857 C398.267705,97.6111238 377.375174,30.2776381 349.70522,29.6397714 C322.033515,29.0001524 298.061292,95.2962286 296.161652,177.715962 C296.161652,177.715962 294.696216,207.778057 303.565869,264.667352"/>
<path fill="#FFD700" d="M490.910577,354.797562 C492.545843,352.0656 493.45977,348.7904 493.396741,345.310171 C493.927239,334.065143 486.214879,323.871543 475.484105,325.000076 C475.794,323.713829 475.997095,322.376762 476.075882,320.997638 C476.718433,309.733333 468.957049,300.025143 458.739266,299.315429 C458.515161,299.30141 458.294557,299.2944 458.072203,299.28739 C459.134951,296.492343 459.661948,293.371352 459.47461,290.06461 C458.945862,280.692876 452.488839,272.796648 444.088407,271.242286 C441.054236,270.681524 438.111108,270.960152 435.416597,271.894171 C432.870905,265.617143 427.525652,260.961067 421.023108,259.977981 C420.508367,249.786133 413.153174,241.397486 403.68299,240.740343 C397.728452,240.326781 392.257141,243.064 388.597928,247.646476 C385.245108,243.032457 380.099449,240.030629 374.234203,239.983314 C365.145698,239.907962 357.207482,247.3328 355.812079,257.235505 C355.416393,260.048076 355.554708,262.790552 356.132479,265.354286 C352.35421,265.834438 348.901593,267.585067 346.145803,270.218895 C345.830656,270.518552 345.520761,270.823467 345.224872,271.145905 C343.631626,262.264838 334.515108,253.236571 325.93259,252.63901 C317.903331,252.081752 306.575528,256.963886 303.565869,264.667352 C304.885987,278.101105 313.275915,314.72061 343.621121,347.570743 L343.890748,347.590019 C346.816367,350.239619 350.636656,351.699352 354.709062,351.33661 C357.233744,351.110552 359.558833,350.206324 361.56177,348.811429 L362.050249,348.844724 C364.720249,350.700495 367.929502,351.671314 371.320839,351.369905 C372.616446,351.254248 373.852525,350.942324 375.027325,350.497219 C377.933685,356.51139 384.41522,360.398171 391.649607,359.760305 C397.25223,359.266133 402.014459,356.164419 404.81402,351.799238 L405.720944,351.862324 C408.517003,354.636343 412.233993,356.252038 416.24337,356.131124 C419.557672,361.149943 425.645272,364.237638 432.363167,363.647086 C434.893102,363.424533 437.254957,362.692038 439.361193,361.582781 C442.875089,365.90941 448.642289,368.481905 454.969751,367.924648 C461.22543,367.376152 466.509403,363.904686 469.384249,359.104914 C472.208321,361.374248 475.746728,362.592152 479.503987,362.257448 C483.149193,361.931505 486.38821,360.208914 488.821849,357.603124 L489.228039,357.631162 C489.781298,356.828571 490.25402,356.006705 490.693475,355.177829 C490.70398,355.162057 490.712734,355.144533 490.721489,355.128762 C490.781016,355.018362 490.854551,354.909714 490.910577,354.797562"/>
<path fill="#04A53C" d="M281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 Z M302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.316952 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 Z M33.8766098,243.950705 C26.3585902,221.21181 24.03,207.9936 22.1723803,189.066133 L266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 Z M33.8766098,108.391771 L275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.327086 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 Z M378.597246,25.7126857 C363.342354,7.93478095 352.390977,-0.411809524 343.010085,0.672914286 C341.261016,0.895466667 76.1168852,37.8952381 76.1168852,37.8952381 C34.0429377,42.1780571 0.416695082,103.32739 0,176.172114 C0.416695082,249.015086 34.0429377,310.164419 76.1168852,314.44899 C76.1168852,314.44899 341.758249,351.583695 343.010085,351.669562 C345.228374,351.655543 347.418649,351.357638 349.57741,350.803886 C347.476426,350.178286 345.538269,349.083048 343.890748,347.590019 L343.621121,347.570743 C313.275915,314.722362 304.885987,278.101105 303.565869,264.667352 C303.56937,264.656838 303.576374,264.648076 303.579875,264.637562 C303.576374,264.648076 303.56937,264.656838 303.565869,264.667352 C294.696216,207.778057 296.161652,177.715962 296.161652,177.715962 C298.061292,95.2962286 322.033515,29.0001524 349.70522,29.638019 C377.375174,30.2776381 398.267705,97.6111238 396.368066,180.030857 C395.814807,203.999924 393.384669,226.586362 389.550374,246.577524 C393.489718,242.226362 398.773692,240.393371 403.68299,240.740343 C404.586413,240.805181 405.465325,240.957638 406.326728,241.155657 C423.131095,149.125867 405.514348,59.262019 378.597246,25.7126857 Z"/>
</g>
</svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><g transform="translate(33 140)"><path d="m43.802 267.32l237.94 23.482c4.7937 14.937 11.149 29.517 20.259 40.256l-230.27-30.125c-10.248-6.7519-20.861-19.877-27.936-33.612zm222.88-75.298c0.60053 20.906 3.4316 47.964 8.3462 71.017l-241.15-19.083c-7.518-22.739-9.8466-35.959-11.704-54.885l244.51 2.951zm8.3462-102.71c-4.9146 23.053-7.7456 50.111-8.3462 71.017l-244.51 2.951c1.8576-18.926 4.1862-32.146 11.704-54.885l241.15-19.083zm26.973-68.019c-9.1095 10.74-15.465 25.318-20.259 40.257l-237.94 23.48c7.0751-13.735 17.689-26.859 27.936-33.612l230.27-30.125z" fill="#fff"/><path d="m303.57 264.67c3.155-7.8209 14.337-12.586 22.367-12.028 8.5825 0.59581 17.699 9.6258 19.292 18.507 0.29589-0.32244 0.60578-0.62735 0.92093-0.92701 2.7558-2.6356 6.2084-4.3845 9.9867-4.8664-0.57777-2.562-0.71609-5.3045-0.3204-8.1188 1.3954-9.901 9.3336-17.326 18.422-17.252 5.8652 0.047314 11.011 3.0509 14.364 7.6649 0.29939-0.37501 0.6303-0.71848 0.95245-1.069 3.8343-19.991 6.2644-42.578 6.8177-66.547 1.8996-82.42-18.993-149.75-46.663-150.39-27.672-0.63962-51.644 65.656-53.544 148.08 0 0-1.4654 30.062 7.4042 86.951" fill="#00641E"/><path d="m490.91 354.8c1.6353-2.732 2.5492-6.0072 2.4862-9.4874 0.5305-11.245-7.1819-21.439-17.913-20.31 0.3099-1.2862 0.51299-2.6233 0.59178-4.0024 0.64255-11.264-7.1188-20.972-17.337-21.682-0.2241-0.014019-0.44471-0.021029-0.66706-0.028039 1.0627-2.795 1.5897-5.916 1.4024-9.2228-0.52875-9.3717-6.9858-17.268-15.386-18.822-3.0342-0.56076-5.9773-0.28213-8.6718 0.65188-2.5457-6.277-7.8909-10.933-14.393-11.916-0.51474-10.192-7.8699-18.58-17.34-19.238-5.9545-0.41356-11.426 2.3237-15.085 6.9061-3.3528-4.614-8.4985-7.6158-14.364-7.6632-9.0885-0.075352-17.027 7.3495-18.422 17.252-0.39569 2.8126-0.25737 5.555 0.3204 8.1188-3.7783 0.48015-7.2309 2.2308-9.9867 4.8646-0.31515 0.29966-0.62504 0.60457-0.92093 0.92701-1.5932-8.8811-10.71-17.909-19.292-18.507-8.0293-0.55726-19.357 4.3249-22.367 12.028 1.3201 13.434 9.71 50.053 40.055 82.903l0.26963 0.019276c2.9256 2.6496 6.7459 4.1093 10.818 3.7466 2.5247-0.22606 4.8498-1.1303 6.8527-2.5252l0.48848 0.033295c2.67 1.8558 5.8793 2.8266 9.2706 2.5252 1.2956-0.11566 2.5317-0.42758 3.7065-0.87269 2.9064 6.0142 9.3879 9.901 16.622 9.2631 5.6026-0.49417 10.365-3.5959 13.164-7.9611l0.90692 0.063086c2.7961 2.774 6.513 4.3897 10.522 4.2688 3.3143 5.0188 9.4019 8.1065 16.12 7.516 2.5299-0.22255 4.8918-0.95505 6.998-2.0643 3.5139 4.3266 9.2811 6.8991 15.609 6.3419 6.2557-0.5485 11.54-4.02 14.414-8.8197 2.8241 2.2693 6.3625 3.4872 10.12 3.1525 3.6452-0.32594 6.8842-2.0485 9.3179-4.6543l0.40619 0.028038c0.55326-0.80259 1.026-1.6245 1.4654-2.4533 0.010505-0.015772 0.019259-0.033296 0.028014-0.049067 0.059527-0.1104 0.13306-0.21905 0.18909-0.3312" fill="#FFD700"/><path d="m281.75 61.547l-237.94 23.48c7.0751-13.735 17.689-26.859 27.936-33.612l230.27-30.125c-9.1095 10.74-15.465 25.318-20.259 40.257zm20.259 269.51l-230.27-30.125c-10.248-6.7519-20.861-19.877-27.936-33.611l237.94 23.48c4.7937 14.937 11.149 29.517 20.259 40.256zm-268.13-87.102c-7.518-22.739-9.8466-35.957-11.704-54.885l244.51 2.951c0.60053 20.906 3.4316 47.964 8.3462 71.017l-241.15-19.083zm0-135.56l241.15-19.083c-4.9146 23.053-7.7456 50.111-8.3462 71.019l-244.51 2.9493c1.8576-18.926 4.1862-32.146 11.704-54.885zm344.72-82.679c-15.255-17.778-26.206-26.124-35.587-25.04-1.7491 0.22255-266.89 37.222-266.89 37.222-42.074 4.2828-75.7 65.432-76.117 138.28 0.4167 72.843 34.043 133.99 76.117 138.28 0 0 265.64 37.135 266.89 37.221 2.2183-0.014019 4.4086-0.31192 6.5673-0.86568-2.101-0.6256-4.0391-1.7208-5.6867-3.2139l-0.26963-0.019276c-30.345-32.848-38.735-69.47-40.055-82.903 0.003501-0.010514 0.010505-0.019276 0.014006-0.02979-0.003501 0.010514-0.010505 0.019276-0.014006 0.02979-8.8697-56.889-7.4042-86.951-7.4042-86.951 1.8996-82.42 25.872-148.72 53.544-148.08 27.67 0.63962 48.562 67.973 46.663 150.39-0.55326 23.969-2.9834 46.556-6.8177 66.547 3.9393-4.3512 9.2233-6.1842 14.133-5.8372 0.90342 0.064838 1.7823 0.2173 2.6437 0.41531 16.804-92.03-0.81238-181.89-27.729-215.44z" fill="#04A53C"/></g></svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"><g fill="none"><path fill="#FA320A" d="M478.29 296.976c-3.99-63.966-36.52-111.823-85.468-138.579.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.265 6.78 108.735 108.63 170.89 211.193 164.49 102.556-6.395 193.466-80.572 186.683-189.307"/><path fill="#00912D" d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023"/></g></svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path d="m478.29 296.98c-3.99-63.966-36.52-111.82-85.468-138.58 0.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.28 7.584 0.285 8.519-1.378 50.072-59.914 52.483-1.382 0.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.26 6.78 108.74 108.63 170.89 211.19 164.49 102.56-6.395 193.47-80.572 186.68-189.31" fill="#FA320A"/><path d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023" fill="#00912D"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"><path fill="#0AC855" d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878"/></svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878" fill="#0AC855"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 575 289.83"><defs><path id="d1pwhf9wy2" d="M575 24.91C573.44 12.15 563.97 1.98 551.91 0C499.05 0 76.18 0 23.32 0C10.11 2.17 0 14.16 0 28.61C0 51.84 0 237.64 0 260.86C0 276.86 12.37 289.83 27.64 289.83C79.63 289.83 495.6 289.83 547.59 289.83C561.65 289.83 573.26 278.82 575 264.57C575 216.64 575 48.87 575 24.91Z"/><path id="g5jjnq26yS" d="M69.35 58.24L114.98 58.24L114.98 233.89L69.35 233.89L69.35 58.24Z"/><path id="i3Prh1JpXt" d="M201.2 139.15C197.28 112.38 195.1 97.5 194.67 94.53C192.76 80.2 190.94 67.73 189.2 57.09C185.25 57.09 165.54 57.09 130.04 57.09L130.04 232.74L170.01 232.74L170.15 116.76L186.97 232.74L215.44 232.74L231.39 114.18L231.54 232.74L271.38 232.74L271.38 57.09L211.77 57.09L201.2 139.15Z"/><path id="a4ov9rRGQm" d="M346.71 93.63C347.21 95.87 347.47 100.95 347.47 108.89C347.47 115.7 347.47 170.18 347.47 176.99C347.47 188.68 346.71 195.84 345.2 198.48C343.68 201.12 339.64 202.43 333.09 202.43C333.09 190.9 333.09 98.66 333.09 87.13C338.06 87.13 341.45 87.66 343.25 88.7C345.05 89.75 346.21 91.39 346.71 93.63ZM367.32 230.95C372.75 229.76 377.31 227.66 381.01 224.67C384.7 221.67 387.29 217.52 388.77 212.21C390.26 206.91 391.14 196.38 391.14 180.63C391.14 174.47 391.14 125.12 391.14 118.95C391.14 102.33 390.49 91.19 389.48 85.53C388.46 79.86 385.93 74.71 381.88 70.09C377.82 65.47 371.9 62.15 364.12 60.13C356.33 58.11 343.63 57.09 321.54 57.09C319.27 57.09 307.93 57.09 287.5 57.09L287.5 232.74L342.78 232.74C355.52 232.34 363.7 231.75 367.32 230.95Z"/><path id="fk968BpsX" d="M464.76 204.7C463.92 206.93 460.24 208.06 457.46 208.06C454.74 208.06 452.93 206.98 452.01 204.81C451.09 202.65 450.64 197.72 450.64 190C450.64 185.36 450.64 148.22 450.64 143.58C450.64 135.58 451.04 130.59 451.85 128.6C452.65 126.63 454.41 125.63 457.13 125.63C459.91 125.63 463.64 126.76 464.6 129.03C465.55 131.3 466.03 136.15 466.03 143.58C466.03 146.58 466.03 161.58 466.03 188.59C465.74 197.84 465.32 203.21 464.76 204.7ZM406.68 231.21L447.76 231.21C449.47 224.5 450.41 220.77 450.6 220.02C454.32 224.52 458.41 227.9 462.9 230.14C467.37 232.39 474.06 233.51 479.24 233.51C486.45 233.51 492.67 231.62 497.92 227.83C503.16 224.05 506.5 219.57 507.92 214.42C509.34 209.26 510.05 201.42 510.05 190.88C510.05 185.95 510.05 146.53 510.05 141.6C510.05 131 509.81 124.08 509.34 120.83C508.87 117.58 507.47 114.27 505.14 110.88C502.81 107.49 499.42 104.86 494.98 102.98C490.54 101.1 485.3 100.16 479.26 100.16C474.01 100.16 467.29 101.21 462.81 103.28C458.34 105.35 454.28 108.49 450.64 112.7C450.64 108.89 450.64 89.85 450.64 55.56L406.68 55.56L406.68 231.21Z"/></defs><g><g><g><use fill="#f6c700" fill-opacity="1" opacity="1" xlink:href="#d1pwhf9wy2"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#d1pwhf9wy2"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#g5jjnq26yS"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#g5jjnq26yS"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#i3Prh1JpXt"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#i3Prh1JpXt"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#a4ov9rRGQm"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#a4ov9rRGQm"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#fk968BpsX"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#fk968BpsX"/></g></g></g></g></svg>
<svg version="1.1" viewBox="0 0 575 289.83" xmlns="http://www.w3.org/2000/svg"><path fill="#f6c700" d="m575 24.91c-1.56-12.76-11.03-22.93-23.09-24.91h-528.59c-13.21 2.17-23.32 14.16-23.32 28.61v232.25c0 16 12.37 28.97 27.64 28.97h519.95c14.06 0 25.67-11.01 27.41-25.26v-239.66z"/><path stroke="#000" d="m69.35 58.24h45.63v175.65h-45.63v-175.65z"/><path stroke="#000" d="m201.2 139.15c-3.92-26.77-6.1-41.65-6.53-44.62-1.91-14.33-3.73-26.8-5.47-37.44h-59.16v175.65h39.97l0.14-115.98 16.82 115.98h28.47l15.95-118.56 0.15 118.56h39.84v-175.65h-59.61l-10.57 82.06z"/><path stroke="#000" d="m346.71 93.63c0.5 2.24 0.76 7.32 0.76 15.26v68.1c0 11.69-0.76 18.85-2.27 21.49-1.52 2.64-5.56 3.95-12.11 3.95v-115.3c4.97 0 8.36 0.53 10.16 1.57 1.8 1.05 2.96 2.69 3.46 4.93zm20.61 137.32c5.43-1.19 9.99-3.29 13.69-6.28 3.69-3 6.28-7.15 7.76-12.46 1.49-5.3 2.37-15.83 2.37-31.58v-61.68c0-16.62-0.65-27.76-1.66-33.42-1.02-5.67-3.55-10.82-7.6-15.44-4.06-4.62-9.98-7.94-17.76-9.96-7.79-2.02-20.49-3.04-42.58-3.04h-34.04v175.65h55.28c12.74-0.4 20.92-0.99 24.54-1.79z"/><path stroke="#000" d="m464.76 204.7c-0.84 2.23-4.52 3.36-7.3 3.36-2.72 0-4.53-1.08-5.45-3.25-0.92-2.16-1.37-7.09-1.37-14.81v-46.42c0-8 0.4-12.99 1.21-14.98 0.8-1.97 2.56-2.97 5.28-2.97 2.78 0 6.51 1.13 7.47 3.4 0.95 2.27 1.43 7.12 1.43 14.55v45.01c-0.29 9.25-0.71 14.62-1.27 16.11zm-58.08 26.51h41.08c1.71-6.71 2.65-10.44 2.84-11.19 3.72 4.5 7.81 7.88 12.3 10.12 4.47 2.25 11.16 3.37 16.34 3.37 7.21 0 13.43-1.89 18.68-5.68 5.24-3.78 8.58-8.26 10-13.41 1.42-5.16 2.13-13 2.13-23.54v-49.28c0-10.6-0.24-17.52-0.71-20.77s-1.87-6.56-4.2-9.95-5.72-6.02-10.16-7.9-9.68-2.82-15.72-2.82c-5.25 0-11.97 1.05-16.45 3.12-4.47 2.07-8.53 5.21-12.17 9.42v-57.14h-43.96v175.65z"/></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" data-name="Layer 1" viewBox="0 0 320.03 103.61"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#radial-gradient)}.cls-3{fill:#e5a00d}</style><radialGradient id="radial-gradient" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#f9be03"/><stop offset=".51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><title>plex-logo</title><polygon id="X" points="320.03 -.09 289.96 -.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79 320.03 -.09" class="cls-1"/><g id="chevron"><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -.09" class="cls-2"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -.09" class="cls-3"/></g><path id="E" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z" class="cls-1"/><path id="L" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z" class="cls-1"/><path id="P" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z" class="cls-1"/></svg>
<svg viewBox="0 0 320.03 103.61" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#a)}.cls-3{fill:#e5a00d}</style><radialGradient id="a" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop stop-color="#f9be03" offset=".17"/><stop stop-color="#e8a50b" offset=".51"/><stop stop-color="#cc7c19" offset="1"/></radialGradient></defs><polygon class="cls-1" points="320.03 -0.09 289.96 -0.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79"/><polygon class="cls-2" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76"/><polygon class="cls-3" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76"/><path class="cls-1" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z"/><path class="cls-1" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z"/><path class="cls-1" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg version="1.1" viewBox="0 0 1000 1115.2" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)" d="m105.76 154.15-1.263 714.59c-60.261 6.782-105.02-23.858-104.28-84.025l-0.216-594.25c2.312-188.02 175.85-231.02 280.22-154.52l530.2 314.93c74.563 53.572 88.403 151.53 49.965 218.76-6.873-52.739-29.067-83.101-73.823-113.74l-597.52-345.84c-44.756-30.639-82.453-23.58-83.286 44.109zm-54.377 751.54c44.941 15.597 90.16 8.63 128.04-13.47l621.16-353.43c36.958 53.109 28.79 105.66-16.706 135.19l-522.65 294.46c-75.673 36.68-172.98-2.127-209.85-62.757z" fill="#fff"/><path transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)" d="m240.52 702.59 365.02-216.68-364.35-197.07z" fill="#ffc230"/></svg>

After

Width:  |  Height:  |  Size: 737 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 216.7 216.9" xmlns="http://www.w3.org/2000/svg"><path d="M216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95C53.05 10.517 78.617 0 108.45 0c29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" clip-rule="evenodd" fill="#EEE" fill-rule="evenodd"/><path d="M194.65 42.5l-22.4 22.4C159.152 77.998 158 89.4 158 109.5c0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05L61.1 43.85C74.102 56.852 90.767 60.2 108.7 60.2c18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" clip-rule="evenodd" fill="#3A3F51" fill-rule="evenodd"/><g clip-rule="evenodd"><path d="M78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3 0 8.267-2.916 15.35-8.75 21.25-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z" fill="#0CF" fill-rule="evenodd"/><path d="M157.8 59.75l-15 14.65M30.785 32.526L71.65 73.25m84.6 84.25l27.808 28.78m1.855-153.894L157.8 59.75m-125.45 126l27.35-27.4" fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="2"/><path d="M157.8 59.75l-16.95 17.2M58.97 60.604l17.2 17.15M59.623 158.43l16.75-17.4m61.928-1.396l18.028 17.945" fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="7"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><style>.cls-1{fill:url(#linear-gradient)}</style><linearGradient id="linear-gradient" x2="185.04" y1="66.7" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z" class="cls-1"/></g></g></svg>
<svg viewBox="0 0 185.04 133.4" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:url(#a)}</style><linearGradient id="a" x2="185.04" y1="66.7" y2="66.7" gradientUnits="userSpaceOnUse"><stop stop-color="#90cea1" offset="0"/><stop stop-color="#3cbec9" offset=".56"/><stop stop-color="#00b3e5" offset="1"/></linearGradient></defs><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 49.994 27.765"><metadata/><g stroke-width=".265" transform="translate(-80.836 -134.28)"><ellipse cx="104.39" cy="140.67" fill="#fff" rx="3.04" ry="2.572"/><path fill="#1b7d3d" d="m85.973 162.05c-0.2783-9e-3 -0.76695-0.12188-1.0859-0.25035-0.31894-0.12847-0.75831-0.50788-0.97638-0.84312-0.27168-0.41768-0.81789-4.2912-1.7352-12.306-0.7363-6.4328-1.3387-11.951-1.3387-12.263 0-0.31189 0.36978-0.9125 0.82173-1.3347 0.55888-0.52208 1.0976-0.76762 1.6843-0.76762 0.4744 0 5.7899 0.65473 11.812 1.4549 6.0223 0.80022 11.395 1.5787 11.94 1.73 0.67007 0.18601 1.1196 0.52432 1.3891 1.0455 0.23816 0.46053 0.39842 1.4812 0.39842 2.5375 0 1.6585-0.0587 1.832-0.95642 2.8256-0.52604 0.58226-1.2554 1.7118-1.6209 2.51-0.56065 1.2246-0.6448 1.7259-0.5387 3.2089 0.0692 0.96664 0.33525 2.1733 0.59131 2.6815 0.25605 0.50816 0.9453 1.4214 1.5317 2.0295 1.0049 1.0421 1.0581 1.1768 0.92626 2.3462-0.0925 0.82058-0.35123 1.4496-0.76415 1.8577-0.57063 0.56398-1.5248 0.74328-11.099 2.0855-5.7608 0.80767-10.702 1.461-10.98 1.4517zm5.8777-7.1419c0.64371 9e-3 1.2219-0.14144 1.4082-0.36594 0.17385-0.20948 0.27773-0.71409 0.23083-1.1214-0.07317-0.63544-0.23231-0.76418-1.122-0.90763-0.5702-0.0919-1.1992-0.38942-1.3979-0.66108-0.25954-0.35495-0.34007-1.2022-0.2862-3.011l0.07497-2.517h3.7011l1.7217 4.2333 1.7217 4.2333 1.4787 0.0781c0.97936 0.0517 1.5672-0.0376 1.7409-0.26458 0.14423-0.18847 1.1888-2.5136 2.3212-5.1671 1.1324-2.6534 1.9897-5.0049 1.905-5.2255-0.0964-0.25118-0.49511-0.40113-1.0666-0.40113-0.50198 0-1.0918 0.0687-1.3108 0.15275-0.21893 0.084-1.0155 1.6914-1.7701 3.5719-0.75462 1.8805-1.4697 3.4191-1.589 3.4191-0.1193 0-0.45209-0.62507-0.73953-1.3891-0.28745-0.76398-0.8376-2.2224-1.2226-3.2408s-0.81858-1.971-0.9636-2.1167-1.5382-0.32442-3.0959-0.39718l-2.8323-0.13229-0.13229-1.8521-0.13229-1.8521h-2.6458l-0.13229 1.8365c-0.13098 1.8183-0.14096 1.8388-1.0075 2.062-0.81451 0.20989-0.86954 0.29343-0.79375 1.205 0.07128 0.85741 0.18035 0.99633 0.87518 1.1147l0.79375 0.13524 0.13229 3.2208c0.11196 2.726 0.2174 3.3355 0.68624 3.967 0.30467 0.4104 0.95952 0.88829 1.4552 1.062s1.3927 0.32252 1.9933 0.33073zm12.666-11.891c0.45112 0 0.96309-0.14287 1.1377-0.3175 0.17463-0.17462 0.3175-0.64237 0.3175-1.0394 0-0.39707-0.1871-0.90903-0.41577-1.1377-0.22868-0.22868-0.69642-0.41577-1.0394-0.41577-0.34301 0-0.81075 0.18709-1.0394 0.41577s-0.41577 0.74064-0.41577 1.1377c0 0.39706 0.14287 0.86481 0.3175 1.0394 0.17462 0.17463 0.68659 0.3175 1.1377 0.3175z" transform="translate(-8.3333e-8)"/><path fill="#fff" d="m114.46 154.52c-3.0274 0.0544-3.7379-0.0118-4.6609-0.43422-0.59897-0.27412-1.3995-0.90545-1.779-1.403-0.37946-0.49751-0.83257-1.5225-1.0069-2.2778-0.24624-1.0668-0.24624-1.6796 0-2.7464 0.17434-0.75526 0.61766-1.7674 0.98516-2.2492 0.3675-0.48183 1.049-1.0738 1.5144-1.3155 0.48523-0.25197 1.8056-0.5058 3.0952-0.59503l2.249-0.15561 0.13229-1.8521 0.1323-1.8521h2.9104v14.817zm-0.92604-2.3736 1.3229-0.0718v-5.8208l-1.6062-0.0775c-1.4422-0.0696-1.6785-5e-3 -2.3151 0.6314-0.38991 0.38991-0.72779 1.0201-0.75085 1.4004-0.023 0.38034-0.0238 0.92965-2e-3 1.2207 0.0222 0.29104 0.10377 0.80075 0.18131 1.1327 0.0775 0.33193 0.52474 0.8405 0.99377 1.1302 0.615 0.37978 1.2217 0.50661 2.1757 0.45481zm9.7151 2.4359c-2.5936 4e-3 -3.5477-0.0819-3.6425-0.32887-0.0706-0.18407-0.0967-3.5476-0.0578-7.4745l0.0706-7.1398h2.9104l0.13229 1.8521 0.13229 1.8521 2.2687 0.15541c1.6342 0.11195 2.5591 0.31887 3.3073 0.73989 0.57124 0.32147 1.3303 1.0254 1.6868 1.5644 0.45872 0.69347 0.67992 1.4556 0.75677 2.6074 0.0597 0.89511-0.0338 2.059-0.20789 2.5864-0.17406 0.52742-0.62874 1.33-1.0104 1.7836-0.38165 0.45358-1.1751 1.0432-1.7632 1.3104-0.86286 0.39194-1.7476 0.48683-4.5833 0.49152zm0.84267-2.3754c0.64994 0 1.4871-0.1161 1.8603-0.25799 0.3732-0.14189 0.92337-0.64791 1.2226-1.1245 0.41645-0.66331 0.5175-1.173 0.43097-2.1738-0.0877-1.0148-0.27505-1.4467-0.83705-1.9301-0.61948-0.53286-0.95861-0.6115-2.3491-0.54474l-1.6251 0.078-0.0763 2.627c-0.042 1.4448-0.0159 2.7843 0.0578 2.9766 0.0835 0.21749 0.5806 0.34956 1.3158 0.34956z" transform="translate(-8.3333e-8)"/></g></svg>
<svg version="1.1" viewBox="0 0 49.994 27.765" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-80.836 -134.28)" stroke-width=".265"><ellipse cx="104.39" cy="140.67" rx="3.04" ry="2.572" fill="#fff"/><path transform="translate(-8.3333e-8)" d="m85.973 162.05c-0.2783-9e-3 -0.76695-0.12188-1.0859-0.25035-0.31894-0.12847-0.75831-0.50788-0.97638-0.84312-0.27168-0.41768-0.81789-4.2912-1.7352-12.306-0.7363-6.4328-1.3387-11.951-1.3387-12.263 0-0.31189 0.36978-0.9125 0.82173-1.3347 0.55888-0.52208 1.0976-0.76762 1.6843-0.76762 0.4744 0 5.7899 0.65473 11.812 1.4549 6.0223 0.80022 11.395 1.5787 11.94 1.73 0.67007 0.18601 1.1196 0.52432 1.3891 1.0455 0.23816 0.46053 0.39842 1.4812 0.39842 2.5375 0 1.6585-0.0587 1.832-0.95642 2.8256-0.52604 0.58226-1.2554 1.7118-1.6209 2.51-0.56065 1.2246-0.6448 1.7259-0.5387 3.2089 0.0692 0.96664 0.33525 2.1733 0.59131 2.6815 0.25605 0.50816 0.9453 1.4214 1.5317 2.0295 1.0049 1.0421 1.0581 1.1768 0.92626 2.3462-0.0925 0.82058-0.35123 1.4496-0.76415 1.8577-0.57063 0.56398-1.5248 0.74328-11.099 2.0855-5.7608 0.80767-10.702 1.461-10.98 1.4517zm5.8777-7.1419c0.64371 9e-3 1.2219-0.14144 1.4082-0.36594 0.17385-0.20948 0.27773-0.71409 0.23083-1.1214-0.07317-0.63544-0.23231-0.76418-1.122-0.90763-0.5702-0.0919-1.1992-0.38942-1.3979-0.66108-0.25954-0.35495-0.34007-1.2022-0.2862-3.011l0.07497-2.517h3.7011l1.7217 4.2333 1.7217 4.2333 1.4787 0.0781c0.97936 0.0517 1.5672-0.0376 1.7409-0.26458 0.14423-0.18847 1.1888-2.5136 2.3212-5.1671 1.1324-2.6534 1.9897-5.0049 1.905-5.2255-0.0964-0.25118-0.49511-0.40113-1.0666-0.40113-0.50198 0-1.0918 0.0687-1.3108 0.15275-0.21893 0.084-1.0155 1.6914-1.7701 3.5719-0.75462 1.8805-1.4697 3.4191-1.589 3.4191s-0.45209-0.62507-0.73953-1.3891c-0.28745-0.76398-0.8376-2.2224-1.2226-3.2408s-0.81858-1.971-0.9636-2.1167-1.5382-0.32442-3.0959-0.39718l-2.8323-0.13229-0.13229-1.8521-0.13229-1.8521h-2.6458l-0.13229 1.8365c-0.13098 1.8183-0.14096 1.8388-1.0075 2.062-0.81451 0.20989-0.86954 0.29343-0.79375 1.205 0.07128 0.85741 0.18035 0.99633 0.87518 1.1147l0.79375 0.13524 0.13229 3.2208c0.11196 2.726 0.2174 3.3355 0.68624 3.967 0.30467 0.4104 0.95952 0.88829 1.4552 1.062s1.3927 0.32252 1.9933 0.33073zm12.666-11.891c0.45112 0 0.96309-0.14287 1.1377-0.3175 0.17463-0.17462 0.3175-0.64237 0.3175-1.0394 0-0.39707-0.1871-0.90903-0.41577-1.1377-0.22868-0.22868-0.69642-0.41577-1.0394-0.41577-0.34301 0-0.81075 0.18709-1.0394 0.41577s-0.41577 0.74064-0.41577 1.1377 0.14287 0.86481 0.3175 1.0394c0.17462 0.17463 0.68659 0.3175 1.1377 0.3175z" fill="#1b7d3d"/><path transform="translate(-8.3333e-8)" d="m114.46 154.52c-3.0274 0.0544-3.7379-0.0118-4.6609-0.43422-0.59897-0.27412-1.3995-0.90545-1.779-1.403-0.37946-0.49751-0.83257-1.5225-1.0069-2.2778-0.24624-1.0668-0.24624-1.6796 0-2.7464 0.17434-0.75526 0.61766-1.7674 0.98516-2.2492 0.3675-0.48183 1.049-1.0738 1.5144-1.3155 0.48523-0.25197 1.8056-0.5058 3.0952-0.59503l2.249-0.15561 0.13229-1.8521 0.1323-1.8521h2.9104v14.817zm-0.92604-2.3736 1.3229-0.0718v-5.8208l-1.6062-0.0775c-1.4422-0.0696-1.6785-5e-3 -2.3151 0.6314-0.38991 0.38991-0.72779 1.0201-0.75085 1.4004-0.023 0.38034-0.0238 0.92965-2e-3 1.2207 0.0222 0.29104 0.10377 0.80075 0.18131 1.1327 0.0775 0.33193 0.52474 0.8405 0.99377 1.1302 0.615 0.37978 1.2217 0.50661 2.1757 0.45481zm9.7151 2.4359c-2.5936 4e-3 -3.5477-0.0819-3.6425-0.32887-0.0706-0.18407-0.0967-3.5476-0.0578-7.4745l0.0706-7.1398h2.9104l0.13229 1.8521 0.13229 1.8521 2.2687 0.15541c1.6342 0.11195 2.5591 0.31887 3.3073 0.73989 0.57124 0.32147 1.3303 1.0254 1.6868 1.5644 0.45872 0.69347 0.67992 1.4556 0.75677 2.6074 0.0597 0.89511-0.0338 2.059-0.20789 2.5864-0.17406 0.52742-0.62874 1.33-1.0104 1.7836-0.38165 0.45358-1.1751 1.0432-1.7632 1.3104-0.86286 0.39194-1.7476 0.48683-4.5833 0.49152zm0.84267-2.3754c0.64994 0 1.4871-0.1161 1.8603-0.25799s0.92337-0.64791 1.2226-1.1245c0.41645-0.66331 0.5175-1.173 0.43097-2.1738-0.0877-1.0148-0.27505-1.4467-0.83705-1.9301-0.61948-0.53286-0.95861-0.6115-2.3491-0.54474l-1.6251 0.078-0.0763 2.627c-0.042 1.4448-0.0159 2.7843 0.0578 2.9766 0.0835 0.21749 0.5806 0.34956 1.3158 0.34956z" fill="#fff"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" stroke="currentColor" viewBox="-2 -2 42 42"><g fill="none" fillRule="evenodd"><g stroke-width="6" transform="translate(1 1)"><circle cx="18" cy="18" r="18" stroke-opacity=".5"/><path d="M36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" dur="1s" from="0 18 18" repeatCount="indefinite" to="360 18 18" type="rotate"/></path></g></g></svg>
<svg stroke="currentColor" viewBox="-2 -2 42 42" xmlns="http://www.w3.org/2000/svg"><g transform="translate(1 1)" stroke-width="6" fill="none"><circle cx="18" cy="18" r="18" stroke-opacity=".5"/><path d="m36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" dur="1s" from="0 18 18" repeatCount="indefinite" to="360 18 18" type="rotate"/></path></g></svg>

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><g data-name="Layer 2"><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)" data-name="Layer 1"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none" viewBox="0 0 44 44"><path fill="#667EEA" d="M43.5 0H0L43.5 43.5V0Z"/><path fill="#fff" fill-rule="evenodd" d="M31 20.2C32.9096 20.2 34.7409 19.4414 36.0912 18.0912C37.4414 16.7409 38.2 14.9095 38.2 13C38.2 11.0904 37.4414 9.25908 36.0912 7.90882C34.7409 6.55856 32.9096 5.79999 31 5.79999C29.0904 5.79999 27.2591 6.55856 25.9088 7.90882C24.5586 9.25908 23.8 11.0904 23.8 13C23.8 14.9095 24.5586 16.7409 25.9088 18.0912C27.2591 19.4414 29.0904 20.2 31 20.2ZM31.9 9.39999C31.9 9.16129 31.8052 8.93237 31.6364 8.76359C31.4676 8.59481 31.2387 8.49999 31 8.49999C30.7613 8.49999 30.5324 8.59481 30.3636 8.76359C30.1948 8.93237 30.1 9.16129 30.1 9.39999V13C30.1 13.2387 30.1949 13.4675 30.3637 13.6363L32.9089 16.1824C32.9925 16.266 33.0918 16.3323 33.201 16.3776C33.3103 16.4228 33.4274 16.4461 33.5456 16.4461C33.6639 16.4461 33.781 16.4228 33.8903 16.3776C33.9995 16.3323 34.0988 16.266 34.1824 16.1824C34.266 16.0988 34.3323 15.9995 34.3776 15.8902C34.4229 15.781 34.4461 15.6639 34.4461 15.5456C34.4461 15.4274 34.4229 15.3103 34.3776 15.201C34.3323 15.0918 34.266 14.9925 34.1824 14.9089L31.9 12.6274V9.39999Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor" class="w-6 h-6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>

Before

Width:  |  Height:  |  Size: 284 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" class="w-6 h-6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>

Before

Width:  |  Height:  |  Size: 260 B

View File

@@ -1,15 +1,15 @@
import { DownloadIcon, DuplicateIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { uniq } from 'lodash';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useContext, useState } from 'react';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { MediaStatus } from '../../../server/constants/media';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { Collection } from '../../../server/models/Collection';
import { LanguageContext } from '../../context/LanguageContext';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@@ -47,14 +47,13 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
const router = useRouter();
const settings = useSettings();
const { addToast } = useToasts();
const { locale } = useContext(LanguageContext);
const { hasPermission } = useUser();
const [requestModal, setRequestModal] = useState(false);
const [isRequesting, setRequesting] = useState(false);
const [is4k, setIs4k] = useState(false);
const { data, error, revalidate } = useSWR<Collection>(
`/api/v1/collection/${router.query.collectionId}?language=${locale}`,
`/api/v1/collection/${router.query.collectionId}`,
{
initialData: collection,
revalidateOnMount: true,
@@ -62,7 +61,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
);
const { data: genres } = useSWR<{ id: number; name: string }[]>(
`/api/v1/genres/movie?language=${locale}`
`/api/v1/genres/movie`
);
if (!data && !error) {
@@ -109,11 +108,18 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}
const hasRequestable =
hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], {
type: 'or',
}) &&
data.parts.filter(
(part) => !part.mediaInfo || part.mediaInfo.status === MediaStatus.UNKNOWN
).length > 0;
const hasRequestable4k =
settings.currentSettings.movie4kEnabled &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], {
type: 'or',
}) &&
data.parts.filter(
(part) =>
!part.mediaInfo || part.mediaInfo.status4k === MediaStatus.UNKNOWN
@@ -248,22 +254,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
title={intl.formatMessage(
is4k ? messages.requestcollection4k : messages.requestcollection
)}
iconSvg={
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
}
iconSvg={<DuplicateIcon />}
>
<p>
{intl.formatMessage(
@@ -339,81 +330,42 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
</span>
</div>
<div className="media-actions">
{hasPermission(Permission.REQUEST) &&
(hasRequestable ||
(settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
hasRequestable4k)) && (
<ButtonWithDropdown
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(!hasRequestable);
}}
text={
<>
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span>
{intl.formatMessage(
hasRequestable
? messages.requestcollection
: messages.requestcollection4k
)}
</span>
</>
}
>
{settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
hasRequestable &&
hasRequestable4k && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<svg
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
)}
{(hasRequestable || hasRequestable4k) && (
<ButtonWithDropdown
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(!hasRequestable);
}}
text={
<>
<DownloadIcon />
<span>
{intl.formatMessage(
hasRequestable
? messages.requestcollection
: messages.requestcollection4k
)}
</span>
</>
}
>
{hasRequestable && hasRequestable4k && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<DownloadIcon />
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
)}
</div>
</div>
{data.overview && (

View File

@@ -1,3 +1,8 @@
import {
ExclamationIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/solid';
import React from 'react';
interface AlertProps {
@@ -10,21 +15,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-yellow-600',
titleColor: 'text-yellow-200',
textColor: 'text-yellow-300',
svg: (
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
),
svg: <ExclamationIcon className="w-5 h-5" />,
};
switch (type) {
@@ -33,22 +24,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-indigo-600',
titleColor: 'text-indigo-200',
textColor: 'text-indigo-300',
svg: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
svg: <InformationCircleIcon className="w-5 h-5" />,
};
break;
case 'error':
@@ -56,22 +32,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
bgColor: 'bg-red-600',
titleColor: 'text-red-200',
textColor: 'text-red-300',
svg: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
svg: <XCircleIcon className="w-5 h-5" />,
};
break;
}

View File

@@ -81,16 +81,14 @@ function Button<P extends ElementTypes = 'button'>(
switch (buttonSize) {
case 'sm':
buttonStyle.push('px-2.5 py-1.5 text-xs');
break;
case 'md':
buttonStyle.push('px-4 py-2 text-sm');
buttonStyle.push('px-2.5 py-1.5 text-xs button-sm');
break;
case 'lg':
buttonStyle.push('px-6 py-3 text-base');
buttonStyle.push('px-6 py-3 text-base button-lg');
break;
case 'md':
default:
buttonStyle.push('px-4 py-2 text-sm');
buttonStyle.push('px-4 py-2 text-sm button-md');
}
buttonStyle.push(className ?? '');

View File

@@ -1,13 +1,14 @@
import { ChevronDownIcon } from '@heroicons/react/solid';
import React, {
useState,
useRef,
AnchorHTMLAttributes,
ReactNode,
ButtonHTMLAttributes,
ReactNode,
useRef,
useState,
} from 'react';
import useClickOutside from '../../../hooks/useClickOutside';
import Transition from '../../Transition';
import { withProperties } from '../../../utils/typeHelpers';
import Transition from '../../Transition';
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
buttonType?: 'primary' | 'ghost';
@@ -18,16 +19,16 @@ const DropdownItem: React.FC<DropdownItemProps> = ({
buttonType = 'primary',
...props
}) => {
let styleClass = '';
let styleClass = 'button-md text-white';
switch (buttonType) {
case 'ghost':
styleClass =
'text-white bg-gray-700 hover:bg-gray-600 hover:text-white focus:border-gray-500 focus:text-white';
styleClass +=
' bg-gray-700 hover:bg-gray-600 focus:border-gray-500 focus:text-white';
break;
default:
styleClass =
'text-white bg-indigo-600 hover:bg-indigo-500 hover:text-white focus:border-indigo-700 focus:text-white';
styleClass +=
' bg-indigo-600 hover:bg-indigo-500 focus:border-indigo-700 focus:text-white';
}
return (
<a
@@ -59,9 +60,9 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
useClickOutside(buttonRef, () => setIsOpen(false));
const styleClasses = {
mainButtonClasses: 'text-white border',
dropdownSideButtonClasses: 'border',
dropdownClasses: '',
mainButtonClasses: 'button-md text-white border',
dropdownSideButtonClasses: 'button-md border',
dropdownClasses: 'button-md',
};
switch (buttonType) {
@@ -69,14 +70,14 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
styleClasses.mainButtonClasses +=
' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
styleClasses.dropdownSideButtonClasses = styleClasses.mainButtonClasses;
styleClasses.dropdownClasses = 'bg-gray-700';
styleClasses.dropdownClasses += ' bg-gray-700';
break;
default:
styleClasses.mainButtonClasses +=
' bg-indigo-600 border-indigo-600 hover:bg-indigo-500 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue';
styleClasses.dropdownSideButtonClasses +=
' bg-indigo-700 border-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue';
styleClasses.dropdownClasses = 'bg-indigo-600';
styleClasses.dropdownClasses += ' bg-indigo-600';
}
return (
@@ -99,22 +100,7 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
aria-label="Expand"
onClick={() => setIsOpen((state) => !state)}
>
{dropdownIcon ? (
dropdownIcon
) : (
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
)}
{dropdownIcon ? dropdownIcon : <ChevronDownIcon />}
</button>
<Transition
show={isOpen}

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