Compare commits

..

87 Commits

Author SHA1 Message Date
TheCatLady
30275450cc fix: use new SettingsBadge component 2022-08-28 23:21:59 -07:00
TheCatLady
feffccaca8 feat: add 'Hide Requested Media' setting 2022-08-28 23:21:58 -07:00
Ryan Cohen
889caaa733 refactor: modal redesign and fix for transitions (#2987) 2022-08-29 14:56:04 +09:00
Brandon Cohen
4d56320870 fix: settings log modal when closing (#2985) 2022-08-26 21:38:13 +09:00
Ryan Cohen
1a0053221b fix: clicking outside modal closes modal again (#2984) 2022-08-25 13:28:44 +09:00
Brandon Cohen
b925857dfa fix: username will not show undefined on cancel or delete (#2982) 2022-08-25 01:45:05 +00:00
Ryan Cohen
c4aa08f5f0 fix: correct spacing on season header badges (#2983) 2022-08-25 10:14:16 +09:00
TheCatLady
5d73bc2238 fix: check perms to view watchlist slider on user profile (#2980) 2022-08-25 02:43:25 +04:00
Brandon Cohen
095048d94a fix: issues and login page still had incorrect animations (#2979)
Issues dropdown and the login page transition including the language picker were set as fragments
2022-08-24 13:10:57 -07:00
Brandon Cohen
98028bf2f4 fix: transition animation (#2974)
switched to using headlessui transition instead of react-css-transition due to new version breaking the
animation
2022-08-24 10:18:09 -07:00
renovate[bot]
baf1ea95a3 fix(deps): pin dependency @formatjs/intl-utils to 3.8.4 (#2975) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-24 21:47:39 +09:00
Ryan Cohen
23409e6f2f fix: correct safe margin for slideover (#2977) 2022-08-24 20:57:30 +09:00
TheCatLady
dd28200040 fix: watch data not required to show Tautulli button (#2976) 2022-08-24 15:26:25 +09:00
Ryan Cohen
22360f3b87 refactor: slideover redesign (#2973) 2022-08-24 15:00:04 +09:00
TheCatLady
815d709bcf feat(frontend): a few more tooltips (#2972)
* feat(frontend): a few more tooltips

* feat: add tooltips to status badges
2022-08-24 13:59:26 +09:00
Ryan Cohen
8a2acb7f2b feat: season/episode list on series details (#2967) 2022-08-24 04:09:10 +00:00
Ryan Cohen
67f3a3829e feat: improved user dropdown (#2969) 2022-08-24 01:49:25 +00:00
renovate[bot]
f5e5016ca5 chore(deps): update github actions (major) (#2947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2022-08-23 18:32:49 -04:00
Ryan Cohen
6e60a275c7 fix: use fallbackData to prepare user data during SSR (#2968) 2022-08-23 17:36:02 +09:00
Ryan Cohen
3b2633812b fix: use image.tmdb.org for setup/login backdrop images (#2966) 2022-08-23 14:25:20 +09:00
Ryan Cohen
507227aa49 feat: restore option to cache and optimize images locally (#2964) 2022-08-23 01:46:52 +09:00
Weblate (bot)
29ab178fb0 feat(lang): translations update from Hosted Weblate (#2958)
* feat(lang): translated using Weblate (French)

Currently translated at 99.4% (1067 of 1073 strings)

feat(lang): translated using Weblate (French)

Currently translated at 97.1% (1040 of 1071 strings)

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 (Portuguese (Brazil))

Currently translated at 99.2% (1063 of 1071 strings)

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/pt_BR/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 99.8% (1083 of 1085 strings)

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

Currently translated at 99.6% (1081 of 1085 strings)

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

Currently translated at 100.0% (1073 of 1073 strings)

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

Currently translated at 99.4% (1067 of 1073 strings)

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

Currently translated at 99.5% (1066 of 1071 strings)

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

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

Currently translated at 100.0% (1073 of 1073 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (1073 of 1073 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.6% (1067 of 1071 strings)

Co-authored-by: Ben <ben.david.wallner@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Ben <ben.david.wallner@gmail.com>
2022-08-22 08:17:31 -07:00
TheCatLady
f5e6b620c1 fix(lang): manage movie -> manage series (#2963) 2022-08-22 06:05:10 +00:00
TheCatLady
0839718806 feat: view other users' watchlists (#2959)
* feat: view other users' watchlists

* test: add cypress tests

* feat(lang): translation keys

* refactor: yarn format

* fix: manage requests perm is parent of view watchlist perm
2022-08-22 05:50:27 +00:00
TheCatLady
950b1712b7 feat(frontend): add more tooltips (#2961)
* feat(frontend): add more tooltips

* fix: remove styling from Tooltip

* refactor: tooltip expects a single child
2022-08-22 14:37:22 +09:00
renovate[bot]
43a9067976 chore(deps): pin dependencies (#2962) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-22 13:36:51 +09:00
Ryan Cohen
c6a133d4e5 refactor: absolute imports with path alias (#2960) [skip ci] 2022-08-22 11:02:46 +09:00
Ryan Cohen
4b855b8114 style: fix organize-imports to work with tailwindcss prettier plugin (#2957) [skip ci] 2022-08-21 10:40:12 +00:00
TheCatLady
6c0fd40877 feat(notif): auto-request notif type (#2956) 2022-08-21 10:26:19 +00:00
Ryan Cohen
301f2bf7ab feat: plex watchlist sync integration (#2885) 2022-08-21 16:33:49 +09:00
allcontributors[bot]
7943e0c339 docs: add miknii as a contributor for translation (#2955) [skip ci]
* docs: update README.md

* docs: update .all-contributorsrc

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-08-21 01:25:12 +00:00
allcontributors[bot]
6ce0aa5b10 docs: add byakurau as a contributor for translation (#2954) [skip ci]
* docs: update README.md

* docs: update .all-contributorsrc

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-08-21 01:14:50 +00:00
Weblate (bot)
a0301e2d83 feat(lang): translations update from Hosted Weblate (#2915)
* feat(lang): translated using Weblate (Greek)

Currently translated at 74.1% (775 of 1045 strings)

feat(lang): translated using Weblate (Greek)

Currently translated at 74.0% (774 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 62.9% (658 of 1045 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 62.9% (658 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Albanian)

Currently translated at 99.2% (1037 of 1045 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 99.1% (1036 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 99.2% (1037 of 1045 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.1% (1036 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Spanish)

Currently translated at 92.4% (966 of 1045 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 92.3% (965 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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

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

Currently translated at 99.3% (1038 of 1045 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 99.2% (1037 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Czech)

Currently translated at 94.6% (989 of 1045 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 94.5% (988 of 1045 strings)

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

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

Currently translated at 100.0% (1048 of 1048 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1045 of 1045 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 99.2% (1037 of 1045 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 99.1% (1036 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1045 of 1045 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 99.9% (1044 of 1045 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 99.2% (1037 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mikael Nilsson <mikni@proton.me>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Russian)

Currently translated at 93.7% (980 of 1045 strings)

feat(lang): translated using Weblate (Russian)

Currently translated at 93.6% (979 of 1045 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
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 (Portuguese (Portugal))

Currently translated at 77.7% (813 of 1045 strings)

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

Currently translated at 77.7% (812 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Danish)

Currently translated at 98.7% (1032 of 1045 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 98.6% (1031 of 1045 strings)

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

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

Currently translated at 98.2% (1039 of 1058 strings)

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

Currently translated at 98.1% (1038 of 1058 strings)

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

Currently translated at 99.3% (1038 of 1045 strings)

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

Currently translated at 99.3% (1038 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 94.8% (991 of 1045 strings)

feat(lang): translated using Weblate (Hungarian)

Currently translated at 94.7% (990 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 99.3% (1038 of 1045 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 99.2% (1037 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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

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

Currently translated at 100.0% (1058 of 1058 strings)

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

Currently translated at 100.0% (1058 of 1058 strings)

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

Currently translated at 100.0% (1050 of 1050 strings)

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

Currently translated at 99.9% (1047 of 1048 strings)

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

Currently translated at 100.0% (1045 of 1045 strings)

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

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

Currently translated at 99.3% (1038 of 1045 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 99.2% (1037 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Chinese (Traditional))

Currently translated at 100.0% (1058 of 1058 strings)

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

Currently translated at 100.0% (1058 of 1058 strings)

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

Currently translated at 100.0% (1050 of 1050 strings)

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

Currently translated at 100.0% (1048 of 1048 strings)

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

Currently translated at 100.0% (1045 of 1045 strings)

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

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

Currently translated at 99.1% (1036 of 1045 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.0% (1035 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 (Norwegian Bokmål)

Currently translated at 98.9% (1047 of 1058 strings)

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

Currently translated at 97.9% (1036 of 1058 strings)

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

Currently translated at 99.3% (1038 of 1045 strings)

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

Currently translated at 99.2% (1037 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: exentler <gurandsrud@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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Mikael Nilsson <mikni@proton.me>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-08-20 16:47:15 -04:00
TheCatLady
9021696cf0 fix(lang): correct capitalization of 'TMDB' (#2953) 2022-08-20 12:23:16 +09:00
TheCatLady
9bc1f89777 fix(frontend): better request/media cards for items without valid TMDb IDs (#2181) 2022-08-20 11:21:53 +09:00
TheCatLady
a12697b061 feat(perms): add new permission for viewing recently added media (#2129)
* feat(perms): add new permission for viewing recently added media

* test: update login test to check for Trending instead of Recently Added

* fix: avoid conflict with new watchlist perms
2022-08-19 20:32:24 +00:00
renovate[bot]
5247f14968 fix(deps): pin dependency react-popper-tooltip to 4.4.2 (#2952) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-19 21:19:14 +09:00
Danshil Kokil Mungur
fd0ff4bd5f chore: remove empty lines from imports section (#2951) [skip ci] 2022-08-19 10:59:27 +00:00
Ryan Cohen
16545eec22 feat: tooltip foundation (#2950)
* feat: add foundation for tooltips

* fix: add lang

* refactor: remove React import where no longer necessary
2022-08-19 19:35:50 +09:00
Brandon Cohen
36d17fed6e feat: user delete modal shows username and requires confirmation (#2779)
The delete user modal will now show the user that is being deleted and the delete button will now
ask you to confirm deletion similar to the delete request button.
2022-08-19 10:21:29 +00:00
TheCatLady
ac34328074 ci: don't lint/test in snap publish workflow (#2948) [skip ci] 2022-08-19 14:38:11 +09:00
Danshil Kokil Mungur
91e0928aa0 feat(ui): revalidate requests slider on discover page (#2818) 2022-08-18 17:07:23 +00:00
renovate[bot]
f836cadd23 chore(deps): update node.js to v16.17 (#2941)
* chore(deps): update node.js to v16.17

* chore(deps): also bump node.js in snapcraft.yaml

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2022-08-18 20:44:18 +09:00
Ryan Cohen
f4910a1483 build(snap): re-enable snap workflow (#2945)
* build(snap): try re-enabling snap workflow

* build: try adding a safe directory exception?

* build(snap): disable cypress binary install

* build(snap): add yarnrc to force frozen lockfile and timeout arguments

* build(snap): add back in release snap workflow

* build(snap): revert back to only running on dev branch
2022-08-18 19:16:24 +09:00
renovate[bot]
103c4ca49c fix(deps): pin dependencies (#2946) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-18 18:38:52 +09:00
TheCatLady
c143c0b8d2 fix: better ordering of RequestButton options & properly handle failed requests (#2944)
* fix: better ordering of RequestButton options & properly handle failed requests

* fix: appease prettier
2022-08-18 08:30:40 +00:00
Ryan Cohen
e5d8c93ab8 chore(deps): update react to 18 (#2943) 2022-08-18 17:05:58 +09:00
Ryan Cohen
72d7a3477f chore(deps): update dependencies (#2942) 2022-08-18 13:49:07 +09:00
TheCatLady
808fabba9a chore(deps): update renovate config to group node container deps (#2939) [skip ci] 2022-08-18 11:54:08 +09:00
Ryan Cohen
7a5fab35ff build(snap): add architectures to snapcraft.yaml (#2938) [skip ci] 2022-08-18 10:57:40 +09:00
renovate[bot]
17ac5069e5 chore(deps): update github-actions (#2933) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-17 15:04:19 -04:00
renovate[bot]
cfab63c0ca chore(deps): pin dependencies (#2925) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-17 11:52:16 +00:00
Danshil Kokil Mungur
0fa84eae8d build(deps): bump dependencies (#2796) 2022-08-17 20:43:35 +09:00
renovate[bot]
821bb79d83 chore(deps): configure renovate [skip ci]
* chore(deps): add renovate.json

* chore(deps): update renovate.json to add grouping

* chore(deps): update renovate.json to disable major docker updates

* chore(deps): remove dependabot config

* chore(deps): bundle github-action updates

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: sct <ryan@sct.dev>
Co-authored-by: Ryan Cohen <r-cohen@mercari.com>
2022-08-17 18:04:16 +09:00
TheCatLady
233035dbd7 build(docker): downgrade to node 16.16 (#2921) 2022-08-17 13:10:11 +09:00
TheCatLady
114943ae2c build(docker): skip cypress install & bump node version (#2920) 2022-08-17 12:46:03 +09:00
TheCatLady
a6f7b19693 chore(vscode): remove deprecated npm extension from recommendations (#2919) [skip ci] 2022-08-17 09:16:47 +09:00
TheCatLady
3db3044210 feat(lang): add Arabic and Lithuanian display languages (#2916) 2022-08-17 09:15:30 +09:00
allcontributors[bot]
1fcfe93b58 docs: add PovilasID as a contributor for translation (#2918) [skip ci]
* docs: update README.md

* docs: update .all-contributorsrc

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-08-16 22:12:47 +00:00
allcontributors[bot]
6cb456cb69 docs: add Fhd-pro as a contributor for translation (#2917) [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>
2022-08-16 22:06:08 +00:00
Weblate (bot)
e939dc678e feat(lang): translations update from Hosted Weblate (#2659)
* 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.

feat(lang): translated using Weblate (Japanese)

Currently translated at 63.2% (660 of 1043 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 62.4% (651 of 1043 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 61.7% (644 of 1043 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 50.3% (525 of 1043 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Serbian)

Currently translated at 58.8% (614 of 1043 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Albanian)

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Denis Çerri <deniscerri3@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sq/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rémi Guerrero <remidu34070@hotmail.fr>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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

* 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 (Romanian)

Currently translated at 3.6% (38 of 1042 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 0.8% (9 of 1042 strings)

feat(lang): added translation using Weblate (Romanian)

Co-authored-by: Constantin <bimasakti.ro@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ro/
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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1043 of 1043 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1043 of 1043 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Maite Guix <maite.guix@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/ca/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Czech)

Currently translated at 95.3% (994 of 1043 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 91.4% (954 of 1043 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 91.4% (954 of 1043 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 91.3% (953 of 1043 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 91.3% (953 of 1043 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 50.3% (525 of 1043 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Pollyi <weblate.ntxx4@simplelogin.co>
Co-authored-by: Smexhy <smexhy@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/cs/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Danish)

Currently translated at 99.5% (1037 of 1042 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 97.6% (1018 of 1042 strings)

Co-authored-by: Daniel Maslygan <danielmaslygan@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/da/
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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (1043 of 1043 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
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.

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

Currently translated at 100.0% (1043 of 1043 strings)

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

Currently translated at 100.0% (1043 of 1043 strings)

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

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Miniers <m@lk.mk>
Co-authored-by: yzqzss <yzqzss@othing.xyz>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1043 of 1043 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1043 of 1043 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 75.6% (788 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 62.7% (654 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 54.6% (569 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 52.9% (552 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 49.6% (517 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 47.0% (490 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 42.5% (443 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 38.1% (398 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 36.8% (384 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 32.7% (341 of 1042 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 0.1% (1 of 1042 strings)

feat(lang): added translation using Weblate (Arabic)

Co-authored-by: Fhd-pro <juve.11@msn.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ar/
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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Hungarian)

Currently translated at 95.3% (995 of 1043 strings)

feat(lang): translated using Weblate (Hungarian)

Currently translated at 88.3% (921 of 1043 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sir Hóksalot <haaax2000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1043 of 1043 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

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

Currently translated at 99.6% (1041 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 100.0% (1043 of 1043 strings)

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

Currently translated at 100.0% (1043 of 1043 strings)

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.

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (1043 of 1043 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simone <simoneungaro@hotmail.it>
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 (Chinese (Traditional))

Currently translated at 100.0% (1045 of 1045 strings)

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

Currently translated at 99.9% (1044 of 1045 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 100.0% (1043 of 1043 strings)

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

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: 주서현 <adan.89lion@gmail.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.

feat(lang): translated using Weblate (German)

Currently translated at 99.8% (1041 of 1043 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Samuel Nitzsche <samuel.nitzsche+github@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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 100.0% (1043 of 1043 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: exentler <gurandsrud@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.

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 52.2% (545 of 1043 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 43.8% (457 of 1043 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 31.8% (332 of 1043 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 28.2% (295 of 1043 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 25.7% (269 of 1043 strings)

feat(lang): added translation using Weblate (Lithuanian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: PovilasID <povilas.sidaravicius@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/lt/
Translation: Overseerr/Overseerr Frontend

* feat(lang): added translation using Weblate (Hindi)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sct <sctsnipe@gmail.com>

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

Currently translated at 0.2% (3 of 1043 strings)

feat(lang): translated using Weblate (Hebrew)

Currently translated at 0.1% (2 of 1043 strings)

feat(lang): added translation using Weblate (Hebrew)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: tallevi1000 <tal_levi1000@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/he/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Milan Smudja <smudja@gmail.com>
Co-authored-by: Denis Çerri <deniscerri3@gmail.com>
Co-authored-by: Rémi Guerrero <remidu34070@hotmail.fr>
Co-authored-by: Constantin <bimasakti.ro@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: Maite Guix <maite.guix@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Pollyi <weblate.ntxx4@simplelogin.co>
Co-authored-by: Smexhy <smexhy@gmail.com>
Co-authored-by: Daniel Maslygan <danielmaslygan@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Miniers <m@lk.mk>
Co-authored-by: yzqzss <yzqzss@othing.xyz>
Co-authored-by: Fhd-pro <juve.11@msn.com>
Co-authored-by: Sir Hóksalot <haaax2000@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Co-authored-by: Samuel Nitzsche <samuel.nitzsche+github@gmail.com>
Co-authored-by: exentler <gurandsrud@gmail.com>
Co-authored-by: PovilasID <povilas.sidaravicius@gmail.com>
Co-authored-by: tallevi1000 <tal_levi1000@hotmail.com>
2022-08-16 17:28:12 -04:00
TheCatLady
f3e56da3b7 feat: show alert/prompt when settings changes require restart (#2401)
* fix: correct 'StatusChecker' typo

* feat: add restart required check to StatusChecker

* fix(perms): remove MANAGE_SETTINGS permission

* fix: allow alert to be dismissed

* fix(lang): add missing string in SettingsServices

* fix(frontend): fix modal icon border

* fix(frontend): un-dismiss alert if setting reverted not require server restart

* fix(backend): restart flag only needs to track main settings

* fix: rebase issue

* refactor: appease Prettier

* refactor: swap settings badge order

* fix: type import for MainSettings

* test: add cypress test for restart prompt
2022-08-16 16:58:11 +00:00
allcontributors[bot]
70dc4c4b3b docs: add Gylesie as a contributor for code (#2912) [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>
2022-08-16 16:39:35 +00:00
TheCatLady
6428b8d419 fix: log level value should not be case sensitive (#2913) 2022-08-17 01:33:13 +09:00
Gylesie
004e1bb17e fix(api): lookup shows using english title only (#2911)
Fixes most of the time irrelevant lookup list when using localized TMDB metadata. #2801
2022-08-16 11:56:46 -04:00
Ryan Cohen
ebd22ffcea test: add waits in user test (#2907) [skip ci] 2022-08-15 19:50:42 +09:00
Ryan Cohen
22ec058431 test: add cypress foundation (#2903) [skip ci] 2022-08-15 08:34:38 +09:00
allcontributors[bot]
db898db9f2 docs: add TheMeanCanEHdian as a contributor for code (#2889) [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>
2022-08-04 16:05:30 +09:00
TheMeanCanEHdian
b33956e6b8 feat: add 20th Century Studios to Studio Slider (#2288)
* Add 20th Century Studios to Studio Sliders

* Remove 20th Century Fox from Studio Sliders

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-08-03 04:13:21 +00:00
Ryan Cohen
f5864b49de refactor: update a few dev deps and convert to using type imports where possible (#2886)
* build: bump deps and add some new eslint rules

* refactor: run eslint --fix on code to convert to type imports where possible
2022-08-03 12:57:51 +09:00
allcontributors[bot]
25eb765f9b docs: add frank-cywong as a contributor for code (#2884) [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>
2022-08-03 10:26:02 +09:00
Chun Yeung Wong
9da8461225 fix: update Discord ID regex to include 19 digit IDs (#2860) 2022-08-03 10:24:57 +09:00
Ryan Cohen
aed1409f29 fix(api): add rate limiter to TMDb requests to hopefully deal with 429s (#2881)
fixes #2853
2022-08-02 16:00:34 +09:00
Brandon Cohen
575da306b0 feat: plex deep links for iOS devices (#2680) 2022-08-02 06:41:09 +00:00
Danshil Kokil Mungur
6cd0c9b2c8 fix(api): use correct path param type in openapi spec (#2834) 2022-07-22 15:00:59 -04:00
Danshil Kokil Mungur
a6c1f3f7ce fix(api): ignore filter if unset in media route (#2647)
Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-05-28 12:48:23 +00:00
allcontributors[bot]
eb5248d8d1 docs: add sambartik as a contributor for code (#2783)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-05-28 19:13:54 +09:00
Samuel Bartík
db05172d8b fix(ui): rectangular avatars getting stretched (#2782) 2022-05-25 20:42:17 +04:00
Danshil Kokil Mungur
90095bb185 feat(manage slideover): show more request override details (#2772)
* feat(manage slideover): show the language profile if request is for a show

* feat(manage slideover): show name of profiles instead of id
2022-05-23 09:49:35 +09:00
Brandon Cohen
14519ef555 fix(recommendations): only load more titles if there can be more than 40 (#2749)
* fix: fixed recommendations page causing infinite network requests to tmdb api

TMDB API would only return 40 results and the recommendations page expected more. This would cause
an infinite amount of network requests. I set a limit specifically for this solving the problem.

fix #2710
2022-05-13 23:15:53 +04:00
Danshil Kokil Mungur
1054b4e2d7 fix(search): use correct param to filter movies by year (#2727) 2022-04-30 23:31:02 +09:00
Danshil Kokil Mungur
e4039d09c0 feat(api): add issue counts endpoint (#2713) 2022-04-25 23:06:36 +00:00
Danshil Kokil Mungur
29be659512 fix(ui): don't show 0 playcount in slideover (#2714) 2022-04-26 07:53:33 +09:00
Gian Marco Cinalli
475314c87b docs: update fail2ban documentation to handle logging changes (#2707) [skip ci] 2022-04-25 16:13:41 +00:00
TheCatLady
1d00229a48 feat(discover): add Paramount+ to network slider (#2608) 2022-04-25 13:09:22 +00:00
Brandon Cohen
b2878390b4 fix: manual browser refresh would redirect to home on search page (#2692)
If you used the search feature and tried to manual refresh or share the link, it would reset the
query. Taking you back to the home page.

fix #2683
2022-04-25 14:47:38 +04:00
TheCatLady
b75fc7b238 fix(auth): resolve local/password authentication issues (#2677)
* fix(auth): only add Plex ID to user after verifying server access

* fix(auth): do not fail local auth if fetching Plex users is unsuccessful
2022-04-06 08:31:14 +09:00
467 changed files with 15852 additions and 9558 deletions

View File

@@ -665,6 +665,78 @@
"contributions": [
"translation"
]
},
{
"login": "sambartik",
"name": "Samuel Bartík",
"avatar_url": "https://avatars.githubusercontent.com/u/63553146?v=4",
"profile": "https://github.com/sambartik",
"contributions": [
"code"
]
},
{
"login": "frank-cywong",
"name": "Chun Yeung Wong",
"avatar_url": "https://avatars.githubusercontent.com/u/90653148?v=4",
"profile": "https://github.com/frank-cywong",
"contributions": [
"code"
]
},
{
"login": "TheMeanCanEHdian",
"name": "TheMeanCanEHdian",
"avatar_url": "https://avatars.githubusercontent.com/u/16025103?v=4",
"profile": "https://github.com/TheMeanCanEHdian",
"contributions": [
"code"
]
},
{
"login": "Gylesie",
"name": "Gylesie",
"avatar_url": "https://avatars.githubusercontent.com/u/86306812?v=4",
"profile": "https://github.com/Gylesie",
"contributions": [
"code"
]
},
{
"login": "Fhd-pro",
"name": "Fhd-pro",
"avatar_url": "https://avatars.githubusercontent.com/u/82862079?v=4",
"profile": "https://github.com/Fhd-pro",
"contributions": [
"translation"
]
},
{
"login": "PovilasID",
"name": "PovilasID",
"avatar_url": "https://avatars.githubusercontent.com/u/396243?v=4",
"profile": "https://github.com/PovilasID",
"contributions": [
"translation"
]
},
{
"login": "byakurau",
"name": "byakurau",
"avatar_url": "https://avatars.githubusercontent.com/u/1811683?v=4",
"profile": "https://github.com/byakurau",
"contributions": [
"translation"
]
},
{
"login": "miknii",
"name": "miknii",
"avatar_url": "https://avatars.githubusercontent.com/u/109232569?v=4",
"profile": "https://github.com/miknii",
"contributions": [
"translation"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
@@ -673,5 +745,5 @@
"projectOwner": "sct",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
"skipCi": false
}

View File

@@ -26,3 +26,4 @@ public/os_logo_filled.png
public/preview.jpg
snap
stylelint.config.js
cypress

View File

@@ -7,6 +7,7 @@ module.exports = {
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:react/jsx-runtime',
'prettier',
],
parserOptions: {
@@ -26,11 +27,21 @@ module.exports = {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'formatjs/no-offset': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'@typescript-eslint/array-type': ['error', { default: 'array' }],
'jsx-a11y/no-onchange': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
},
],
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ allowSameFolder: true },
],
},
overrides: [
{
@@ -40,7 +51,7 @@ module.exports = {
},
},
],
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
plugins: ['jsx-a11y', 'react-hooks', 'formatjs', 'no-relative-import-paths'],
settings: {
react: {
pragma: 'React',

View File

@@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
time: '20:00'
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: daily
time: '20:00'
open-pull-requests-limit: 10

View File

@@ -13,16 +13,18 @@ jobs:
name: Lint & Test Build
if: github.event_name == 'pull_request'
runs-on: ubuntu-20.04
container: node:16.14-alpine
container: node:16.17-alpine
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
HUSKY: 0
run: yarn
- name: Lint
run: yarn lint
- name: Formatting
run: yarn format:check
- name: Build
run: yarn build
@@ -34,29 +36,29 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Log in to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
@@ -86,7 +88,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2
uses: technote-space/workflow-conclusion-action@v3
- name: Combine Job Status
id: status
run: |

30
.github/workflows/cypress.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Cypress Tests
on:
pull_request:
branches:
- '*'
push:
branches:
- develop
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cypress run
uses: cypress-io/github-action@v4
with:
build: yarn cypress:build
start: yarn start
wait-on: 'http://localhost:5055'
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_MIGRATIONS: true
# Fix test titles in cypress dashboard
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}

View File

@@ -16,22 +16,22 @@ jobs:
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile

View File

@@ -9,8 +9,6 @@ jobs:
semantic-release:
name: Tag and release latest version
runs-on: ubuntu-20.04
env:
HUSKY: 0
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -21,16 +19,16 @@ jobs:
with:
node-version: 16
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -74,7 +72,7 @@ jobs:
echo ::set-output name=RELEASE::edge
fi
- name: Set Up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- name: Build Snap Package
@@ -83,7 +81,7 @@ jobs:
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
@@ -105,7 +103,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2
uses: technote-space/workflow-conclusion-action@v3
- name: Combine Job Status
id: status
run: |

88
.github/workflows/snap.yaml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Publish Snap
on:
push:
branches:
- develop
jobs:
jobs:
name: Job Check
runs-on: ubuntu-20.04
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.10.0
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: jobs
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
- armhf
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Prepare
id: prepare
run: |
git fetch --prune --unshallow --tags
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
echo ::set-output name=RELEASE::stable
else
echo ::set-output name=RELEASE::edge
fi
- name: Set Up QEMU
uses: docker/setup-qemu-action@v2
- name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v3
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1
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: build-snap
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v3
- name: Combine Job Status
id: status
run: |
failures=(neutral, skipped, timed_out, action_required)
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
echo ::set-output name=status::failure
else
echo ::set-output name=status::$WORKFLOW_CONCLUSION
fi
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ steps.status.outputs.status }}
title: ${{ github.workflow }}
nofail: true

11
.gitignore vendored
View File

@@ -53,3 +53,14 @@ config/db/db.sqlite3-journal
# VS Code
.vscode/launch.json
# Cypress
cypress.env.json
cypress/videos
cypress/screenshots
# ESLint
.eslintcache
# TS Build Info
tsconfig.tsbuildinfo

5
.prettierrc.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: [require('./merged-prettier-plugin.js')],
singleQuote: true,
trailingComma: 'es5',
};

View File

@@ -11,9 +11,6 @@
// https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
"esbenp.prettier-vscode",
// https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script
"eg2.vscode-npm-script",
// https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest
"Orta.vscode-jest",

View File

@@ -15,8 +15,6 @@
"database": "./config/db/db.sqlite3"
}
],
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"editor.formatOnSave": true
"editor.formatOnSave": true,
"typescript.preferences.importModuleSpecifier": "non-relative"
}

File diff suppressed because it is too large Load Diff

View File

@@ -86,7 +86,7 @@ When adding new UI text, please try to adhere to the following guidelines:
1. Be concise and clear, and use as few words as possible to make your point.
2. Use the Oxford comma where appropriate.
3. Use the appropriate Unicode characters for ellipses, arrows, and other special characters/symbols.
4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., TMDb and IMDb have a lowercase 'b', whereas TheTVDB has a capital 'B'.
4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., IMDb has a lowercase 'b', whereas TMDB and TheTVDB have a capital 'B'.
5. Title case headings, button text, and form labels. Note that verbs such as "is" should be capitalized, whereas prepositions like "from" should be lowercase (unless as the first or last word of the string, in which case they are also capitalized).
6. Capitalize the first word in validation error messages, dropdowns, and form "tips." These strings should not end in punctuation.
7. Ensure that toast notification strings are complete sentences ending in punctuation.

View File

@@ -1,4 +1,4 @@
FROM node:16.14-alpine AS BUILD_IMAGE
FROM node:16.17-alpine AS BUILD_IMAGE
WORKDIR /app
@@ -14,7 +14,7 @@ RUN \
esac
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --network-timeout 1000000
RUN CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
COPY . ./
@@ -33,7 +33,7 @@ RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:16.14-alpine
FROM node:16.17-alpine
WORKDIR /app

View File

@@ -1,4 +1,4 @@
FROM node:16.14-alpine
FROM node:16.17-alpine
COPY . /app
WORKDIR /app

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-72-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-80-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
@@ -167,6 +167,16 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center"><a href="https://github.com/Andersborrits"><img src="https://avatars.githubusercontent.com/u/29452218?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andersborrits</b></sub></a><br /><a href="#translation-Andersborrits" title="Translation">🌍</a></td>
<td align="center"><a href="http://maxentrouault.fr"><img src="https://avatars.githubusercontent.com/u/67283154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maxent</b></sub></a><br /><a href="#translation-Maxentr" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sambartik" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/frank-cywong"><img src="https://avatars.githubusercontent.com/u/90653148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chun Yeung Wong</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=frank-cywong" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/TheMeanCanEHdian"><img src="https://avatars.githubusercontent.com/u/16025103?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TheMeanCanEHdian</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=TheMeanCanEHdian" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Gylesie"><img src="https://avatars.githubusercontent.com/u/86306812?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gylesie</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Gylesie" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Fhd-pro"><img src="https://avatars.githubusercontent.com/u/82862079?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fhd-pro</b></sub></a><br /><a href="#translation-Fhd-pro" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/PovilasID"><img src="https://avatars.githubusercontent.com/u/396243?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PovilasID</b></sub></a><br /><a href="#translation-PovilasID" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/byakurau"><img src="https://avatars.githubusercontent.com/u/1811683?v=4?s=100" width="100px;" alt=""/><br /><sub><b>byakurau</b></sub></a><br /><a href="#translation-byakurau" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/miknii"><img src="https://avatars.githubusercontent.com/u/109232569?v=4?s=100" width="100px;" alt=""/><br /><sub><b>miknii</b></sub></a><br /><a href="#translation-miknii" title="Translation">🌍</a></td>
</tr>
</table>

19
cypress.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'cypress';
export default defineConfig({
projectId: 'onnqy3',
e2e: {
baseUrl: 'http://localhost:5055',
experimentalSessionAndOrigin: true,
},
env: {
ADMIN_EMAIL: 'admin@seerr.dev',
ADMIN_PASSWORD: 'test1234',
USER_EMAIL: 'friend@seerr.dev',
USER_PASSWORD: 'test1234',
},
retries: {
runMode: 2,
openMode: 0,
},
});

View File

@@ -0,0 +1,149 @@
{
"clientId": "6919275e-142a-48d8-be6b-93594cbd4626",
"vapidPrivate": "tmnslaO8ZWN6bNbSEv_rolPeBTlNxOwCCAHrM9oZz3M",
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
"main": {
"apiKey": "testkey",
"applicationTitle": "Overseerr",
"applicationUrl": "",
"csrfProtection": false,
"cacheImages": false,
"defaultPermissions": 32,
"defaultQuotas": {
"movie": {},
"tv": {}
},
"hideAvailable": false,
"localLogin": true,
"newPlexLogin": true,
"region": "",
"originalLanguage": "",
"trustProxy": false,
"partialRequestsEnabled": true,
"locale": "en"
},
"plex": {
"name": "Seerr",
"ip": "192.168.1.1",
"port": 32400,
"useSsl": false,
"libraries": [
{
"id": "1",
"name": "Movies",
"enabled": true,
"type": "movie"
}
],
"machineId": "test"
},
"tautulli": {},
"radarr": [],
"sonarr": [],
"public": {
"initialized": true
},
"notifications": {
"agents": {
"email": {
"enabled": false,
"options": {
"emailFrom": "",
"smtpHost": "",
"smtpPort": 587,
"secure": false,
"ignoreTls": false,
"requireTls": false,
"allowSelfSigned": false,
"senderName": "Overseerr"
}
},
"discord": {
"enabled": false,
"types": 0,
"options": {
"webhookUrl": "",
"enableMentions": true
}
},
"lunasea": {
"enabled": false,
"types": 0,
"options": {
"webhookUrl": ""
}
},
"slack": {
"enabled": false,
"types": 0,
"options": {
"webhookUrl": ""
}
},
"telegram": {
"enabled": false,
"types": 0,
"options": {
"botAPI": "",
"chatId": "",
"sendSilently": false
}
},
"pushbullet": {
"enabled": false,
"types": 0,
"options": {
"accessToken": ""
}
},
"pushover": {
"enabled": false,
"types": 0,
"options": {
"accessToken": "",
"userToken": ""
}
},
"webhook": {
"enabled": false,
"types": 0,
"options": {
"webhookUrl": "",
"jsonPayload": "IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i"
}
},
"webpush": {
"enabled": false,
"options": {}
},
"gotify": {
"enabled": false,
"types": 0,
"options": {
"url": "",
"token": ""
}
}
}
},
"jobs": {
"plex-recently-added-scan": {
"schedule": "0 */5 * * * *"
},
"plex-full-scan": {
"schedule": "0 0 3 * * *"
},
"radarr-scan": {
"schedule": "0 0 4 * * *"
},
"sonarr-scan": {
"schedule": "0 30 4 * * *"
},
"download-sync": {
"schedule": "0 * * * * *"
},
"download-sync-reset": {
"schedule": "0 0 1 * * *"
}
}
}

210
cypress/e2e/discover.cy.ts Normal file
View File

@@ -0,0 +1,210 @@
const clickFirstTitleCardInSlider = (sliderTitle: string): void => {
cy.contains('.slider-header', sliderTitle)
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.trigger('mouseover')
.find('[data-testid=title-card-title]')
.invoke('text')
.then((text) => {
cy.contains('.slider-header', sliderTitle)
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.click();
cy.get('[data-testid=media-title]').should('contain', text);
});
};
describe('Discover', () => {
beforeEach(() => {
cy.loginAsAdmin();
});
it('loads a trending item', () => {
cy.intercept('/api/v1/discover/trending*').as('getTrending');
cy.visit('/');
cy.wait('@getTrending');
clickFirstTitleCardInSlider('Trending');
});
it('loads popular movies', () => {
cy.intercept('/api/v1/discover/movies*').as('getPopularMovies');
cy.visit('/');
cy.wait('@getPopularMovies');
clickFirstTitleCardInSlider('Popular Movies');
});
it('loads upcoming movies', () => {
cy.intercept('/api/v1/discover/movies/upcoming*').as('getUpcomingMovies');
cy.visit('/');
cy.wait('@getUpcomingMovies');
clickFirstTitleCardInSlider('Upcoming Movies');
});
it('loads popular series', () => {
cy.intercept('/api/v1/discover/tv*').as('getPopularTv');
cy.visit('/');
cy.wait('@getPopularTv');
clickFirstTitleCardInSlider('Popular Series');
});
it('loads upcoming series', () => {
cy.intercept('/api/v1/discover/tv/upcoming*').as('getUpcomingSeries');
cy.visit('/');
cy.wait('@getUpcomingSeries');
clickFirstTitleCardInSlider('Upcoming Series');
});
it('displays error for media with invalid TMDB ID', () => {
cy.intercept('GET', '/api/v1/media?*', {
pageInfo: { pages: 1, pageSize: 20, results: 1, page: 1 },
results: [
{
downloadStatus: [],
downloadStatus4k: [],
id: 1922,
mediaType: 'movie',
tmdbId: 998814,
tvdbId: null,
imdbId: null,
status: 5,
status4k: 1,
createdAt: '2022-08-18T18:11:13.000Z',
updatedAt: '2022-08-18T19:56:41.000Z',
lastSeasonChange: '2022-08-18T19:56:41.000Z',
mediaAddedAt: '2022-08-18T19:56:41.000Z',
serviceId: null,
serviceId4k: null,
externalServiceId: null,
externalServiceId4k: null,
externalServiceSlug: null,
externalServiceSlug4k: null,
ratingKey: null,
ratingKey4k: null,
seasons: [],
},
],
}).as('getMedia');
cy.visit('/');
cy.wait('@getMedia');
cy.contains('.slider-header', 'Recently Added')
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.find('[data-testid=title-card-title]')
.contains('Movie Not Found');
});
it('displays error for request with invalid TMDB ID', () => {
cy.intercept('GET', '/api/v1/request?*', {
pageInfo: { pages: 1, pageSize: 10, results: 1, page: 1 },
results: [
{
id: 582,
status: 1,
createdAt: '2022-08-18T18:11:13.000Z',
updatedAt: '2022-08-18T18:11:13.000Z',
type: 'movie',
is4k: false,
serverId: null,
profileId: null,
rootFolder: null,
languageProfileId: null,
tags: null,
media: {
downloadStatus: [],
downloadStatus4k: [],
id: 1922,
mediaType: 'movie',
tmdbId: 998814,
tvdbId: null,
imdbId: null,
status: 2,
status4k: 1,
createdAt: '2022-08-18T18:11:13.000Z',
updatedAt: '2022-08-18T18:11:13.000Z',
lastSeasonChange: '2022-08-18T18:11:13.000Z',
mediaAddedAt: null,
serviceId: null,
serviceId4k: null,
externalServiceId: null,
externalServiceId4k: null,
externalServiceSlug: null,
externalServiceSlug4k: null,
ratingKey: null,
ratingKey4k: null,
},
seasons: [],
modifiedBy: null,
requestedBy: {
permissions: 4194336,
id: 18,
email: 'friend@seerr.dev',
plexUsername: null,
username: '',
recoveryLinkExpirationDate: null,
userType: 2,
avatar:
'https://gravatar.com/avatar/c77fdc27cab83732b8623d2ea873d330?default=mm&size=200',
movieQuotaLimit: null,
movieQuotaDays: null,
tvQuotaLimit: null,
tvQuotaDays: null,
createdAt: '2022-08-17T04:55:28.000Z',
updatedAt: '2022-08-17T04:55:28.000Z',
requestCount: 1,
displayName: 'friend@seerr.dev',
},
seasonCount: 0,
},
],
}).as('getRequests');
cy.visit('/');
cy.wait('@getRequests');
cy.contains('.slider-header', 'Recent Requests')
.next('[data-testid=media-slider]')
.find('[data-testid=request-card]')
.first()
.find('[data-testid=request-card-title]')
.contains('Movie Not Found');
});
it('loads plex watchlist', () => {
cy.intercept('/api/v1/discover/watchlist', {
fixture: 'watchlist.json',
}).as('getWatchlist');
// Wait for one of the watchlist movies to resolve
cy.intercept('/api/v1/movie/361743').as('getTmdbMovie');
cy.visit('/');
cy.wait('@getWatchlist');
const sliderHeader = cy.contains('.slider-header', 'Your Plex Watchlist');
sliderHeader.scrollIntoView();
cy.wait('@getTmdbMovie');
// Wait a little longer to make sure the movie component reloaded
cy.wait(500);
sliderHeader
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.trigger('mouseover')
.find('[data-testid=title-card-title]')
.invoke('text')
.then((text) => {
cy.contains('.slider-header', 'Plex Watchlist')
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.click();
cy.get('[data-testid=media-title]').should('contain', text);
});
});
});

13
cypress/e2e/login.cy.ts Normal file
View File

@@ -0,0 +1,13 @@
describe('Login Page', () => {
it('succesfully logs in as an admin', () => {
cy.loginAsAdmin();
cy.visit('/');
cy.contains('Trending');
});
it('succesfully logs in as a local user', () => {
cy.loginAsUser();
cy.visit('/');
cy.contains('Trending');
});
});

View File

@@ -0,0 +1,12 @@
describe('Movie Details', () => {
it('loads a movie page', () => {
cy.loginAsAdmin();
// Try to load minions: rise of gru
cy.visit('/movie/438148');
cy.get('[data-testid=media-title]').should(
'contain',
'Minions: The Rise of Gru (2022)'
);
});
});

View File

@@ -0,0 +1,32 @@
describe('General Settings', () => {
beforeEach(() => {
cy.loginAsAdmin();
});
it('opens the settings page from the home page', () => {
cy.visit('/');
cy.get('[data-testid=sidebar-toggle]').click();
cy.get('[data-testid=sidebar-menu-settings-mobile]').click();
cy.get('.heading').should('contain', 'General Settings');
});
it('modifies setting that requires restart', () => {
cy.visit('/settings');
cy.get('#trustProxy').click();
cy.get('form').submit();
cy.get('[data-testid=modal-title]').should(
'contain',
'Server Restart Required'
);
cy.get('[data-testid=modal-ok-button]').click();
cy.get('[data-testid=modal-title]').should('not.exist');
cy.get('[type=checkbox]#trustProxy').click();
cy.get('form').submit();
cy.get('[data-testid=modal-title]').should('not.exist');
});
});

View File

@@ -0,0 +1,28 @@
describe('TV Details', () => {
it('loads a tv details page', () => {
cy.loginAsAdmin();
// Try to load stranger things
cy.visit('/tv/66732');
cy.get('[data-testid=media-title]').should(
'contain',
'Stranger Things (2016)'
);
});
it('shows seasons and expands episodes', () => {
cy.loginAsAdmin();
// Try to load stranger things
cy.visit('/tv/66732');
// intercept request for season info
cy.intercept('/api/v1/tv/66732/season/4').as('season4');
cy.contains('Season 4').should('be.visible').scrollIntoView().click();
cy.wait('@season4');
cy.contains('Chapter Nine').should('be.visible');
});
});

View File

@@ -0,0 +1,74 @@
const visitUserEditPage = (email: string): void => {
cy.visit('/users');
cy.contains('[data-testid=user-list-row]', email).contains('Edit').click();
};
describe('Auto Request Settings', () => {
beforeEach(() => {
cy.loginAsAdmin();
});
it('should not see watchlist sync settings on an account without permissions', () => {
visitUserEditPage(Cypress.env('USER_EMAIL'));
cy.contains('Auto-Request Movies').should('not.exist');
cy.contains('Auto-Request Series').should('not.exist');
});
it('should see watchlist sync settings on an admin account', () => {
visitUserEditPage(Cypress.env('ADMIN_EMAIL'));
cy.contains('Auto-Request Movies').should('exist');
cy.contains('Auto-Request Series').should('exist');
});
it('should see auto-request settings after being given permission', () => {
visitUserEditPage(Cypress.env('USER_EMAIL'));
cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click();
cy.get('#autorequest').should('not.be.checked').click();
cy.intercept('/api/v1/user/*/settings/permissions').as('userPermissions');
cy.contains('Save Changes').click();
cy.wait('@userPermissions');
cy.reload();
cy.get('#autorequest').should('be.checked');
cy.get('#autorequestmovies').should('be.checked');
cy.get('#autorequesttv').should('be.checked');
cy.get('[data-testid=settings-nav-desktop').contains('General').click();
cy.contains('Auto-Request Movies').should('exist');
cy.contains('Auto-Request Series').should('exist');
cy.get('#watchlistSyncMovies').should('not.be.checked').click();
cy.get('#watchlistSyncTv').should('not.be.checked').click();
cy.intercept('/api/v1/user/*/settings/main').as('userMain');
cy.contains('Save Changes').click();
cy.wait('@userMain');
cy.reload();
cy.get('#watchlistSyncMovies').should('be.checked').click();
cy.get('#watchlistSyncTv').should('be.checked').click();
cy.contains('Save Changes').click();
cy.wait('@userMain');
cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click();
cy.get('#autorequest').should('be.checked').click();
cy.contains('Save Changes').click();
});
});

View File

@@ -0,0 +1,50 @@
describe('User Profile', () => {
beforeEach(() => {
cy.loginAsAdmin();
});
it('opens user profile page from the home page', () => {
cy.visit('/');
cy.get('[data-testid=user-menu]').click();
cy.get('[data-testid=user-menu-profile]').click();
cy.get('h1').should('contain', Cypress.env('ADMIN_EMAIL'));
});
it('loads plex watchlist', () => {
cy.intercept('/api/v1/user/[0-9]*/watchlist', {
fixture: 'watchlist.json',
}).as('getWatchlist');
// Wait for one of the watchlist movies to resolve
cy.intercept('/api/v1/movie/361743').as('getTmdbMovie');
cy.visit('/profile');
cy.wait('@getWatchlist');
const sliderHeader = cy.contains('.slider-header', 'Plex Watchlist');
sliderHeader.scrollIntoView();
cy.wait('@getTmdbMovie');
// Wait a little longer to make sure the movie component reloaded
cy.wait(500);
sliderHeader
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.trigger('mouseover')
.find('[data-testid=title-card-title]')
.invoke('text')
.then((text) => {
cy.contains('.slider-header', 'Plex Watchlist')
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]')
.first()
.click();
cy.get('[data-testid=media-title]').should('contain', text);
});
});
});

View File

@@ -0,0 +1,70 @@
const testUser = {
displayName: 'Test User',
emailAddress: 'test@seeerr.dev',
password: 'test1234',
};
describe('User List', () => {
beforeEach(() => {
cy.loginAsAdmin();
});
it('opens the user list from the home page', () => {
cy.visit('/');
cy.get('[data-testid=sidebar-toggle]').click();
cy.get('[data-testid=sidebar-menu-users-mobile]').click();
cy.get('[data-testid=page-header]').should('contain', 'User List');
});
it('can find the admin user and friend user in the user list', () => {
cy.visit('/users');
cy.get('[data-testid=user-list-row]').contains(Cypress.env('ADMIN_EMAIL'));
cy.get('[data-testid=user-list-row]').contains(Cypress.env('USER_EMAIL'));
});
it('can create a local user', () => {
cy.visit('/users');
cy.contains('Create Local User').click();
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
cy.get('#displayName').type(testUser.displayName);
cy.get('#email').type(testUser.emailAddress);
cy.get('#password').type(testUser.password);
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
cy.get('[data-testid=modal-ok-button]').click();
cy.wait('@user');
// Wait a little longer for the user list to fully re-render
cy.wait(1000);
cy.get('[data-testid=user-list-row]').contains(testUser.emailAddress);
});
it('can delete the created local test user', () => {
cy.visit('/users');
cy.contains('[data-testid=user-list-row]', testUser.emailAddress)
.contains('Delete')
.click();
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
cy.wait('@user');
cy.wait(1000);
cy.get('[data-testid=user-list-row]')
.contains(testUser.emailAddress)
.should('not.exist');
});
});

View File

@@ -0,0 +1,25 @@
{
"page": 1,
"totalPages": 1,
"totalResults": 3,
"results": [
{
"ratingKey": "5d776be17a53e9001e732ab9",
"title": "Top Gun: Maverick",
"mediaType": "movie",
"tmdbId": 361743
},
{
"ratingKey": "5e16338fbc1372003ea68ab3",
"title": "Nope",
"mediaType": "movie",
"tmdbId": 762504
},
{
"ratingKey": "5f409b8452f200004161e126",
"title": "Hocus Pocus 2",
"mediaType": "movie",
"tmdbId": 642885
}
]
}

View File

@@ -0,0 +1,34 @@
/// <reference types="cypress" />
Cypress.Commands.add('login', (email, password) => {
cy.session(
[email, password],
() => {
cy.visit('/login');
cy.contains('Use your Overseerr account').click();
cy.get('[data-testid=email]').type(email);
cy.get('[data-testid=password]').type(password);
cy.intercept('/api/v1/auth/local').as('localLogin');
cy.get('[data-testid=local-signin-button]').click();
cy.wait('@localLogin');
cy.url().should('contain', '/');
},
{
validate() {
cy.request('/api/v1/auth/me').its('status').should('eq', 200);
},
}
);
});
Cypress.Commands.add('loginAsAdmin', () => {
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
});
Cypress.Commands.add('loginAsUser', () => {
cy.login(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD'));
});

7
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,7 @@
import './commands';
before(() => {
if (Cypress.env('SEED_DATABASE')) {
cy.exec('yarn cypress:prepare');
}
});

14
cypress/support/index.ts Normal file
View File

@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-namespace */
/// <reference types="cypress" />
declare global {
namespace Cypress {
interface Chainable {
login(email?: string, password?: string): Chainable<Element>;
loginAsAdmin(): Chainable<Element>;
loginAsUser(): Chainable<Element>;
}
}
}
export {};

10
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"],
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": ["**/*.ts"]
}

View File

@@ -8,7 +8,7 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you
```
[Definition]
failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":"<HOST>"
failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
```
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.

View File

@@ -9,7 +9,7 @@
- [LunaSea](https://docs.lunasea.app/modules/overseerr), a self-hosted controller for mobile and macOS
- [Requestrr](https://github.com/darkalfx/requestrr/wiki/Configuring-Overseerr), a Discord chatbot
- [Doplarr](https://github.com/kiranshila/Doplarr), a Discord request bot
- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDb and IMDb
- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDB and IMDb
- [ha-overseerr](https://github.com/vaparr/ha-overseerr), a custom Home Assistant component
- [OverCLIrr](https://github.com/WillFantom/OverCLIrr), a command-line tool
- [Overseerr Exporter](https://github.com/WillFantom/overseerr-exporter), a Prometheus exporter

View File

@@ -45,7 +45,7 @@ Overseerr currently supports the following agents:
- New Plex TV
- Legacy Plex TV
- TheTVDB
- TMDb
- TMDB
- [HAMA](https://github.com/ZeroQI/Hama.bundle)
Please verify that your library is using one of the agents previously listed.
@@ -67,7 +67,7 @@ You can also perform the following to verify the media item has a GUID Overseerr
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
2. Verify that the media item's GUID follows one of the below formats:
1. TMDb agent `guid="com.plexapp.agents.themoviedb://1705"`
1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"`
2. New Plex Movie agent `<Guid id="tmdb://464052"/>`
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`

View File

@@ -81,7 +81,7 @@ These following special variables are only included in media-related notificatio
| Variable | Value |
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
| `{{media_type}}` | The media type (`movie` or `tv`) |
| `{{media_tmdbid}}` | The media's TMDb ID |
| `{{media_tmdbid}}` | The media's TMDB ID |
| `{{media_tvdbid}}` | The media's TheTVDB ID |
| `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
| `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |

21
merged-prettier-plugin.js Normal file
View File

@@ -0,0 +1,21 @@
/* eslint-disable */
const tailwind = require('prettier-plugin-tailwindcss');
const organizeImports = require('prettier-plugin-organize-imports');
const combinedFormatter = {
...tailwind,
parsers: {
...tailwind.parsers,
...Object.keys(organizeImports.parsers).reduce((acc, key) => {
acc[key] = {
...tailwind.parsers[key],
preprocess(code, options) {
return organizeImports.parsers[key].preprocess(code, options);
},
};
return acc;
}, {}),
},
};
module.exports = combinedFormatter;

View File

@@ -1,3 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
module.exports = {
env: {
commitTag: process.env.COMMIT_TAG || 'local',

View File

@@ -121,6 +121,9 @@ components:
hideAvailable:
type: boolean
example: false
hideRequested:
type: boolean
example: false
partialRequestsEnabled:
type: boolean
example: false
@@ -1793,14 +1796,14 @@ components:
paths:
/status:
get:
summary: Get Overseerr version
description: Returns the current Overseerr version in a JSON object.
summary: Get Overseerr status
description: Returns the current Overseerr status in a JSON object.
security: []
tags:
- public
responses:
'200':
description: Returned version
description: Returned status
content:
application/json:
schema:
@@ -1811,6 +1814,12 @@ paths:
example: 1.0.0
commitTag:
type: string
updateAvailable:
type: boolean
commitsBehind:
type: number
restartRequired:
type: boolean
/status/appdata:
get:
summary: Get application data volume status
@@ -3170,8 +3179,8 @@ paths:
name: guid
required: true
schema:
type: number
example: 1
type: string
example: '9afef5a7-ec89-4d5f-9397-261e96970b50'
responses:
'200':
description: OK
@@ -3506,6 +3515,53 @@ paths:
restricted:
type: boolean
example: false
/user/{userId}/watchlist:
get:
summary: Get user by ID
description: |
Retrieves a user's Plex Watchlist in a JSON object.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
- in: query
name: page
schema:
type: number
example: 1
default: 1
responses:
'200':
description: Watchlist data returned
content:
application/json:
schema:
type: object
properties:
page:
type: number
totalPages:
type: number
totalResults:
type: number
results:
type: array
items:
type: object
properties:
tmdbId:
type: number
example: 1
ratingKey:
type: string
type:
type: string
title:
type: string
/user/{userId}/settings/main:
get:
summary: Get general settings for a user
@@ -4397,6 +4453,46 @@ paths:
name:
type: string
example: Genre Name
/discover/watchlist:
get:
summary: Get the Plex watchlist.
tags:
- search
parameters:
- in: query
name: page
schema:
type: number
example: 1
default: 1
responses:
'200':
description: Watchlist data returned
content:
application/json:
schema:
type: object
properties:
page:
type: number
totalPages:
type: number
totalResults:
type: number
results:
type: array
items:
type: object
properties:
tmdbId:
type: number
example: 1
ratingKey:
type: string
type:
type: string
title:
type: string
/request:
get:
summary: Get all requests
@@ -4424,7 +4520,16 @@ paths:
schema:
type: string
nullable: true
enum: [all, approved, available, pending, processing, unavailable]
enum:
[
all,
approved,
available,
pending,
processing,
unavailable,
failed,
]
- in: query
name: sort
schema:
@@ -5327,7 +5432,7 @@ paths:
$ref: '#/components/schemas/SonarrSeries'
/regions:
get:
summary: Regions supported by TMDb
summary: Regions supported by TMDB
description: Returns a list of regions in a JSON object.
tags:
- tmdb
@@ -5349,7 +5454,7 @@ paths:
example: United States of America
/languages:
get:
summary: Languages supported by TMDb
summary: Languages supported by TMDB
description: Returns a list of languages in a JSON object.
tags:
- tmdb
@@ -5414,7 +5519,7 @@ paths:
$ref: '#/components/schemas/ProductionCompany'
/genres/movie:
get:
summary: Get list of official TMDb movie genres
summary: Get list of official TMDB movie genres
description: Returns a list of genres in a JSON array.
tags:
- tmdb
@@ -5442,7 +5547,7 @@ paths:
example: Family
/genres/tv:
get:
summary: Get list of official TMDb movie genres
summary: Get list of official TMDB movie genres
description: Returns a list of genres in a JSON array.
tags:
- tmdb
@@ -5562,6 +5667,36 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Issue'
/issue/count:
get:
summary: Gets issue counts
description: |
Returns the number of open and closed issues, as well as the number of issues of each type.
tags:
- issue
responses:
'200':
description: Issue counts returned
content:
application/json:
schema:
type: object
properties:
total:
type: number
video:
type: number
audio:
type: number
subtitles:
type: number
others:
type: number
open:
type: number
closed:
type: number
/issue/{issueId}:
get:
summary: Get issue

View File

@@ -1,20 +1,27 @@
{
"name": "overseerr",
"version": "1.29.0",
"version": "0.1.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",
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates",
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json",
"build:next": "next build",
"build": "yarn build:next && yarn build:server",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache",
"start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault true \"./src/**/!(*.test).{ts,tsx}\"",
"migration:generate": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate",
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create",
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run",
"format": "prettier --write .",
"prepare": "husky install"
"migration:generate": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -d server/datasource.ts",
"migration:create": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create -d server/datasource.ts",
"migration:run": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run -d server/datasource.ts",
"format": "prettier --loglevel warn --write --cache .",
"format:check": "prettier --check --cache .",
"typecheck": "yarn typecheck:server && yarn typecheck:client",
"typecheck:server": "tsc --project server/tsconfig.json --noEmit",
"typecheck:client": "tsc --noEmit",
"prepare": "husky install",
"cypress:open": "cypress open",
"cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts",
"cypress:build": "yarn build && yarn cypress:prepare"
},
"repository": {
"type": "git",
@@ -22,127 +29,140 @@
},
"license": "MIT",
"dependencies": {
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.6",
"@supercharge/request-ip": "^1.2.0",
"@svgr/webpack": "^6.2.1",
"@tanem/react-nprogress": "^4.0.10",
"ace-builds": "^1.4.14",
"axios": "^0.26.1",
"bcrypt": "^5.0.1",
"bowser": "^2.11.0",
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.4.21",
"csurf": "^1.11.0",
"email-templates": "^8.0.10",
"express": "^4.17.3",
"express-openapi-validator": "^4.13.6",
"express-rate-limit": "^6.3.0",
"express-session": "^1.17.2",
"formik": "^2.2.9",
"gravatar-url": "^3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.21",
"next": "12.1.0",
"node-cache": "^5.1.2",
"node-gyp": "^9.0.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"openpgp": "^5.2.0",
"plex-api": "^5.3.2",
"pug": "^3.0.2",
"react": "17.0.2",
"react-ace": "^9.5.0",
"react-animate-height": "^2.0.23",
"react-dom": "17.0.2",
"react-intersection-observer": "^8.33.1",
"react-intl": "5.24.7",
"react-markdown": "^8.0.0",
"react-select": "^5.2.2",
"react-spring": "^9.4.4",
"react-toast-notifications": "^2.5.1",
"react-transition-group": "^4.4.2",
"react-truncate-markup": "^5.1.0",
"react-use-clipboard": "1.0.7",
"reflect-metadata": "^0.1.13",
"secure-random-password": "^0.2.3",
"semver": "^7.3.5",
"sqlite3": "^5.0.2",
"swagger-ui-express": "^4.3.0",
"swr": "^1.2.2",
"typeorm": "0.2.45",
"web-push": "^3.4.5",
"winston": "^3.6.0",
"winston-daily-rotate-file": "^4.6.1",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.32.11"
"@formatjs/intl-displaynames": "6.0.3",
"@formatjs/intl-locale": "3.0.3",
"@formatjs/intl-pluralrules": "5.0.3",
"@formatjs/intl-utils": "3.8.4",
"@headlessui/react": "^0.0.0-insiders.b301f04",
"@heroicons/react": "1.0.6",
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.3.1",
"@tanem/react-nprogress": "5.0.11",
"ace-builds": "1.9.6",
"axios": "0.27.2",
"axios-rate-limit": "1.3.0",
"bcrypt": "5.0.1",
"bowser": "2.11.0",
"connect-typeorm": "1.1.4",
"cookie-parser": "1.4.6",
"copy-to-clipboard": "3.3.2",
"country-flag-icons": "1.5.5",
"csurf": "1.11.0",
"date-fns": "2.29.1",
"email-templates": "9.0.0",
"express": "4.18.1",
"express-openapi-validator": "4.13.8",
"express-rate-limit": "6.5.1",
"express-session": "1.17.3",
"formik": "2.2.9",
"gravatar-url": "3.1.0",
"intl": "1.2.5",
"lodash": "4.17.21",
"next": "12.2.5",
"node-cache": "5.1.2",
"node-gyp": "9.1.0",
"node-schedule": "2.1.0",
"nodemailer": "6.7.8",
"openpgp": "5.4.0",
"plex-api": "5.3.2",
"pug": "3.0.2",
"react": "18.2.0",
"react-ace": "10.1.0",
"react-animate-height": "2.1.2",
"react-dom": "18.2.0",
"react-intersection-observer": "9.4.0",
"react-intl": "6.0.5",
"react-markdown": "8.0.3",
"react-popper-tooltip": "4.4.2",
"react-select": "5.4.0",
"react-spring": "9.5.2",
"react-toast-notifications": "2.5.1",
"react-truncate-markup": "5.1.2",
"react-use-clipboard": "1.0.8",
"reflect-metadata": "0.1.13",
"secure-random-password": "0.2.3",
"semver": "7.3.7",
"sqlite3": "5.0.11",
"swagger-ui-express": "4.5.0",
"swr": "1.3.0",
"typeorm": "0.3.7",
"web-push": "3.5.0",
"winston": "3.8.1",
"winston-daily-rotate-file": "4.7.1",
"xml2js": "0.4.23",
"yamljs": "0.3.0",
"yup": "0.32.11"
},
"devDependencies": {
"@babel/cli": "^7.17.6",
"@commitlint/cli": "^16.2.1",
"@commitlint/config-conventional": "^16.2.1",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/commit-analyzer": "^9.0.2",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@tailwindcss/aspect-ratio": "^0.4.0",
"@tailwindcss/forms": "^0.5.0",
"@tailwindcss/typography": "^0.5.2",
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.2",
"@types/country-flag-icons": "^1.2.0",
"@types/csurf": "^1.11.2",
"@types/email-templates": "^8.0.4",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/lodash": "^4.14.179",
"@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4",
"@types/react": "^17.0.40",
"@types/react-dom": "^17.0.13",
"@types/react-transition-group": "^4.4.4",
"@types/secure-random-password": "^0.2.1",
"@types/semver": "^7.3.9",
"@types/swagger-ui-express": "^4.1.3",
"@types/web-push": "^3.3.2",
"@types/xml2js": "^0.4.9",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.13",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"autoprefixer": "^10.4.2",
"babel-plugin-react-intl": "^8.2.25",
"babel-plugin-react-intl-auto": "^3.3.0",
"commitizen": "^4.2.4",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.11.0",
"eslint-config-next": "^12.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-formatjs": "^3.0.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-react-hooks": "^4.3.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^7.0.4",
"lint-staged": "^12.3.5",
"nodemon": "^2.0.15",
"postcss": "^8.4.8",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.8",
"semantic-release": "^19.0.2",
"semantic-release-docker-buildx": "^1.0.1",
"tailwindcss": "^3.0.23",
"ts-node": "^10.7.0",
"typescript": "^4.6.2"
"@babel/cli": "7.18.10",
"@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "17.0.3",
"@semantic-release/changelog": "6.0.1",
"@semantic-release/commit-analyzer": "9.0.2",
"@semantic-release/exec": "6.0.3",
"@semantic-release/git": "10.0.1",
"@tailwindcss/aspect-ratio": "0.4.0",
"@tailwindcss/forms": "0.5.2",
"@tailwindcss/typography": "0.5.4",
"@types/bcrypt": "5.0.0",
"@types/cookie-parser": "1.4.3",
"@types/country-flag-icons": "1.2.0",
"@types/csurf": "1.11.2",
"@types/email-templates": "8.0.4",
"@types/express": "4.17.13",
"@types/express-session": "1.17.4",
"@types/lodash": "4.14.183",
"@types/node": "17.0.36",
"@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.5",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"@types/react-transition-group": "4.4.5",
"@types/secure-random-password": "0.2.1",
"@types/semver": "7.3.12",
"@types/swagger-ui-express": "4.1.3",
"@types/web-push": "3.3.2",
"@types/xml2js": "0.4.11",
"@types/yamljs": "0.2.31",
"@types/yup": "0.29.14",
"@typescript-eslint/eslint-plugin": "5.33.1",
"@typescript-eslint/parser": "5.33.1",
"autoprefixer": "10.4.8",
"babel-plugin-react-intl": "8.2.25",
"babel-plugin-react-intl-auto": "3.3.0",
"commitizen": "4.2.5",
"copyfiles": "2.4.1",
"cypress": "10.6.0",
"cz-conventional-changelog": "3.3.0",
"eslint": "8.22.0",
"eslint-config-next": "12.2.5",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-formatjs": "4.1.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-no-relative-import-paths": "1.4.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.30.1",
"eslint-plugin-react-hooks": "4.6.0",
"extract-react-intl-messages": "4.1.1",
"husky": "8.0.1",
"lint-staged": "12.4.3",
"nodemon": "2.0.19",
"postcss": "8.4.16",
"prettier": "2.7.1",
"prettier-plugin-organize-imports": "3.1.0",
"prettier-plugin-tailwindcss": "0.1.13",
"semantic-release": "19.0.3",
"semantic-release-docker-buildx": "1.0.1",
"tailwindcss": "3.1.8",
"ts-node": "10.9.1",
"tsc-alias": "1.7.0",
"tsconfig-paths": "4.1.0",
"typescript": "4.7.4"
},
"resolutions": {
"sqlite3/node-gyp": "^8.4.1"
"sqlite3/node-gyp": "8.4.1",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6"
},
"config": {
"commitizen": {
@@ -163,10 +183,6 @@
"@commitlint/config-conventional"
]
},
"prettier": {
"singleQuote": true,
"trailingComma": "es5"
},
"release": {
"plugins": [
"@semantic-release/commit-analyzer",

21
renovate.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:js-app",
"group:allNonMajor",
"docker:disableMajor",
"helpers:disableTypesNodeMajor"
],
"packageRules": [
{
"matchManagers": ["github-actions"],
"groupName": "GitHub Actions",
"groupSlug": "github-actions"
},
{
"matchPackageNames": ["node"],
"groupName": "Node.js",
"groupSlug": "node"
}
]
}

View File

@@ -1,8 +1,8 @@
import logger from '@server/logger';
import axios from 'axios';
import xml2js from 'xml2js';
import fs, { promises as fsp } from 'fs';
import path from 'path';
import logger from '../logger';
import xml2js from 'xml2js';
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
// originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml
@@ -14,7 +14,7 @@ const LOCAL_PATH = process.env.CONFIG_DIRECTORY
const mappingRegexp = new RegExp(/;[0-9]+-([0-9]+)/g);
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDb IDs
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDB IDs
// https://github.com/Anime-Lists/anime-lists/
interface AnimeMapping {

View File

@@ -1,5 +1,7 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import NodeCache from 'node-cache';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import axios from 'axios';
import rateLimit from 'axios-rate-limit';
import type NodeCache from 'node-cache';
// 5 minute default TTL (in seconds)
const DEFAULT_TTL = 300;
@@ -10,6 +12,10 @@ const DEFAULT_ROLLING_BUFFER = 10000;
interface ExternalAPIOptions {
nodeCache?: NodeCache;
headers?: Record<string, unknown>;
rateLimit?: {
maxRPS: number;
maxRequests: number;
};
}
class ExternalAPI {
@@ -31,6 +37,14 @@ class ExternalAPI {
...options.headers,
},
});
if (options.rateLimit) {
this.axios = rateLimit(this.axios, {
maxRequests: options.rateLimit.maxRequests,
maxRPS: options.rateLimit.maxRPS,
});
}
this.baseUrl = baseUrl;
this.cache = options.nodeCache;
}

View File

@@ -1,5 +1,5 @@
import cacheManager from '../lib/cache';
import logger from '../logger';
import cacheManager from '@server/lib/cache';
import logger from '@server/logger';
import ExternalAPI from './externalapi';
interface GitHubRelease {

View File

@@ -1,6 +1,7 @@
import type { Library, PlexSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import NodePlexAPI from 'plex-api';
import { getSettings, Library, PlexSettings } from '../lib/settings';
import logger from '../logger';
export interface PlexLibraryItem {
ratingKey: string;
@@ -130,7 +131,6 @@ class PlexAPI {
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async getStatus() {
return await this.plexClient.query('/');
}

View File

@@ -1,8 +1,9 @@
import axios, { AxiosInstance } from 'axios';
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
import cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import xml2js from 'xml2js';
import { PlexDevice } from '../interfaces/api/plexInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import ExternalAPI from './externalapi';
interface PlexAccountResponse {
user: PlexUser;
@@ -111,20 +112,54 @@ interface UsersResponse {
};
}
class PlexTvAPI {
interface WatchlistResponse {
MediaContainer: {
totalSize: number;
Metadata?: {
ratingKey: string;
}[];
};
}
interface MetadataResponse {
MediaContainer: {
Metadata: {
ratingKey: string;
type: 'movie' | 'show';
title: string;
Guid: {
id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`;
}[];
}[];
};
}
export interface PlexWatchlistItem {
ratingKey: string;
tmdbId: number;
tvdbId?: number;
type: 'movie' | 'show';
title: string;
}
class PlexTvAPI extends ExternalAPI {
private authToken: string;
private axios: AxiosInstance;
constructor(authToken: string) {
super(
'https://plex.tv',
{},
{
headers: {
'X-Plex-Token': authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('plextv').data,
}
);
this.authToken = authToken;
this.axios = axios.create({
baseURL: 'https://plex.tv',
headers: {
'X-Plex-Token': this.authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
}
public async getDevices(): Promise<PlexDevice[]> {
@@ -252,6 +287,83 @@ class PlexTvAPI {
)) as UsersResponse;
return parsedXml;
}
public async getWatchlist({
offset = 0,
size = 20,
}: { offset?: number; size?: number } = {}): Promise<{
offset: number;
size: number;
totalSize: number;
items: PlexWatchlistItem[];
}> {
try {
const response = await this.axios.get<WatchlistResponse>(
'/library/sections/watchlist/all',
{
params: {
'X-Plex-Container-Start': offset,
'X-Plex-Container-Size': size,
},
baseURL: 'https://metadata.provider.plex.tv',
}
);
const watchlistDetails = await Promise.all(
(response.data.MediaContainer.Metadata ?? []).map(
async (watchlistItem) => {
const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{
baseURL: 'https://metadata.provider.plex.tv',
}
);
const metadata = detailedResponse.MediaContainer.Metadata[0];
const tmdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tmdb')
);
const tvdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tvdb')
);
return {
ratingKey: metadata.ratingKey,
// This should always be set? But I guess it also cannot be?
// We will filter out the 0's afterwards
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
tvdbId: tvdbString
? Number(tvdbString.id.split('//')[1])
: undefined,
title: metadata.title,
type: metadata.type,
};
}
)
);
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
return {
offset,
size,
totalSize: response.data.MediaContainer.totalSize,
items: filteredList,
};
} catch (e) {
logger.error('Failed to retrieve watchlist items', {
label: 'Plex.TV Metadata API',
errorMessage: e.message,
});
return {
offset,
size,
totalSize: 0,
items: [],
};
}
}
}
export default PlexTvAPI;

View File

@@ -1,4 +1,4 @@
import cacheManager from '../lib/cache';
import cacheManager from '@server/lib/cache';
import ExternalAPI from './externalapi';
interface RTSearchResult {

View File

@@ -1,6 +1,7 @@
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
import { DVRSettings } from '../../lib/settings';
import ExternalAPI from '../externalapi';
import ExternalAPI from '@server/api/externalapi';
import type { AvailableCacheIds } from '@server/lib/cache';
import cacheManager from '@server/lib/cache';
import type { DVRSettings } from '@server/lib/settings';
export interface SystemStatus {
version: string;

View File

@@ -1,4 +1,4 @@
import logger from '../../logger';
import logger from '@server/logger';
import ServarrBase from './base';
export interface RadarrMovieOptions {
@@ -69,7 +69,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
return response.data[0];
} catch (e) {
logger.error('Error retrieving movie by TMDb ID', {
logger.error('Error retrieving movie by TMDB ID', {
label: 'Radarr API',
errorMessage: e.message,
tmdbId: id,

View File

@@ -1,4 +1,4 @@
import logger from '../../logger';
import logger from '@server/logger';
import ServarrBase from './base';
interface SonarrSeason {

View File

@@ -1,8 +1,9 @@
import axios, { AxiosInstance } from 'axios';
import type { User } from '@server/entity/User';
import type { TautulliSettings } from '@server/lib/settings';
import logger from '@server/logger';
import type { AxiosInstance } from 'axios';
import axios from 'axios';
import { uniqWith } from 'lodash';
import { User } from '../entity/User';
import { TautulliSettings } from '../lib/settings';
import logger from '../logger';
export interface TautulliHistoryRecord {
date: number;

View File

@@ -1,7 +1,7 @@
import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';
import { sortBy } from 'lodash';
import cacheManager from '../../lib/cache';
import ExternalAPI from '../externalapi';
import {
import type {
TmdbCollection,
TmdbExternalIdResponse,
TmdbGenre,
@@ -92,6 +92,10 @@ class TheMovieDb extends ExternalAPI {
},
{
nodeCache: cacheManager.getCache('tmdb').data,
rateLimit: {
maxRequests: 20,
maxRPS: 50,
},
}
);
this.region = region;
@@ -129,7 +133,13 @@ class TheMovieDb extends ExternalAPI {
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
try {
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
params: { query, page, include_adult: includeAdult, language, year },
params: {
query,
page,
include_adult: includeAdult,
language,
primary_release_year: year,
},
});
return data;
@@ -186,7 +196,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch person details: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
}
};
@@ -208,7 +218,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(
`[TMDb] Failed to fetch person combined credits: ${e.message}`
`[TMDB] Failed to fetch person combined credits: ${e.message}`
);
}
};
@@ -235,7 +245,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch movie details: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
}
};
@@ -261,7 +271,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
}
};
@@ -287,7 +297,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
}
};
@@ -313,7 +323,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
}
}
@@ -339,7 +349,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
}
}
@@ -365,7 +375,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch movies by keyword: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
}
}
@@ -392,7 +402,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(
`[TMDb] Failed to fetch TV recommendations: ${e.message}`
`[TMDB] Failed to fetch TV recommendations: ${e.message}`
);
}
}
@@ -416,7 +426,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch TV similar: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch TV similar: ${e.message}`);
}
}
@@ -449,7 +459,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
}
};
@@ -482,7 +492,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch discover TV: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch discover TV: ${e.message}`);
}
};
@@ -508,7 +518,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch upcoming movies: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
}
};
@@ -535,7 +545,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
}
};
@@ -558,7 +568,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
}
};
@@ -581,7 +591,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
}
};
@@ -613,7 +623,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to find by external ID: ${e.message}`);
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
}
}
@@ -651,7 +661,7 @@ class TheMovieDb extends ExternalAPI {
throw new Error(`No movie or show returned from API for ID ${imdbId}`);
} catch (e) {
throw new Error(
`[TMDb] Failed to find media using external IMDb ID: ${e.message}`
`[TMDB] Failed to find media using external IMDb ID: ${e.message}`
);
}
}
@@ -681,7 +691,7 @@ class TheMovieDb extends ExternalAPI {
throw new Error(`No show returned from API for ID ${tvdbId}`);
} catch (e) {
throw new Error(
`[TMDb] Failed to get TV show using the external TVDB ID: ${e.message}`
`[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
);
}
}
@@ -705,7 +715,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch collection: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
}
}
@@ -721,7 +731,7 @@ class TheMovieDb extends ExternalAPI {
return regions;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch countries: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`);
}
}
@@ -737,7 +747,7 @@ class TheMovieDb extends ExternalAPI {
return languages;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch langauges: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`);
}
}
@@ -749,7 +759,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch movie studio: ${e.message}`);
}
}
@@ -759,7 +769,7 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch TV network: ${e.message}`);
}
}
@@ -810,7 +820,7 @@ class TheMovieDb extends ExternalAPI {
return movieGenres;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch movie genres: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch movie genres: ${e.message}`);
}
}
@@ -861,7 +871,7 @@ class TheMovieDb extends ExternalAPI {
return tvGenres;
} catch (e) {
throw new Error(`[TMDb] Failed to fetch TV genres: ${e.message}`);
throw new Error(`[TMDB] Failed to fetch TV genres: ${e.message}`);
}
}
}

View File

@@ -2,6 +2,7 @@ export enum MediaRequestStatus {
PENDING = 1,
APPROVED,
DECLINED,
FAILED,
}
export enum MediaType {

View File

@@ -1,4 +1,8 @@
const devConfig = {
import 'reflect-metadata';
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
import { DataSource } from 'typeorm';
const devConfig: DataSourceOptions = {
type: 'sqlite',
database: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
@@ -10,31 +14,30 @@ const devConfig = {
entities: ['server/entity/**/*.ts'],
migrations: ['server/migration/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
cli: {
entitiesDir: 'server/entity',
migrationsDir: 'server/migration',
},
};
const prodConfig = {
const prodConfig: DataSourceOptions = {
type: 'sqlite',
database: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
: 'config/db/db.sqlite3',
synchronize: false,
migrationsRun: false,
logging: false,
enableWAL: true,
entities: ['dist/entity/**/*.js'],
migrations: ['dist/migration/**/*.js'],
migrationsRun: false,
subscribers: ['dist/subscriber/**/*.js'],
cli: {
entitiesDir: 'dist/entity',
migrationsDir: 'dist/migration',
},
};
const finalConfig =
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig;
const dataSource = new DataSource(
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig
);
module.exports = finalConfig;
export const getRepository = <Entity>(
target: EntityTarget<Entity>
): Repository<Entity> => {
return dataSource.getRepository(target);
};
export default dataSource;

View File

@@ -1,3 +1,5 @@
import type { IssueType } from '@server/constants/issue';
import { IssueStatus } from '@server/constants/issue';
import {
Column,
CreateDateColumn,
@@ -7,7 +9,6 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { IssueStatus, IssueType } from '../constants/issue';
import IssueComment from './IssueComment';
import Media from './Media';
import { User } from './User';

View File

@@ -1,21 +1,22 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import type { DownloadingItem } from '@server/lib/downloadtracker';
import downloadTracker from '@server/lib/downloadtracker';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import {
AfterLoad,
Column,
CreateDateColumn,
Entity,
getRepository,
In,
Index,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI from '../api/servarr/sonarr';
import { MediaStatus, MediaType } from '../constants/media';
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import Issue from './Issue';
import { MediaRequest } from './MediaRequest';
import Season from './Season';
@@ -36,7 +37,7 @@ class Media {
}
const media = await mediaRepository.find({
tmdbId: In(finalIds),
where: { tmdbId: In(finalIds) },
});
return media;
@@ -55,10 +56,10 @@ class Media {
try {
const media = await mediaRepository.findOne({
where: { tmdbId: id, mediaType },
relations: ['requests', 'issues'],
relations: { requests: true, issues: true },
});
return media;
return media ?? undefined;
} catch (e) {
logger.error(e.message);
return undefined;
@@ -145,6 +146,9 @@ class Media {
public plexUrl?: string;
public plexUrl4k?: string;
public iOSPlexUrl?: string;
public iOSPlexUrl4k?: string;
public tautulliUrl?: string;
public tautulliUrl4k?: string;
@@ -164,6 +168,8 @@ class Media {
this.ratingKey
}`;
this.iOSPlexUrl = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey}&server=${machineId}`;
if (tautulliUrl) {
this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`;
}
@@ -176,6 +182,8 @@ class Media {
this.ratingKey4k
}`;
this.iOSPlexUrl4k = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}&server=${machineId}`;
if (tautulliUrl) {
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
}

View File

@@ -1,3 +1,23 @@
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
import RadarrAPI from '@server/api/servarr/radarr';
import type {
AddSeriesOptions,
SonarrSeries,
} from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import TheMovieDb from '@server/api/themoviedb';
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { getRepository } from '@server/datasource';
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
import notificationManager, { Notification } from '@server/lib/notifications';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isEqual, truncate } from 'lodash';
import {
AfterInsert,
@@ -6,30 +26,347 @@ import {
Column,
CreateDateColumn,
Entity,
getRepository,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
RelationCount,
UpdateDateColumn,
} from 'typeorm';
import RadarrAPI, { RadarrMovieOptions } from '../api/servarr/radarr';
import SonarrAPI, {
AddSeriesOptions,
SonarrSeries,
} from '../api/servarr/sonarr';
import TheMovieDb from '../api/themoviedb';
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
import notificationManager, { Notification } from '../lib/notifications';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import Media from './Media';
import SeasonRequest from './SeasonRequest';
import { User } from './User';
export class RequestPermissionError extends Error {}
export class QuotaRestrictedError extends Error {}
export class DuplicateMediaRequestError extends Error {}
export class NoSeasonsAvailableError extends Error {}
type MediaRequestOptions = {
isAutoRequest?: boolean;
};
@Entity()
export class MediaRequest {
public static async request(
requestBody: MediaRequestBody,
user: User,
options: MediaRequestOptions = {}
): Promise<MediaRequest> {
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
let requestUser = user;
if (
requestBody.userId &&
!requestUser.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
throw new RequestPermissionError(
'You do not have permission to modify the request user.'
);
} else if (requestBody.userId) {
requestUser = await userRepository.findOneOrFail({
where: { id: requestBody.userId },
});
}
if (!requestUser) {
throw new Error('User missing from request context.');
}
if (
requestBody.mediaType === MediaType.MOVIE &&
!requestUser.hasPermission(
requestBody.is4k
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
{
type: 'or',
}
)
) {
throw new RequestPermissionError(
`You do not have permission to make ${
requestBody.is4k ? '4K ' : ''
}movie requests.`
);
} else if (
requestBody.mediaType === MediaType.TV &&
!requestUser.hasPermission(
requestBody.is4k
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
: [Permission.REQUEST, Permission.REQUEST_TV],
{
type: 'or',
}
)
) {
throw new RequestPermissionError(
`You do not have permission to make ${
requestBody.is4k ? '4K ' : ''
}series requests.`
);
}
const quotas = await requestUser.getQuota();
if (requestBody.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
throw new QuotaRestrictedError('Movie Quota exceeded.');
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
throw new QuotaRestrictedError('Series Quota exceeded.');
}
const tmdbMedia =
requestBody.mediaType === MediaType.MOVIE
? await tmdb.getMovie({ movieId: requestBody.mediaId })
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
let media = await mediaRepository.findOne({
where: {
tmdbId: requestBody.mediaId,
mediaType: requestBody.mediaType,
},
relations: ['requests'],
});
if (!media) {
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
mediaType: requestBody.mediaType,
});
} else {
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
media.status = MediaStatus.PENDING;
}
if (media.status4k === MediaStatus.UNKNOWN && requestBody.is4k) {
media.status4k = MediaStatus.PENDING;
}
}
const existing = await requestRepository
.createQueryBuilder('request')
.leftJoin('request.media', 'media')
.leftJoinAndSelect('request.requestedBy', 'user')
.where('request.is4k = :is4k', { is4k: requestBody.is4k })
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
.andWhere('media.mediaType = :mediaType', {
mediaType: requestBody.mediaType,
})
.getMany();
if (existing && existing.length > 0) {
// If there is an existing movie request that isn't declined, don't allow a new one.
if (
requestBody.mediaType === MediaType.MOVIE &&
existing[0].status !== MediaRequestStatus.DECLINED
) {
logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id,
mediaType: requestBody.mediaType,
is4k: requestBody.is4k,
label: 'Media Request',
});
throw new DuplicateMediaRequestError(
'Request for this media already exists.'
);
}
// If an existing auto-request for this media exists from the same user,
// don't allow a new one.
if (
existing.find(
(r) => r.requestedBy.id === requestUser.id && r.isAutoRequest
)
) {
throw new DuplicateMediaRequestError(
'Auto-request for this media and user already exists.'
);
}
}
if (requestBody.mediaType === MediaType.MOVIE) {
await mediaRepository.save(media);
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: user.hasPermission(
[
requestBody.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
requestBody.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: user.hasPermission(
[
requestBody.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
requestBody.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? user
: undefined,
is4k: requestBody.is4k,
serverId: requestBody.serverId,
profileId: requestBody.profileId,
rootFolder: requestBody.rootFolder,
tags: requestBody.tags,
isAutoRequest: options.isAutoRequest ?? false,
});
await requestRepository.save(request);
return request;
} else {
const tmdbMediaShow = tmdbMedia as Awaited<
ReturnType<typeof tmdb.getTvShow>
>;
const requestedSeasons =
requestBody.seasons === 'all'
? tmdbMediaShow.seasons
.map((season) => season.season_number)
.filter((sn) => sn > 0)
: (requestBody.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 === requestBody.is4k &&
request.status !== MediaRequestStatus.DECLINED
)
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
// We should also check seasons that are available/partially available but don't have existing requests
if (media.seasons) {
existingSeasons = [
...existingSeasons,
...media.seasons
.filter(
(season) =>
season[requestBody.is4k ? 'status4k' : 'status'] !==
MediaStatus.UNKNOWN
)
.map((season) => season.seasonNumber),
];
}
const finalSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
if (finalSeasons.length === 0) {
throw new NoSeasonsAvailableError('No seasons available to request');
} else if (
quotas.tv.limit &&
finalSeasons.length > (quotas.tv.remaining ?? 0)
) {
throw new QuotaRestrictedError('Series Quota exceeded.');
}
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: user.hasPermission(
[
requestBody.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: user.hasPermission(
[
requestBody.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? user
: undefined,
is4k: requestBody.is4k,
serverId: requestBody.serverId,
profileId: requestBody.profileId,
rootFolder: requestBody.rootFolder,
languageProfileId: requestBody.languageProfileId,
tags: requestBody.tags,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
seasonNumber: sn,
status: user.hasPermission(
[
requestBody.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
requestBody.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
})
),
isAutoRequest: options.isAutoRequest ?? false,
});
await requestRepository.save(request);
return request;
}
}
@PrimaryGeneratedColumn()
public id: number;
@@ -120,6 +457,9 @@ export class MediaRequest {
})
public tags?: number[];
@Column({ default: false })
public isAutoRequest: boolean;
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
@@ -147,6 +487,10 @@ export class MediaRequest {
}
this.sendNotification(media, Notification.MEDIA_PENDING);
if (this.isAutoRequest) {
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
}
}
}
@@ -191,6 +535,14 @@ export class MediaRequest {
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED
);
if (
this.status === MediaRequestStatus.APPROVED &&
autoApproved &&
this.isAutoRequest
) {
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
}
}
}
@@ -207,7 +559,7 @@ export class MediaRequest {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
relations: { requests: true },
});
if (!media) {
logger.error('Media data not found', {
@@ -272,7 +624,7 @@ export class MediaRequest {
const mediaRepository = getRepository(Media);
const fullMedia = await mediaRepository.findOneOrFail({
where: { id: this.media.id },
relations: ['requests'],
relations: { requests: true },
});
if (
@@ -452,10 +804,13 @@ export class MediaRequest {
await mediaRepository.save(media);
})
.catch(async () => {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
const requestRepository = getRepository(MediaRequest);
this.status = MediaRequestStatus.FAILED;
requestRepository.save(this);
logger.warn(
'Something went wrong sending movie request to Radarr, marking status as UNKNOWN',
'Something went wrong sending movie request to Radarr, marking status as FAILED',
{
label: 'Media Request',
requestId: this.id,
@@ -543,7 +898,7 @@ export class MediaRequest {
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
relations: { requests: true },
});
if (!media) {
@@ -670,7 +1025,7 @@ export class MediaRequest {
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
relations: { requests: true },
});
if (!media) {
@@ -685,10 +1040,13 @@ export class MediaRequest {
await mediaRepository.save(media);
})
.catch(async () => {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
const requestRepository = getRepository(MediaRequest);
this.status = MediaRequestStatus.FAILED;
requestRepository.save(this);
logger.warn(
'Something went wrong sending series request to Sonarr, marking status as UNKNOWN',
'Something went wrong sending series request to Sonarr, marking status as FAILED',
{
label: 'Media Request',
requestId: this.id,
@@ -723,6 +1081,7 @@ export class MediaRequest {
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
let event: string | undefined;
let notifyAdmin = true;
let notifySystem = true;
switch (type) {
case Notification.MEDIA_APPROVED:
@@ -736,6 +1095,13 @@ export class MediaRequest {
case Notification.MEDIA_PENDING:
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
break;
case Notification.MEDIA_AUTO_REQUESTED:
event = `${
this.is4k ? '4K ' : ''
}${mediaType} Request Automatically Submitted`;
notifyAdmin = false;
notifySystem = false;
break;
case Notification.MEDIA_AUTO_APPROVED:
event = `${
this.is4k ? '4K ' : ''
@@ -752,6 +1118,7 @@ export class MediaRequest {
media,
request: this,
notifyAdmin,
notifySystem,
notifyUser: notifyAdmin ? undefined : this.requestedBy,
event,
subject: `${movie.title}${
@@ -770,6 +1137,7 @@ export class MediaRequest {
media,
request: this,
notifyAdmin,
notifySystem,
notifyUser: notifyAdmin ? undefined : this.requestedBy,
event,
subject: `${tv.name}${

View File

@@ -1,12 +1,12 @@
import { MediaStatus } from '@server/constants/media';
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { MediaStatus } from '../constants/media';
import Media from './Media';
@Entity()

View File

@@ -1,12 +1,12 @@
import { MediaRequestStatus } from '@server/constants/media';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { MediaRequestStatus } from '../constants/media';
import { MediaRequest } from './MediaRequest';
@Entity()

View File

@@ -1,5 +1,5 @@
import { ISession } from 'connect-typeorm';
import { Index, Column, PrimaryColumn, Entity } from 'typeorm';
import type { ISession } from 'connect-typeorm';
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity()
export class Session implements ISession {

View File

@@ -1,3 +1,13 @@
import { MediaRequestStatus, MediaType } from '@server/constants/media';
import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import PreparedEmail from '@server/lib/email';
import type { PermissionCheckOptions } from '@server/lib/permissions';
import { hasPermission, Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { AfterDate } from '@server/utils/dateHelpers';
import bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
import path from 'path';
@@ -7,8 +17,6 @@ import {
Column,
CreateDateColumn,
Entity,
getRepository,
MoreThan,
Not,
OneToMany,
OneToOne,
@@ -16,17 +24,6 @@ import {
RelationCount,
UpdateDateColumn,
} from 'typeorm';
import { MediaRequestStatus, MediaType } from '../constants/media';
import { UserType } from '../constants/user';
import { QuotaResponse } from '../interfaces/api/userInterfaces';
import PreparedEmail from '../lib/email';
import {
hasPermission,
Permission,
PermissionCheckOptions,
} from '../lib/permissions';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import Issue from './Issue';
import { MediaRequest } from './MediaRequest';
import SeasonRequest from './SeasonRequest';
@@ -255,13 +252,14 @@ export class User {
if (movieQuotaDays) {
movieDate.setDate(movieDate.getDate() - movieQuotaDays);
}
const movieQuotaStartDate = movieDate.toJSON();
const movieQuotaUsed = movieQuotaLimit
? await requestRepository.count({
where: {
requestedBy: this,
createdAt: MoreThan(movieQuotaStartDate),
requestedBy: {
id: this.id,
},
createdAt: AfterDate(movieDate),
type: MediaType.MOVIE,
status: Not(MediaRequestStatus.DECLINED),
},

View File

@@ -1,3 +1,6 @@
import type { NotificationAgentTypes } from '@server/interfaces/api/userSettingsInterfaces';
import { hasNotificationType, Notification } from '@server/lib/notifications';
import { NotificationAgentKey } from '@server/lib/settings';
import {
Column,
Entity,
@@ -5,9 +8,6 @@ import {
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
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)
@@ -57,6 +57,12 @@ export class UserSettings {
@Column({ nullable: true })
public telegramSendSilently?: boolean;
@Column({ nullable: true })
public watchlistSyncMovies?: boolean;
@Column({ nullable: true })
public watchlistSyncTv?: boolean;
@Column({
type: 'text',
nullable: true,

View File

@@ -1,34 +1,37 @@
import PlexAPI from '@server/api/plexapi';
import dataSource, { getRepository } from '@server/datasource';
import { Session } from '@server/entity/Session';
import { User } from '@server/entity/User';
import { startJobs } from '@server/job/schedule';
import notificationManager from '@server/lib/notifications';
import DiscordAgent from '@server/lib/notifications/agents/discord';
import EmailAgent from '@server/lib/notifications/agents/email';
import GotifyAgent from '@server/lib/notifications/agents/gotify';
import LunaSeaAgent from '@server/lib/notifications/agents/lunasea';
import PushbulletAgent from '@server/lib/notifications/agents/pushbullet';
import PushoverAgent from '@server/lib/notifications/agents/pushover';
import SlackAgent from '@server/lib/notifications/agents/slack';
import TelegramAgent from '@server/lib/notifications/agents/telegram';
import WebhookAgent from '@server/lib/notifications/agents/webhook';
import WebPushAgent from '@server/lib/notifications/agents/webpush';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import routes from '@server/routes';
import { getAppVersion } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import express, { NextFunction, Request, Response } from 'express';
import type { NextFunction, Request, Response } from 'express';
import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import session, { Store } from 'express-session';
import type { Store } from 'express-session';
import session 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 PlexAPI from './api/plexapi';
import { Session } from './entity/Session';
import { User } from './entity/User';
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 GotifyAgent from './lib/notifications/agents/gotify';
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');
@@ -40,7 +43,7 @@ const handle = app.getRequestHandler();
app
.prepare()
.then(async () => {
const dbConnection = await createConnection();
const dbConnection = await dataSource.initialize();
// Run migrations in production
if (process.env.NODE_ENV === 'production') {
@@ -51,6 +54,7 @@ app
// Load Settings
const settings = getSettings().load();
restartFlag.initializeSettings(settings.main);
// Migrate library types
if (
@@ -59,8 +63,8 @@ app
) {
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
select: { id: true, plexToken: true },
where: { id: 1 },
});
if (admin) {

View File

@@ -3,3 +3,17 @@ export interface GenreSliderItem {
name: string;
backdrops: string[];
}
export interface WatchlistItem {
ratingKey: string;
tmdbId: number;
mediaType: 'movie' | 'tv';
title: string;
}
export interface WatchlistResponse {
page: number;
totalPages: number;
totalResults: number;
results: WatchlistItem[];
}

View File

@@ -1,5 +1,5 @@
import Issue from '../../entity/Issue';
import { PaginatedResponse } from './common';
import type Issue from '@server/entity/Issue';
import type { PaginatedResponse } from './common';
export interface IssueResultsResponse extends PaginatedResponse {
results: Issue[];

View File

@@ -1,6 +1,6 @@
import type Media from '../../entity/Media';
import { User } from '../../entity/User';
import { PaginatedResponse } from './common';
import type Media from '@server/entity/Media';
import type { User } from '@server/entity/User';
import type { PaginatedResponse } from './common';
export interface MediaResultsResponse extends PaginatedResponse {
results: Media[];

View File

@@ -1,4 +1,4 @@
import { PersonCreditCast, PersonCreditCrew } from '../../models/Person';
import type { PersonCreditCast, PersonCreditCrew } from '@server/models/Person';
export interface PersonCombinedCreditsResponse {
id: number;

View File

@@ -1,4 +1,4 @@
import { PlexSettings } from '../../lib/settings';
import type { PlexSettings } from '@server/lib/settings';
export interface PlexStatus {
settings: PlexSettings;

View File

@@ -1,6 +1,21 @@
import type { MediaType } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import type { PaginatedResponse } from './common';
import type { MediaRequest } from '../../entity/MediaRequest';
export interface RequestResultsResponse extends PaginatedResponse {
results: MediaRequest[];
}
export type MediaRequestBody = {
mediaType: MediaType;
mediaId: number;
tvdbId?: number;
seasons?: number[] | 'all';
is4k?: boolean;
serverId?: number;
profileId?: number;
rootFolder?: string;
languageProfileId?: number;
userId?: number;
tags?: number[];
};

View File

@@ -1,5 +1,5 @@
import { QualityProfile, RootFolder, Tag } from '../../api/servarr/base';
import { LanguageProfile } from '../../api/servarr/sonarr';
import type { QualityProfile, RootFolder, Tag } from '@server/api/servarr/base';
import type { LanguageProfile } from '@server/api/servarr/sonarr';
export interface ServiceCommonServer {
id: number;

View File

@@ -25,6 +25,7 @@ export interface PublicSettingsResponse {
applicationTitle: string;
applicationUrl: string;
hideAvailable: boolean;
hideRequested: boolean;
localLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
@@ -56,4 +57,5 @@ export interface StatusResponse {
commitTag: string;
updateAvailable: boolean;
commitsBehind: number;
restartRequired: boolean;
}

View File

@@ -1,7 +1,7 @@
import Media from '../../entity/Media';
import { MediaRequest } from '../../entity/MediaRequest';
import type { User } from '../../entity/User';
import { PaginatedResponse } from './common';
import type Media from '@server/entity/Media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import type { User } from '@server/entity/User';
import type { PaginatedResponse } from './common';
export interface UserResultsResponse extends PaginatedResponse {
results: User[];
@@ -23,6 +23,7 @@ export interface QuotaResponse {
movie: QuotaStatus;
tv: QuotaStatus;
}
export interface UserWatchDataResponse {
recentlyWatched: Media[];
playCount: number;

View File

@@ -1,4 +1,4 @@
import { NotificationAgentKey } from '../../lib/settings';
import type { NotificationAgentKey } from '@server/lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
@@ -14,6 +14,8 @@ export interface UserSettingsGeneralResponse {
globalMovieQuotaLimit?: number;
globalTvQuotaLimit?: number;
globalTvQuotaDays?: number;
watchlistSyncMovies?: boolean;
watchlistSyncTv?: boolean;
}
export type NotificationAgentTypes = Record<NotificationAgentKey, number>;

View File

@@ -1,10 +1,12 @@
import downloadTracker from '@server/lib/downloadtracker';
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
import { radarrScanner } from '@server/lib/scanners/radarr';
import { sonarrScanner } from '@server/lib/scanners/sonarr';
import type { JobId } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import watchlistSync from '@server/lib/watchlistsync';
import logger from '@server/logger';
import schedule from 'node-schedule';
import downloadTracker from '../lib/downloadtracker';
import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex';
import { radarrScanner } from '../lib/scanners/radarr';
import { sonarrScanner } from '../lib/scanners/sonarr';
import { getSettings, JobId } from '../lib/settings';
import logger from '../logger';
interface ScheduledJob {
id: JobId;
@@ -53,6 +55,20 @@ export const startJobs = (): void => {
cancelFn: () => plexFullScanner.cancel(),
});
// Run watchlist sync every 5 minutes
scheduledJobs.push({
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
interval: 'long',
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
label: 'Jobs',
});
watchlistSync.syncWatchlist();
}),
});
// Run full radarr scan every 24 hours
scheduledJobs.push({
id: 'radarr-scan',

View File

@@ -6,7 +6,8 @@ export type AvailableCacheIds =
| 'sonarr'
| 'rt'
| 'github'
| 'plexguid';
| 'plexguid'
| 'plextv';
const DEFAULT_TTL = 300;
const DEFAULT_CHECK_PERIOD = 120;
@@ -58,6 +59,10 @@ class CacheManager {
stdTtl: 86400 * 7, // 1 week cache
checkPeriod: 60 * 30,
}),
plextv: new Cache('plextv', 'Plex TV', {
stdTtl: 86400 * 7, // 1 week cache
checkPeriod: 60,
}),
};
public getCache(id: AvailableCacheIds): Cache {

View File

@@ -1,9 +1,9 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaType } from '@server/constants/media';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { uniqWith } from 'lodash';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI from '../api/servarr/sonarr';
import { MediaType } from '../constants/media';
import logger from '../logger';
import { getSettings } from './settings';
export interface DownloadingItem {
mediaType: MediaType;

View File

@@ -1,7 +1,8 @@
import type { NotificationAgentEmail } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import Email from 'email-templates';
import nodemailer from 'nodemailer';
import { URL } from 'url';
import { getSettings, NotificationAgentEmail } from '../settings';
import { openpgpEncrypt } from './openpgpEncrypt';
class PreparedEmail extends Email {

View File

@@ -1,7 +1,8 @@
import logger from '@server/logger';
import { randomBytes } from 'crypto';
import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream';
import logger from '../../logger';
import type { TransformCallback } from 'stream';
import { Transform } from 'stream';
interface EncryptorOptions {
signingKey?: string;
@@ -26,7 +27,7 @@ class PGPEncryptor extends Transform {
// just save the whole message
_transform = (
chunk: any,
chunk: Uint8Array,
_encoding: BufferEncoding,
callback: TransformCallback
): void => {
@@ -184,6 +185,9 @@ class PGPEncryptor extends Transform {
}
export const openpgpEncrypt = (options: EncryptorOptions) => {
// Disabling this line because I don't want to fix it but I am tired
// of seeing the lint warning
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (mail: any, callback: () => unknown): void {
if (!options.encryptionKeys.length) {
setImmediate(callback);

View File

@@ -1,14 +1,15 @@
import { Notification } from '..';
import type Issue from '../../../entity/Issue';
import IssueComment from '../../../entity/IssueComment';
import Media from '../../../entity/Media';
import { MediaRequest } from '../../../entity/MediaRequest';
import { User } from '../../../entity/User';
import { NotificationAgentConfig } from '../../settings';
import type Issue from '@server/entity/Issue';
import type IssueComment from '@server/entity/IssueComment';
import type Media from '@server/entity/Media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import type { User } from '@server/entity/User';
import type { NotificationAgentConfig } from '@server/lib/settings';
import type { Notification } from '..';
export interface NotificationPayload {
event?: string;
subject: string;
notifySystem: boolean;
notifyAdmin: boolean;
notifyUser?: User;
media?: Media;

View File

@@ -1,19 +1,17 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import type { NotificationAgentDiscord } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { getRepository } from 'typeorm';
import {
hasNotificationType,
Notification,
shouldSendAdminNotification,
} from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import {
getSettings,
NotificationAgentDiscord,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
enum EmbedColors {
DEFAULT = 0,
@@ -245,7 +243,10 @@ class DiscordAgent
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
if (
!payload.notifySystem ||
!hasNotificationType(type, settings.types ?? 0)
) {
return true;
}

View File

@@ -1,18 +1,16 @@
import { EmailOptions } from 'email-templates';
import { IssueType, IssueTypeName } from '@server/constants/issue';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import PreparedEmail from '@server/lib/email';
import type { NotificationAgentEmail } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import type { EmailOptions } from 'email-templates';
import path from 'path';
import { getRepository } from 'typeorm';
import { Notification, shouldSendAdminNotification } from '..';
import { IssueType, IssueTypeName } from '../../../constants/issue';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import PreparedEmail from '../../email';
import {
getSettings,
NotificationAgentEmail,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
@@ -83,6 +81,11 @@ class EmailAgent
is4k ? 'in 4K ' : ''
}is pending approval:`;
break;
case Notification.MEDIA_AUTO_REQUESTED:
body = `A new request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}was automatically submitted:`;
break;
case Notification.MEDIA_APPROVED:
body = `Your request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''

View File

@@ -1,15 +1,17 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import type { NotificationAgentGotify } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import logger from '../../../logger';
import { getSettings, NotificationAgentGotify } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface GotifyPayload {
title: string;
message: string;
priority: number;
extras: any;
extras: Record<string, unknown>;
}
class GotifyAgent
@@ -115,7 +117,10 @@ class GotifyAgent
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
if (
!payload.notifySystem ||
!hasNotificationType(type, settings.types ?? 0)
) {
return true;
}

View File

@@ -1,10 +1,12 @@
import { IssueStatus, IssueType } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import type { NotificationAgentLunaSea } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { IssueStatus, IssueType } from '../../../constants/issue';
import { MediaStatus } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentLunaSea } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
class LunaSeaAgent
extends BaseAgent<NotificationAgentLunaSea>
@@ -85,7 +87,10 @@ class LunaSeaAgent
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
if (
!payload.notifySystem ||
!hasNotificationType(type, settings.types ?? 0)
) {
return true;
}

View File

@@ -1,19 +1,18 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import type { NotificationAgentPushbullet } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { getRepository } from 'typeorm';
import {
hasNotificationType,
Notification,
shouldSendAdminNotification,
} from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import {
getSettings,
NotificationAgentKey,
NotificationAgentPushbullet,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface PushbulletPayload {
type: string;
@@ -54,6 +53,12 @@ class PushbulletAgent
let status = '';
switch (type) {
case Notification.MEDIA_AUTO_REQUESTED:
status =
payload.media?.status === MediaStatus.PENDING
? 'Pending Approval'
: 'Processing';
break;
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
@@ -106,6 +111,7 @@ class PushbulletAgent
// Send system notification
if (
payload.notifySystem &&
hasNotificationType(type, settings.types ?? 0) &&
settings.enabled &&
settings.options.accessToken

View File

@@ -1,19 +1,18 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import type { NotificationAgentPushover } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { getRepository } from 'typeorm';
import {
hasNotificationType,
Notification,
shouldSendAdminNotification,
} from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import {
getSettings,
NotificationAgentKey,
NotificationAgentPushover,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface PushoverPayload {
token: string;
@@ -63,6 +62,12 @@ class PushoverAgent
let status = '';
switch (type) {
case Notification.MEDIA_AUTO_REQUESTED:
status =
payload.media?.status === MediaStatus.PENDING
? 'Pending Approval'
: 'Processing';
break;
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
@@ -137,6 +142,7 @@ class PushoverAgent
// Send system notification
if (
payload.notifySystem &&
hasNotificationType(type, settings.types ?? 0) &&
settings.enabled &&
settings.options.accessToken &&

View File

@@ -1,9 +1,11 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import type { NotificationAgentSlack } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import logger from '../../../logger';
import { getSettings, NotificationAgentSlack } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface EmbedField {
type: 'plain_text' | 'mrkdwn';
@@ -223,7 +225,10 @@ class SlackAgent
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
if (
!payload.notifySystem ||
!hasNotificationType(type, settings.types ?? 0)
) {
return true;
}

View File

@@ -1,19 +1,18 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import type { NotificationAgentTelegram } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { getRepository } from 'typeorm';
import {
hasNotificationType,
Notification,
shouldSendAdminNotification,
} from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import {
getSettings,
NotificationAgentKey,
NotificationAgentTelegram,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface TelegramMessagePayload {
text: string;
@@ -81,6 +80,12 @@ class TelegramAgent
let status = '';
switch (type) {
case Notification.MEDIA_AUTO_REQUESTED:
status =
payload.media?.status === MediaStatus.PENDING
? 'Pending Approval'
: 'Processing';
break;
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
@@ -159,6 +164,7 @@ class TelegramAgent
// Send system notification
if (
payload.notifySystem &&
hasNotificationType(type, settings.types ?? 0) &&
settings.options.chatId
) {

View File

@@ -1,11 +1,13 @@
import { IssueStatus, IssueType } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import type { NotificationAgentWebhook } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { get } from 'lodash';
import { hasNotificationType, Notification } from '..';
import { IssueStatus, IssueType } from '../../../constants/issue';
import { MediaStatus } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentWebhook } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
type KeyMapFunction = (
payload: NotificationPayload,
@@ -162,7 +164,10 @@ class WebhookAgent
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
if (
!payload.notifySystem ||
!hasNotificationType(type, settings.types ?? 0)
) {
return true;
}

View File

@@ -1,17 +1,15 @@
import { getRepository } from 'typeorm';
import { IssueType, IssueTypeName } from '@server/constants/issue';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
import type { NotificationAgentConfig } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import webpush from 'web-push';
import { Notification, shouldSendAdminNotification } from '..';
import { IssueType, IssueTypeName } from '../../../constants/issue';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
import logger from '../../../logger';
import {
getSettings,
NotificationAgentConfig,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface PushNotificationPayload {
notificationType: string;
@@ -59,6 +57,11 @@ class WebPushAgent
case Notification.TEST_NOTIFICATION:
message = payload.message;
break;
case Notification.MEDIA_AUTO_REQUESTED:
message = `Automatically submitted a new ${
is4k ? '4K ' : ''
}${mediaType} request.`;
break;
case Notification.MEDIA_APPROVED:
message = `Your ${
is4k ? '4K ' : ''
@@ -160,7 +163,7 @@ class WebPushAgent
true)
) {
const notifySubs = await userPushSubRepository.find({
where: { user: payload.notifyUser.id },
where: { user: { id: payload.notifyUser.id } },
});
pushSubs.push(...notifySubs);

View File

@@ -1,6 +1,6 @@
import { User } from '../../entity/User';
import logger from '../../logger';
import { Permission } from '../permissions';
import type { User } from '@server/entity/User';
import { Permission } from '@server/lib/permissions';
import logger from '@server/logger';
import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification {
@@ -16,6 +16,7 @@ export enum Notification {
ISSUE_COMMENT = 512,
ISSUE_RESOLVED = 1024,
ISSUE_REOPENED = 2048,
MEDIA_AUTO_REQUESTED = 4096,
}
export const hasNotificationType = (

View File

@@ -1,7 +1,6 @@
export enum Permission {
NONE = 0,
ADMIN = 2,
MANAGE_SETTINGS = 4,
MANAGE_USERS = 8,
MANAGE_REQUESTS = 16,
REQUEST = 32,
@@ -22,6 +21,11 @@ export enum Permission {
MANAGE_ISSUES = 1048576,
VIEW_ISSUES = 2097152,
CREATE_ISSUES = 4194304,
AUTO_REQUEST = 8388608,
AUTO_REQUEST_MOVIE = 16777216,
AUTO_REQUEST_TV = 33554432,
RECENT_VIEW = 67108864,
WATCHLIST_VIEW = 134217728,
}
export interface PermissionCheckOptions {

View File

@@ -1,12 +1,12 @@
import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import Season from '@server/entity/Season';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import AsyncLock from '@server/utils/asyncLock';
import { randomUUID } from 'crypto';
import { getRepository } from 'typeorm';
import TheMovieDb from '../../api/themoviedb';
import { MediaStatus, MediaType } from '../../constants/media';
import Media from '../../entity/Media';
import Season from '../../entity/Season';
import logger from '../../logger';
import AsyncLock from '../../utils/asyncLock';
import { getSettings } from '../settings';
// Default scan rates (can be overidden)
const BUNDLE_SIZE = 20;
@@ -210,7 +210,7 @@ class BaseScanner<T> {
}
/**
* processShow takes a TMDb ID and an array of ProcessableSeasons, which
* processShow takes a TMDB ID and an array of ProcessableSeasons, which
* should include the total episodes a sesaon has + the total available
* episodes that each season currently has. Unlike processMovie, this method
* does not take an `is4k` option. We handle both the 4k _and_ non 4k status

View File

@@ -1,17 +1,20 @@
import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import animeList from '../../../api/animelist';
import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../../api/plexapi';
import { TmdbTvDetails } from '../../../api/themoviedb/interfaces';
import { User } from '../../../entity/User';
import cacheManager from '../../cache';
import { getSettings, Library } from '../../settings';
import BaseScanner, {
import animeList from '@server/api/animelist';
import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi';
import PlexAPI from '@server/api/plexapi';
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import cacheManager from '@server/lib/cache';
import type {
MediaIds,
ProcessableSeason,
RunnableScanner,
StatusBase,
} from '../baseScanner';
} from '@server/lib/scanners/baseScanner';
import BaseScanner from '@server/lib/scanners/baseScanner';
import type { Library } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import { uniqWith } from 'lodash';
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
@@ -59,8 +62,8 @@ class PlexScanner
try {
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
select: { id: true, plexToken: true },
where: { id: 1 },
});
if (!admin) {
@@ -141,7 +144,9 @@ class PlexScanner
'info'
);
} catch (e) {
this.log('Scan interrupted', 'error', { errorMessage: e.message });
this.log('Scan interrupted', 'error', {
errorMessage: e.message,
});
} finally {
this.endRun(sessionId);
}
@@ -369,7 +374,7 @@ class PlexScanner
}
});
// If we got an IMDb ID, but no TMDb ID, lookup the TMDb ID with the IMDb ID
// If we got an IMDb ID, but no TMDB ID, lookup the TMDB ID with the IMDb ID
if (mediaIds.imdbId && !mediaIds.tmdbId) {
const tmdbMedia = await this.tmdb.getMediaByImdbId({
imdbId: mediaIds.imdbId,
@@ -390,7 +395,7 @@ class PlexScanner
});
mediaIds.tmdbId = tmdbMedia.id;
}
// Check if the agent is TMDb
// Check if the agent is TMDB
} else if (plexitem.guid.match(tmdbRegex)) {
const tmdbMatch = plexitem.guid.match(tmdbRegex);
if (tmdbMatch) {
@@ -409,7 +414,7 @@ class PlexScanner
mediaIds.tvdbId = Number(matchedtvdb[1]);
mediaIds.tmdbId = show.id;
}
// Check if the agent (for shows) is TMDb
// Check if the agent (for shows) is TMDB
} else if (plexitem.guid.match(tmdbShowRegex)) {
const matchedtmdb = plexitem.guid.match(tmdbShowRegex);
if (matchedtmdb) {
@@ -484,10 +489,10 @@ class PlexScanner
}
if (!mediaIds.tmdbId) {
throw new Error('Unable to find TMDb ID');
throw new Error('Unable to find TMDB ID');
}
// We check above if we have the TMDb ID, so we can safely assert the type below
// We check above if we have the TMDB ID, so we can safely assert the type below
return mediaIds as MediaIds;
}

View File

@@ -1,7 +1,13 @@
import type { RadarrMovie } from '@server/api/servarr/radarr';
import RadarrAPI from '@server/api/servarr/radarr';
import type {
RunnableScanner,
StatusBase,
} from '@server/lib/scanners/baseScanner';
import BaseScanner from '@server/lib/scanners/baseScanner';
import type { RadarrSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import { uniqWith } from 'lodash';
import RadarrAPI, { RadarrMovie } from '../../../api/servarr/radarr';
import { getSettings, RadarrSettings } from '../../settings';
import BaseScanner, { RunnableScanner, StatusBase } from '../baseScanner';
type SyncStatus = StatusBase & {
currentServer: RadarrSettings;

View File

@@ -1,14 +1,17 @@
import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import SonarrAPI, { SonarrSeries } from '../../../api/servarr/sonarr';
import { TmdbTvDetails } from '../../../api/themoviedb/interfaces';
import Media from '../../../entity/Media';
import { getSettings, SonarrSettings } from '../../settings';
import BaseScanner, {
import type { SonarrSeries } from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import type {
ProcessableSeason,
RunnableScanner,
StatusBase,
} from '../baseScanner';
} from '@server/lib/scanners/baseScanner';
import BaseScanner from '@server/lib/scanners/baseScanner';
import type { SonarrSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import { uniqWith } from 'lodash';
type SyncStatus = StatusBase & {
currentServer: SonarrSettings;

View File

@@ -1,5 +1,5 @@
import TheMovieDb from '../api/themoviedb';
import {
import TheMovieDb from '@server/api/themoviedb';
import type {
TmdbMovieDetails,
TmdbMovieResult,
TmdbPersonDetails,
@@ -9,13 +9,17 @@ import {
TmdbSearchTvResponse,
TmdbTvDetails,
TmdbTvResult,
} from '../api/themoviedb/interfaces';
} from '@server/api/themoviedb/interfaces';
import {
mapMovieDetailsToResult,
mapPersonDetailsToResult,
mapTvDetailsToResult,
} from '../models/Search';
import { isMovie, isMovieDetails, isTvDetails } from '../utils/typeHelpers';
} from '@server/models/Search';
import {
isMovie,
isMovieDetails,
isTvDetails,
} from '@server/utils/typeHelpers';
interface SearchProvider {
pattern: RegExp;

View File

@@ -94,6 +94,7 @@ export interface MainSettings {
tv: Quota;
};
hideAvailable: boolean;
hideRequested: boolean;
localLogin: boolean;
newPlexLogin: boolean;
region: string;
@@ -111,6 +112,7 @@ interface FullPublicSettings extends PublicSettings {
applicationTitle: string;
applicationUrl: string;
hideAvailable: boolean;
hideRequested: boolean;
localLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
@@ -243,6 +245,7 @@ interface JobSettings {
export type JobId =
| 'plex-recently-added-scan'
| 'plex-full-scan'
| 'plex-watchlist-sync'
| 'radarr-scan'
| 'sonarr-scan'
| 'download-sync'
@@ -286,6 +289,7 @@ class Settings {
tv: {},
},
hideAvailable: false,
hideRequested: false,
localLogin: true,
newPlexLogin: true,
region: '',
@@ -398,6 +402,9 @@ class Settings {
'plex-full-scan': {
schedule: '0 0 3 * * *',
},
'plex-watchlist-sync': {
schedule: '0 */10 * * * *',
},
'radarr-scan': {
schedule: '0 0 4 * * *',
},
@@ -475,6 +482,7 @@ class Settings {
applicationTitle: this.data.main.applicationTitle,
applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable,
hideRequested: this.data.main.hideRequested,
localLogin: this.data.main.localLogin,
movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault

163
server/lib/watchlistsync.ts Normal file
View File

@@ -0,0 +1,163 @@
import PlexTvAPI from '@server/api/plextv';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import {
DuplicateMediaRequestError,
MediaRequest,
NoSeasonsAvailableError,
QuotaRestrictedError,
RequestPermissionError,
} from '@server/entity/MediaRequest';
import { User } from '@server/entity/User';
import logger from '@server/logger';
import { Permission } from './permissions';
class WatchlistSync {
public async syncWatchlist() {
const userRepository = getRepository(User);
// Get users who actually have plex tokens
const users = await userRepository
.createQueryBuilder('user')
.addSelect('user.plexToken')
.leftJoinAndSelect('user.settings', 'settings')
.where("user.plexToken != ''")
.getMany();
for (const user of users) {
await this.syncUserWatchlist(user);
}
}
private async syncUserWatchlist(user: User) {
if (!user.plexToken) {
logger.warn('Skipping user watchlist sync for user without plex token', {
label: 'Plex Watchlist Sync',
user: user.displayName,
});
return;
}
if (
!user.hasPermission(
[
Permission.AUTO_REQUEST,
Permission.AUTO_REQUEST_MOVIE,
Permission.AUTO_APPROVE_TV,
],
{ type: 'or' }
)
) {
return;
}
if (
!user.settings?.watchlistSyncMovies &&
!user.settings?.watchlistSyncTv
) {
// Skip sync if user settings have it disabled
return;
}
const plexTvApi = new PlexTvAPI(user.plexToken);
const response = await plexTvApi.getWatchlist({ size: 200 });
const mediaItems = await Media.getRelatedMedia(
response.items.map((i) => i.tmdbId)
);
const unavailableItems = response.items.filter(
// If we can find watchlist items in our database that are also available, we should exclude them
(i) =>
!mediaItems.find(
(m) =>
m.tmdbId === i.tmdbId &&
((m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') ||
(m.mediaType === 'tv' && m.status === MediaStatus.AVAILABLE))
)
);
await Promise.all(
unavailableItems.map(async (mediaItem) => {
try {
logger.info("Creating media request from user's Plex Watchlist", {
label: 'Watchlist Sync',
userId: user.id,
mediaTitle: mediaItem.title,
});
if (mediaItem.type === 'show' && !mediaItem.tvdbId) {
throw new Error('Missing TVDB ID from Plex Metadata');
}
// Check if they have auto-request permissons and watchlist sync
// enabled for the media type
if (
((!user.hasPermission(
[Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_MOVIE],
{ type: 'or' }
) ||
!user.settings?.watchlistSyncMovies) &&
mediaItem.type === 'movie') ||
((!user.hasPermission(
[Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_TV],
{ type: 'or' }
) ||
!user.settings?.watchlistSyncTv) &&
mediaItem.type === 'show')
) {
return;
}
await MediaRequest.request(
{
mediaId: mediaItem.tmdbId,
mediaType:
mediaItem.type === 'show' ? MediaType.TV : MediaType.MOVIE,
seasons: mediaItem.type === 'show' ? 'all' : undefined,
tvdbId: mediaItem.tvdbId,
is4k: false,
},
user,
{ isAutoRequest: true }
);
} catch (e) {
if (!(e instanceof Error)) {
return;
}
switch (e.constructor) {
// During watchlist sync, these errors aren't necessarily
// a problem with Overseerr. Since we are auto syncing these constantly, it's
// possible they are unexpectedly at their quota limit, for example. So we'll
// instead log these as debug messages.
case RequestPermissionError:
case DuplicateMediaRequestError:
case QuotaRestrictedError:
case NoSeasonsAvailableError:
logger.debug('Failed to create media request from watchlist', {
label: 'Watchlist Sync',
userId: user.id,
mediaTitle: mediaItem.title,
errorMessage: e.message,
});
break;
default:
logger.error('Failed to create media request from watchlist', {
label: 'Watchlist Sync',
userId: user.id,
mediaTitle: mediaItem.title,
errorMessage: e.message,
});
}
}
})
);
}
}
const watchlistSync = new WatchlistSync();
export default watchlistSync;

View File

@@ -26,7 +26,7 @@ const hformat = winston.format.printf(
);
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'debug',
level: process.env.LOG_LEVEL?.toLowerCase() || 'debug',
format: winston.format.combine(
winston.format.splat(),
winston.format.timestamp(),

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