Compare commits

...

196 Commits

Author SHA1 Message Date
dd060606
bcd2bb7c96 fix: lint issues 2022-09-28 15:55:56 +02:00
dd060606
5a72f5f86e Merge branch 'develop' into features/deleteMediaFile 2022-09-14 14:58:37 +02:00
Fallenbagel
e0f9a6e12f Merge pull request #219 from notfakie/develop
Remove failing ci job & update with upstream & fix play on button
2022-09-13 02:31:42 +05:00
notfakie
05139717d1 Merge remote-tracking branch 'overseerr/develop' into develop 2022-09-12 19:19:57 +12:00
renovate[bot]
f20ba3fc2e fix(deps): pin dependency cronstrue to 2.11.0 (#3018) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-12 13:11:21 +09:00
Danshil Kokil Mungur
30141f76e0 feat(logs): add search filter (#2505)
* feat(logs): add search filter

* refactor(logs): move loading spinner inside log viewer

Inputting text in the search bar on the logs page would refresh the page
losing focus on the search bar.
This moves the loading spinner inside the log viewer, so that it is not
as disruptive as it would

* fix(logs): escape string for search filter

* chore: rebase

* fix(logs): suggested changes
2022-09-12 02:21:16 +00:00
Brandon Cohen
87825a0e05 feat: pull down to refresh (#2908)
* feat: pull down to refresh functionality

Custom pull down to refresh added to replace the default browser pull down to refresh. This will
allow you to manually reload the page if you are using it as a PWA.

* test: update test to check api call correctly

changed api call for test and made sure it pulls down all the way to trigger refresh

* fix: changed positioning of pull to refresh

Refresh indicator now has absolute positioning and will prevent the top edge from pulling down.
2022-09-12 02:07:37 +00:00
Danshil Kokil Mungur
99fc9a2da0 feat(jobs): show current job frequency in edit modal (#3008)
* fix(jobs): reset job schedule edit modal values when closed

* feat(jobs): show job's current frequency

* fix(jobs): reset job schedule edit modal values when cancelled

* chore: rebase

* refactor(jobs): use reducer instead of several react states

* fix(jobs): reset modal state when opening instead of closing the modal

This prevents the modal state from glitching when saving/closing the modal

* feat(jobs): parse job schedule cron string

unavailable locale will fallback to english
2022-09-12 01:14:27 +00:00
notfakie
6dbb99e0b6 fix: only request Tautulli watch data for Plex media servers (to avoid error messages in logs) 2022-09-11 13:47:37 +12:00
notfakie
3b0c0915fb fix: fix play on Jellyfin/Emby button after previous merge 2022-09-11 13:32:13 +12:00
notfakie
5f7e7eef11 fix: remove failing ci job that builds a test copy to a private repo 2022-09-11 13:01:29 +12:00
notfakie
2dd3925e92 Merge remote-tracking branch 'overseerr/develop' into develop 2022-09-11 13:01:09 +12:00
Weblate (bot)
611ceeb5f4 feat(lang): translations update from Hosted Weblate (#3006)
* feat(lang): translated using Weblate (Albanian)

Currently translated at 95.0% (1063 of 1118 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/sq/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1118 of 1118 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 94.3% (1055 of 1118 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 94.3% (1055 of 1118 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 88.5% (990 of 1118 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/cs/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 92.1% (1030 of 1118 strings)

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

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

Currently translated at 100.0% (1118 of 1118 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 (Croatian)

Currently translated at 4.7% (53 of 1118 strings)

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

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

Co-authored-by: Denis Çerri <deniscerri3@gmail.com>
Co-authored-by: Pollyi <weblate.ntxx4@simplelogin.co>
Co-authored-by: Smexhy <smexhy@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: lpispek <lpispek@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
2022-09-10 14:59:40 -07:00
Fallenbagel
0636ff83a2 Merge pull request #218 from sambartik/merge-english-translations
refactor(en.json): merge translation keys from upstream overseerr
2022-09-08 20:08:39 +05:00
Samuel Bartík
aa005149be refactor(en.json): merge translation keys from upstream overseerr
Also fixes a cypress job error
2022-09-08 16:56:31 +02:00
Fallenbagel
13130188fc Merge pull request #217 from sambartik/fix-dependencies
fix(deps): do not list email-validator as a devDependency
2022-09-07 21:49:16 +05:00
Samuel Bartík
8724058aa5 fix(import statement): import statement 2022-09-07 18:05:59 +02:00
Samuel Bartík
94513425be style: fix formatting 2022-09-07 16:30:45 +02:00
Samuel Bartík
323086db09 refactor: automatic eslint fix 2022-09-07 15:45:08 +02:00
Samuel Bartík
9518cb3635 fix(deps): do not list email-validator as a devDependency
Because it was listed as a devDependency it got removed during a container build process causing module not found error.
2022-09-07 15:14:04 +02:00
Fallenbagel
b66f12a0e1 Merge pull request #216 from notfakie/develop
Merge remote-tracking branch 'overseerr/develop' into 'jellyseerr/develop'
2022-09-07 12:14:27 +05:00
notfakie
e9eba96f5a Merge remote-tracking branch 'overseerr/develop' into develop 2022-09-07 18:24:01 +12:00
Ryan Cohen
14280c5437 fix: scroll restoration (#3005) 2022-09-06 13:27:13 +09:00
Weblate (bot)
867286996b feat(lang): translations update from Hosted Weblate (#2999)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 54.8% (613 of 1118 strings)

feat(lang): translated using Weblate (Serbian)

Currently translated at 54.8% (613 of 1118 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: lpispek <lpispek@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.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 100.0% (1118 of 1118 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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 (Catalan)

Currently translated at 100.0% (1118 of 1118 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 92.3% (1033 of 1118 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Eric <hamburger1024@firemail.cc>
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/zh_Hans/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 100.0% (1118 of 1118 strings)

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

Currently translated at 99.8% (1116 of 1118 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 99.8% (1117 of 1119 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/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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 (Norwegian Bokmål)

Currently translated at 100.0% (1118 of 1118 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: lpispek <lpispek@gmail.com>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Eric <hamburger1024@firemail.cc>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-09-03 10:27:16 -07:00
TheCatLady
03d5e56678 fix(ui): hide 'Recently Added' & 'Recent Requests' sliders when empty (#2190)
* fix(ui): hide 'Recently Added' & 'Recent Requests' sliders when empty

* fix(ui): hide 'errored' sliders too

* fix: type import

* fix: remove unneeded React import

* fix: missing TmdbTitleCard props

* refactor: remove isEmpty param for never-empty sliders

* fix: display empty watchlist message if autorequest enabled

* fix: pr suggestion

* fix(lang): remove no-longer-needed string
2022-08-30 23:51:55 +00:00
TheCatLady
410ad0d4b4 fix: failure to load SearchByNameModal (#3000) 2022-08-31 08:29:41 +09:00
TheCatLady
23f93e311d fix: do not display 'Request More' button if no requestable seasons (#2998) 2022-08-30 19:14:03 +09:00
Weblate (bot)
2950cf4438 feat(lang): translations update from Hosted Weblate (#2971)
* feat(lang): translated using Weblate (Greek)

Currently translated at 68.6% (768 of 1119 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 58.3% (653 of 1119 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 (Serbian)

Currently translated at 54.4% (609 of 1119 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/sr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 92.0% (1030 of 1119 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.1% (1110 of 1119 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.1% (1110 of 1119 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.1% (1110 of 1119 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.1% (1110 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1115 of 1115 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mathieu <math_du_88@yahoo.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

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

Currently translated at 85.7% (959 of 1119 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.2% (1111 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1115 of 1115 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 92.8% (1035 of 1115 strings)

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

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

Currently translated at 87.9% (984 of 1119 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/cs/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 94.7% (1060 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Polish)

Currently translated at 95.5% (1065 of 1115 strings)

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 92.7% (1038 of 1119 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/sv/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 86.9% (973 of 1119 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/ru/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 91.9% (1029 of 1119 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/ar/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 72.0% (806 of 1119 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 91.5% (1025 of 1119 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/da/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 92.4% (1034 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 93.1% (1039 of 1115 strings)

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 87.9% (984 of 1119 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 92.1% (1031 of 1119 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 98.8% (1106 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 99.6% (1111 of 1115 strings)

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

Currently translated at 96.0% (1071 of 1115 strings)

Co-authored-by: Cleiton Carvalho <cleitonsilvacarvalho@gmail.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/pt_BR/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 92.9% (1040 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Italian)

Currently translated at 96.2% (1044 of 1085 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simone <simoneungaro@hotmail.it>
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 99.8% (1117 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 99.8% (1113 of 1115 strings)

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

Currently translated at 99.0% (1104 of 1115 strings)

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

Currently translated at 99.8% (1086 of 1088 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/
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 95.3% (1067 of 1119 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 99.9% (1118 of 1119 strings)

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

Currently translated at 95.8% (1073 of 1119 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 96.5% (1076 of 1115 strings)

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

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

Currently translated at 47.9% (537 of 1119 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/lt/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Cleiton Carvalho <cleitonsilvacarvalho@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-08-29 16:54:33 -07:00
TheCatLady
dbdecb1e0a fix(frontend): only allow 'request as' users w/ request perms (#2991) 2022-08-29 10:33:17 +00:00
renovate[bot]
833f52de56 fix(deps): pin dependency @headlessui/react to v0.0.0-insiders.b301f04 (#2993) [skip ci]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-29 19:22:24 +09: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
dd060606
7d4455ba6b fix: hide remove button when default service is not configured 2022-08-14 12:07:12 +02: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
Fallenbagel
f4c38fa81f Merge pull request #188 from seanzhang98/develop
Update the translations on zh_Hans.json
2022-07-30 01:52:50 +05:00
Sean
a3b620efb3 Update zh_Hans.json 2022-07-25 14:37:31 +08:00
Sean
054da8e456 Update zh_Hans.json
sync en.json and added translation
2022-07-25 14:32:48 +08:00
Danshil Kokil Mungur
6cd0c9b2c8 fix(api): use correct path param type in openapi spec (#2834) 2022-07-22 15:00:59 -04:00
dd060606
2e7458457e feat: add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr 2022-07-22 17:58:33 +02:00
Fallenbagel
b67844a0ee Merge pull request #185 from notfakie/develop
fix: start scheduled jobs on initial admin account setup
2022-07-15 00:16:46 +05:00
notfakie
b08025195e fix: start scheduled jobs on initial admin account setup
issue #170
2022-07-11 17:29:17 +12:00
Fallenbagel
a5cc36c88f Merge pull request #150 from Smexhy/czech-language
feat(language): update czech language
2022-06-24 05:30:57 +05:00
Smexhy
c744e2a9b6 apply review suggestions 2022-06-22 23:59:41 +02:00
Fallenbagel
4a34574a23 Merge pull request #161 from Fallenbagel/Readme-formatting-fix
Readme formatting fix
2022-06-20 23:48:09 +05:00
Fallenbagel
38fc150892 Readme-formatting-fix 2022-06-20 23:47:32 +05:00
Fallenbagel
6e2cf2f80e Merge pull request #148 from Fallenbagel/README-update
docs(readme): added information about upcoming/unique features
2022-06-20 23:46:49 +05:00
Fallenbagel
4ccc956c35 Fixed formatting 2022-06-20 23:45:55 +05:00
Fallenbagel
5af3a7e71b Add in much needed information
upcoming feature explanation and the reason for fork
2022-06-20 22:07:49 +05:00
Fallenbagel
883b9377be Merge pull request #155 from Fallenbagel/change-to-self-hosted-workflow
fix yarn dependecies install in release workflow
2022-06-20 19:26:09 +05:00
Fallenbagel
c7ba553208 fix yarn dependecies install in release workflow 2022-06-20 19:25:25 +05:00
Fallenbagel
76472521ed Merge pull request #154 from Fallenbagel/change-to-self-hosted-workflow
add yarn dependencies
2022-06-20 19:21:38 +05:00
Fallenbagel
a34e14b496 add yarn dependencies 2022-06-20 19:19:28 +05:00
Fallenbagel
715e229e01 Merge pull request #152 from Fallenbagel/change-to-self-hosted-workflow
Changed run-on to self-hosted to speed up release process
2022-06-20 17:39:43 +05:00
Fallenbagel
a5e6217f85 Changed run-on to self-hosted to speed up release process 2022-06-20 17:36:13 +05:00
Smexhy
8619724c65 feat(language): update czech language 2022-06-19 21:03:27 +02:00
Fallenbagel
af522516f7 Merge pull request #149 from jab416171/fix-library-scan-text
Fix text for manual library scan
2022-06-19 23:53:56 +05:00
Fallenbagel
647f594dc8 Merge pull request #147 from jab416171/develop
change email sender name to Jellyseerr on initial setup
2022-06-19 23:42:45 +05:00
jab416171
ae60d44f99 Fix text for manual library scan 2022-06-19 12:38:49 -06:00
Fallenbagel
304b82b594 docs(readme): added information about upcoming/unique features
added information about upcoming/unique features
2022-06-19 23:34:01 +05:00
jab416171
9275119163 change email sender name to Jellyseerr on initial setup 2022-06-19 12:28:09 -06:00
Fallenbagel
94b418bd47 Merge pull request #146 from Fallenbagel/fix-ui-description-mistakes
fix(setup&login): fix a description error in the manual scan in setup and add emby to login page
2022-06-19 22:57:33 +05:00
Fallenbagel
8810c20fc1 fix(setup&login): fix a description error in the manual scan in setup and add emby to login page
Manual scan in setup says "Jellyfin will scan your Jellyfin's server" and same with emby, so I
replaced jellyfin with jellyseerr. And also added emby to login page
2022-06-19 22:27:38 +05:00
Nicolai Van der Storm
63b7be0a38 Update private_registery_push.yml 2022-06-18 22:55:07 +02:00
Nicolai Van der Storm
d3cea69011 Update private_registery_push.yml 2022-06-18 22:48:47 +02:00
Nicolai Van der Storm
31072f4758 Update private_registery_push.yml 2022-06-18 22:45:43 +02:00
Nicolai Van der Storm
89e8825b61 fixes indent issues 2022-06-18 22:39:24 +02:00
Fallenbagel
7956ed8466 Merge pull request #144 from NicolaiVdS/private_registery_workflow
Create private_registery_push.yml
2022-06-19 01:34:08 +05:00
Nicolai Van der Storm
e1081a7bc2 Create private_registery_push.yml 2022-06-18 22:30:15 +02:00
Fallenbagel
fe3495705f Merge pull request #136 from NicolaiVdS/email-validation-and-requirement
feat(userprofile): email requirement and validation + import user button overhaul
2022-06-15 01:29:47 +05:00
Nicolai Van der Storm
29478fc195 fix(import all): fis for import all 2022-06-13 23:13:22 +02:00
Nicolai Van der Storm
f48286043e Merge pull request #139 from Fallenbagel/translation-issue
fix(ui): fixed translation issue where it showed as import {mediaServerName} user
2022-06-13 21:54:07 +02:00
Fallenbagel
d417fcafa1 fix(ui): replaced {mediaServerName} in the plex variable in NL locale
replaced {mediaServerName} in the plex variable in NL locale
2022-06-14 00:51:44 +05:00
Fallenbagel
819190ce98 fix(ui): fixed translation issue where it showed as import {mediaServerName} user
Fixed translation issue where it showed as import {mediaServerName} user as it was using the same
variable both inside the plex import modal and also outside in userlist on the button
2022-06-14 00:48:17 +05:00
Nicolai Van der Storm
a483ca9837 fix(jellyfinimportmodal): fix for importing all jellyfin users 2022-06-13 21:34:51 +02:00
Nicolai Van der Storm
d835336d33 feat(email validation): email requirement and validation + better importer 2022-06-13 14:21:05 +02:00
Nicolai Van der Storm
cc69f66ba9 chore(.idea folder): removed .idea folder and added it to the .gitignore 2022-06-10 12:30:14 +02:00
Nicolai Van der Storm
543859e6f3 feat(uesrprofile): email requirement and validation 2022-06-10 12:19:52 +02:00
Fallenbagel
4fd42874b7 Merge pull request #133 from Fallenbagel/fix-translation-errors
fix(ui): fix translation errors for all locales in the import plex user button
2022-06-09 16:51:56 +05:00
Fallenbagel
0fb5803eb9 fix(ui): fix translation errors for all locales in the import plex user button
fix translation errors for all locales in the import plex user button as it currently shows as
{mediaServerName}
2022-06-09 16:38:18 +05:00
Fallenbagel
00c08b3d67 Merge pull request #132 from notfakie/develop
fix: fix mediaServerType for plex users not being set properly
2022-06-09 16:25:48 +05:00
notfakie
94ade93e16 fix: fix mediaServerType not set for Plex which leads to Plex users seeing Jellyfin settings 2022-06-09 17:08:00 +12:00
Fallenbagel
caa713a968 Merge pull request #128 from NicolaiVdS/feature-add-email-field
feat: add email field in the profile settings
2022-06-05 21:53:53 +05:00
Nicolai Van der Storm
23779f4c7b style: removed .idea folder 2022-06-05 18:43:52 +02:00
Nicolai Van der Storm
5f2ebfe662 Revert "feat(tv): tv seasons"
This reverts commit c117b37cd9.
2022-06-05 18:37:50 +02:00
Nicolai Van der Storm
b22f20b6fa feat(user settings): added email field to user profiel settings
#122
2022-06-05 18:25:15 +02:00
Nicolai Van der Storm
a8bc0c068b feat: email
#122
2022-06-05 18:11:20 +02:00
Nicolai Van der Storm
30c48f16ca feat(user email setting): added field to save user email
fix #122
2022-06-05 17:46:26 +02:00
Fallenbagel
3748f64ce4 Merge pull request #127 from sambartik/fix-sync-errors
fix(jellyfin): fixes sync errors re-introduced in previous commits
2022-06-05 20:27:00 +05:00
Samuel Bartík
d1dbd6e3b9 fix(jellyfin): sync errors 2022-06-05 17:07:27 +02:00
Fallenbagel
6458c054c0 Merge pull request #126 from sambartik/fix-virtual-location
fix(jellyfin): ignore additional items with virtual location type
2022-06-05 18:25:01 +05:00
Samuel Bartík
c81154800f fix(jellyfin): ignore additional items with virtual location type 2022-06-05 12:41:34 +02:00
Fallenbagel
a1cd354691 Merge pull request #125 from sambartik/fix-scan
fix(scan): ignore virtual seasons
2022-06-05 12:35:13 +05:00
Samuel Bartík
6574e18516 fix(scan): ignore virtual seasons
Virtual seasons appeared as available / partially available, even though they were not even shown in the Jellyfin web UI. For more info see #119
2022-06-04 18:07:14 +02:00
Fallenbagel
5298e5fd90 Merge pull request #121 from notfakie/develop
Hide Overseerr settings when running in Jellyfin/Emby mode
2022-06-02 14:42:49 +05:00
notfakie
7450138ac1 fix: hide plex guid cache settings from ui when running in jellyfin/emby mode 2022-06-02 18:47:27 +12:00
notfakie
4b7bdd3d7d fix: remove internal Overseerr sponsor link, this is remaining on the main github page instead 2022-06-02 18:47:25 +12:00
notfakie
739f5f9c9a fix: only show mediaserver settings for current active mediaserver 2022-06-02 18:47:22 +12:00
Nicolai Van der Storm
c117b37cd9 feat(tv): tv seasons
tv seasons
2022-06-01 14:48:05 +02:00
Fallenbagel
3e7d64eb47 Merge pull request #120 from Fallenbagel/embySupport
fix(ui): fix emby ui elements not reflecting the env variable #98
2022-05-30 00:31:05 +05:00
Fallenbagel
b9546e6daa feat(ui): add emby user badge to the userProfile
adds emby user badge to the userProfile general page
2022-05-30 00:21:04 +05:00
Fallenbagel
722dda5856 fix(ui): fix ui elements not reflecting the env variable
Fix emby ui elements not reflecting the emby env variable set during runtime
2022-05-29 23:58:54 +05:00
Fallenbagel
c67ca34111 Merge pull request #118 from jab416171/patch-2
fix aur url
2022-05-29 05:50:09 +05:00
jab416171
16311808b1 fix aur url 2022-05-28 18:34:50 -06:00
Fallenbagel
509c43e552 Merge pull request #117 from jab416171/patch-1
Add manual install steps and aur package info
2022-05-29 05:29:49 +05:00
jab416171
84a97675dc Add manual install steps and aur package info 2022-05-28 18:14:57 -06: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
Fallenbagel
4615286f49 Merge pull request #115 from CyferShepard/JellyfinNoPasswordFix
feat:Remove Requirement for Jellyfin Passwords (#108 #31)
2022-05-28 12:32:02 +05:00
Fallenbagel
7b7354d006 Merge remote-tracking branch 'upstream/develop' into develop 2022-05-28 12:20:26 +05:00
CyferShepard
ad7b3590d7 Move auth.ts to correct folder 2022-05-28 08:50:51 +02:00
CyferShepard
bda7858b66 Delete auth.ts 2022-05-28 08:50:23 +02:00
CyferShepard
d600a45559 feat:Remove Requirement for Jellyfin Passwords 2022-05-28 08:29:45 +02:00
Fallenbagel
1dcfe49b1b Merge pull request #112 from Fallenbagel/forgotPasswordFix
fix: fixes jellyfin forgot password and adds emby support to the forgort password link
2022-05-28 07:29:46 +05:00
Fallenbagel
1dbc565a2e Merge pull request #103 from boring-dragon/develop
feat: conditional media server name to add emby
2022-05-28 07:29:38 +05:00
Fallenbagel
973a3e826f Merge pull request #111 from Fallenbagel/AvatarUrlFix
fix(ui): fix Avatar being broken when setup using internal ip
2022-05-28 07:22:31 +05:00
Fallenbagel
6a6bfe0c68 feat(ui): add emby as a mediaServerType to the import user button
Add emby as a mediaServerType to the import user button and replace any reference to Jellyfin with
mediaServerName
2022-05-27 05:35:00 +05:00
Fallenbagel
410b536c94 feat(ui): add emby user badge to the user list and fix local user badge
add emby user badge to the user list and fix local user badge which was previously not showing
2022-05-27 04:58:24 +05:00
Fallenbagel
18d8d969f1 style(ui): conditional mediaServerName to add emby to setup/login page 2022-05-26 09:39:19 +05:00
Fallenbagel
f8a239b1b8 style(ui): conditional media server name to add emby to settings
Conditionaly media server name to replace every reference of jellyfin with emby in settings tab when
environmental variable set
2022-05-26 08:52:16 +05:00
Fallenbagel
377a4fd85b feat(ui): conditional media server name to add emby to issuedetails play on button 2022-05-26 08:39:39 +05:00
Fallenbagel
14d293799b feat(ui): conditional media server name to add emby to moviedetails 2022-05-26 08:37:51 +05:00
Fallenbagel
ddd773c03f fix: conditional media server name for 4k url to add emby to tvdetails 2022-05-26 08:30:21 +05:00
Fallenbagel
e75b71b816 feat: conditional media server name to add emby to tvdetails 2022-05-26 05:54:57 +05:00
Fallenbagel
ff3e3ce841 feat: conditional media server name to add emby to tvdetails 2022-05-26 05:49:33 +05:00
Fallenbagel
01e81a73a3 fix(ui): fix Avatar being broken when setup using internal ip
allow avatar url to use externalHostname when setup using local ip

fix #110
2022-05-26 02:24:31 +05:00
Samuel Bartík
db05172d8b fix(ui): rectangular avatars getting stretched (#2782) 2022-05-25 20:42:17 +04:00
Mohamed jinas
2bfdf02c79 feat: conditional media server name 2022-05-22 22:12:05 +05:00
482 changed files with 18033 additions and 9001 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

@@ -3,7 +3,7 @@ name: Jellyseerr CI
on:
pull_request:
branches:
- "*"
- '*'
push:
branches:
- develop
@@ -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,23 +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@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
@@ -77,7 +85,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

@@ -3,7 +3,7 @@ name: Jellyseerr Preview
on:
push:
tags:
- "preview-*"
- 'preview-*'
jobs:
build_and_push:
@@ -16,16 +16,16 @@ 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: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile

View File

@@ -18,11 +18,11 @@ 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 }}
@@ -35,14 +35,68 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: npx semantic-release
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: semantic-release
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
- armhf
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Switch to master branch
run: git checkout master
- name: Pull latest changes
run: git pull
- name: Prepare
id: prepare
run: |
git fetch --prune --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@v1
with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v2
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: semantic-release
if: always()
runs-on: ubuntu-20.04
runs-on: self-hosted
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

14
.gitignore vendored
View File

@@ -53,3 +53,17 @@ 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
# Webstorm
.idea

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

@@ -9,12 +9,17 @@
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
## Current Features
- Jellyfin Support
- Emby Support
(Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!)
Along with all the existing Overseerr features:
- Full Plex integration. Authenticate and manage user access with Plex!
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
- Plex library scan, to keep track of the titles which are already available.
@@ -31,6 +36,18 @@ With more features on the way! Check out our [issue tracker](https://github.com/
Check out our dockerhub for instructions on how to install and run Jellyseerr:
https://hub.docker.com/r/fallenbagel/jellyseerr
### Launching Jellyseerr manually:
```bash
yarn install
yarn run build
yarn start
```
### Packages:
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
## Preview
<img src="./public/preview.jpg">
@@ -55,4 +72,4 @@ Our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/COD
## Contributing
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.

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,25 @@
describe('Pull To Refresh', () => {
beforeEach(() => {
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
cy.viewport(390, 844);
cy.visitMobile('/');
});
it('reloads the current page', () => {
cy.wait(500);
cy.intercept({
method: 'GET',
url: '/api/v1/*',
}).as('apiCall');
cy.get('.searchbar').swipe('bottom', [190, 400]);
cy.wait('@apiCall').then((interception) => {
assert.isNotNull(
interception.response.body,
'API was called and received data'
);
});
});
});

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,35 @@
/// <reference types="cypress" />
import 'cy-mobile-commands';
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

@@ -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,7 +1,14 @@
/**
* @type {import('next').NextConfig}
*/
module.exports = {
env: {
commitTag: process.env.COMMIT_TAG || 'local',
},
publicRuntimeConfig: {
// Will be available on both server and client
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
},
images: {
domains: ['image.tmdb.org'],
},
@@ -14,4 +21,7 @@ module.exports = {
return config;
},
experimental: {
scrollRestoration: true,
},
};

View File

@@ -1841,14 +1841,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:
@@ -1859,6 +1859,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
@@ -2725,6 +2731,12 @@ paths:
nullable: true
enum: [debug, info, warn, error]
default: debug
- in: query
name: search
schema:
type: string
nullable: true
example: plex
responses:
'200':
description: Server log returned
@@ -3394,8 +3406,8 @@ paths:
name: guid
required: true
schema:
type: number
example: 1
type: string
example: '9afef5a7-ec89-4d5f-9397-261e96970b50'
responses:
'200':
description: OK
@@ -3759,6 +3771,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
@@ -4650,6 +4709,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
@@ -4677,7 +4776,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:
@@ -5362,6 +5470,23 @@ paths:
responses:
'204':
description: Succesfully removed media item
/media/{mediaId}/file:
delete:
summary: Delete media file
description: Removes a media file from radarr/sonarr. The `ADMIN` permission is required to perform this action.
tags:
- media
parameters:
- in: path
name: mediaId
description: Media ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed media item
/media/{mediaId}/{status}:
post:
summary: Update media status
@@ -5580,7 +5705,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
@@ -5602,7 +5727,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
@@ -5667,7 +5792,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
@@ -5695,7 +5820,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

View File

@@ -3,18 +3,25 @@
"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,145 @@
},
"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",
"cronstrue": "2.11.0",
"csurf": "1.11.0",
"date-fns": "2.29.1",
"email-templates": "9.0.0",
"email-validator": "2.0.4",
"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",
"pulltorefreshjs": "0.1.22",
"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/pulltorefreshjs": "0.1.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",
"cy-mobile-commands": "0.3.0",
"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 +188,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 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosInstance } from 'axios';
import logger from '../logger';
import logger from '@server/logger';
import type { AxiosInstance } from 'axios';
import axios from 'axios';
export interface JellyfinUserResponse {
Name: string;
@@ -16,7 +17,7 @@ export interface JellyfinLoginResponse {
}
export interface JellyfinUserListResponse {
users: Array<JellyfinUserResponse>;
users: JellyfinUserResponse[];
}
export interface JellyfinLibrary {
@@ -31,6 +32,7 @@ export interface JellyfinLibraryItem {
Id: string;
HasSubtitles: boolean;
Type: 'Movie' | 'Episode' | 'Season' | 'Series';
LocationType: 'FileSystem' | 'Offline' | 'Remote' | 'Virtual';
SeriesName?: string;
SeriesId?: string;
SeasonId?: string;
@@ -205,7 +207,9 @@ class JellyfinAPI {
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
);
return contents.data.Items;
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
} catch (e) {
logger.error(
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
@@ -251,7 +255,9 @@ class JellyfinAPI {
try {
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
return contents.data.Items;
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
} catch (e) {
logger.error(
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
@@ -270,7 +276,9 @@ class JellyfinAPI {
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
);
return contents.data.Items;
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
} catch (e) {
logger.error(
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,

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,
@@ -213,6 +213,20 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
);
}
}
public removeMovie = async (movieId: number): Promise<void> => {
try {
const { id, title } = await this.getMovieByTmdbId(movieId);
await this.axios.delete(`/movie/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed movie ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
}
};
}
export default RadarrAPI;

View File

@@ -1,4 +1,4 @@
import logger from '../../logger';
import logger from '@server/logger';
import ServarrBase from './base';
interface SonarrSeason {
@@ -302,6 +302,20 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
return newSeasons;
}
public removeSerie = async (serieId: number): Promise<void> => {
try {
const { id, title } = await this.getSeriesByTvdbId(serieId);
await this.axios.delete(`/series/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed serie ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
}
};
}
export default SonarrAPI;

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;
@@ -192,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}`);
}
};
@@ -214,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}`
);
}
};
@@ -241,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}`);
}
};
@@ -267,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}`);
}
};
@@ -293,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}`);
}
};
@@ -319,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}`);
}
}
@@ -345,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}`);
}
}
@@ -371,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}`);
}
}
@@ -398,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}`
);
}
}
@@ -422,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}`);
}
}
@@ -455,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}`);
}
};
@@ -488,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}`);
}
};
@@ -514,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}`);
}
};
@@ -541,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}`);
}
};
@@ -564,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}`);
}
};
@@ -587,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}`);
}
};
@@ -619,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}`);
}
}
@@ -657,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}`
);
}
}
@@ -687,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}`
);
}
}
@@ -711,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}`);
}
}
@@ -727,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}`);
}
}
@@ -743,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}`);
}
}
@@ -755,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}`);
}
}
@@ -765,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}`);
}
}
@@ -816,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}`);
}
}
@@ -867,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,22 +1,23 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaStatus, MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
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 { MediaServerType } from '../constants/server';
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';
@@ -37,7 +38,7 @@ class Media {
}
const media = await mediaRepository.find({
tmdbId: In(finalIds),
where: { tmdbId: In(finalIds) },
});
return media;
@@ -56,10 +57,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;
@@ -152,6 +153,9 @@ class Media {
public mediaUrl?: string;
public mediaUrl4k?: string;
public iOSPlexUrl?: string;
public iOSPlexUrl4k?: string;
public tautulliUrl?: string;
public tautulliUrl4k?: string;
@@ -172,20 +176,24 @@ 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}`;
}
}
if (this.ratingKey4k) {
this.mediaUrl4k = `${
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
this.ratingKey4k
}`;
if (this.ratingKey4k) {
this.mediaUrl4k = `${
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
this.ratingKey4k
}`;
if (tautulliUrl) {
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
this.iOSPlexUrl4k = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}&server=${machineId}`;
if (tautulliUrl) {
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
}
}
}
} else {

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';
@@ -137,6 +134,8 @@ export class User {
@UpdateDateColumn()
public updatedAt: Date;
public warnings: string[] = [];
constructor(init?: Partial<User>) {
Object.assign(this, init);
}
@@ -268,13 +267,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) {
@@ -87,8 +91,18 @@ app
new WebPushAgent(),
]);
// Start Jobs
startJobs();
const userRepository = getRepository(User);
const totalUsers = await userRepository.count();
if (totalUsers > 0) {
startJobs();
} else {
logger.info(
`Skipping starting the scheduled jobs as we have no Plex/Jellyfin/Emby servers setup yet`,
{
label: 'Server',
}
);
}
const server = express();
if (settings.main.trustProxy) {

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

@@ -59,4 +59,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,7 +1,8 @@
import { NotificationAgentKey } from '../../lib/settings';
import type { NotificationAgentKey } from '@server/lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
email?: string;
discordId?: string;
locale?: string;
region?: string;
@@ -14,6 +15,8 @@ export interface UserSettingsGeneralResponse {
globalMovieQuotaLimit?: number;
globalTvQuotaLimit?: number;
globalTvQuotaDays?: number;
watchlistSyncMovies?: boolean;
watchlistSyncTv?: boolean;
}
export type NotificationAgentTypes = Record<NotificationAgentKey, number>;

View File

@@ -1,17 +1,19 @@
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
import JellyfinAPI from '@server/api/jellyfin';
import TheMovieDb from '@server/api/themoviedb';
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
import { MediaStatus, MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import Season from '@server/entity/Season';
import { User } from '@server/entity/User';
import type { Library } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import AsyncLock from '@server/utils/asyncLock';
import { randomUUID as uuid } from 'crypto';
import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import JellyfinAPI, { JellyfinLibraryItem } from '../../api/jellyfin';
import TheMovieDb from '../../api/themoviedb';
import { TmdbTvDetails } from '../../api/themoviedb/interfaces';
import { MediaStatus, MediaType } from '../../constants/media';
import { MediaServerType } from '../../constants/server';
import Media from '../../entity/Media';
import Season from '../../entity/Season';
import { User } from '../../entity/User';
import { getSettings, Library } from '../../lib/settings';
import logger from '../../logger';
import AsyncLock from '../../utils/asyncLock';
const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000;
@@ -552,6 +554,7 @@ class JobJellyfinSync {
this.running = true;
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
where: { id: 1 },
select: [
'id',
'jellyfinAuthToken',

View File

@@ -1,11 +1,13 @@
import { MediaServerType } from '@server/constants/server';
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 { MediaServerType } from '../constants/server';
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';
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
interface ScheduledJob {
@@ -14,6 +16,7 @@ interface ScheduledJob {
name: string;
type: 'process' | 'command';
interval: 'short' | 'long' | 'fixed';
cronSchedule: string;
running?: () => boolean;
cancelFn?: () => void;
}
@@ -31,6 +34,7 @@ export const startJobs = (): void => {
name: 'Plex Recently Added Scan',
type: 'process',
interval: 'short',
cronSchedule: jobs['plex-recently-added-scan'].schedule,
job: schedule.scheduleJob(
jobs['plex-recently-added-scan'].schedule,
() => {
@@ -50,6 +54,7 @@ export const startJobs = (): void => {
name: 'Plex Full Library Scan',
type: 'process',
interval: 'long',
cronSchedule: jobs['plex-full-scan'].schedule,
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Full Library Scan', {
label: 'Jobs',
@@ -69,6 +74,7 @@ export const startJobs = (): void => {
name: 'Jellyfin Recently Added Sync',
type: 'process',
interval: 'long',
cronSchedule: jobs['jellyfin-recently-added-sync'].schedule,
job: schedule.scheduleJob(
jobs['jellyfin-recently-added-sync'].schedule,
() => {
@@ -88,6 +94,7 @@ export const startJobs = (): void => {
name: 'Jellyfin Full Library Sync',
type: 'process',
interval: 'long',
cronSchedule: jobs['jellyfin-full-sync'].schedule,
job: schedule.scheduleJob(jobs['jellyfin-full-sync'].schedule, () => {
logger.info('Starting scheduled job: Jellyfin Full Sync', {
label: 'Jobs',
@@ -99,12 +106,28 @@ export const startJobs = (): void => {
});
}
// Run watchlist sync every 5 minutes
scheduledJobs.push({
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
interval: 'long',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
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',
name: 'Radarr Scan',
type: 'process',
interval: 'long',
cronSchedule: jobs['radarr-scan'].schedule,
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
radarrScanner.run();
@@ -119,6 +142,7 @@ export const startJobs = (): void => {
name: 'Sonarr Scan',
type: 'process',
interval: 'long',
cronSchedule: jobs['sonarr-scan'].schedule,
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
sonarrScanner.run();
@@ -133,6 +157,7 @@ export const startJobs = (): void => {
name: 'Download Sync',
type: 'command',
interval: 'fixed',
cronSchedule: jobs['download-sync'].schedule,
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
logger.debug('Starting scheduled job: Download Sync', {
label: 'Jobs',
@@ -147,6 +172,7 @@ export const startJobs = (): void => {
name: 'Download Sync Reset',
type: 'command',
interval: 'long',
cronSchedule: jobs['download-sync-reset'].schedule,
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
logger.info('Starting scheduled job: Download Sync Reset', {
label: 'Jobs',

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,17 @@
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 * as EmailValidator from 'email-validator';
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 +82,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 ' : ''
@@ -215,14 +219,23 @@ class EmailAgent
this.getSettings(),
payload.notifyUser.settings?.pgpKey
);
await email.send(
this.buildMessage(
type,
payload,
payload.notifyUser.email,
payload.notifyUser.displayName
)
);
if (EmailValidator.validate(payload.notifyUser.email)) {
await email.send(
this.buildMessage(
type,
payload,
payload.notifyUser.email,
payload.notifyUser.displayName
)
);
} else {
logger.warn('Invalid email address provided for user', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
});
}
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',
@@ -268,9 +281,18 @@ class EmailAgent
this.getSettings(),
user.settings?.pgpKey
);
await email.send(
this.buildMessage(type, payload, user.email, user.displayName)
);
if (EmailValidator.validate(user.email)) {
await email.send(
this.buildMessage(type, payload, user.email, user.displayName)
);
} else {
logger.warn('Invalid email address provided for user', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
});
}
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',

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

@@ -22,6 +22,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

@@ -1,9 +1,9 @@
import { MediaServerType } from '@server/constants/server';
import { randomUUID } from 'crypto';
import fs from 'fs';
import { merge } from 'lodash';
import path from 'path';
import webpush from 'web-push';
import { MediaServerType } from '../constants/server';
import { Permission } from './permissions';
export interface Library {
@@ -134,6 +134,7 @@ interface FullPublicSettings extends PublicSettings {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
userEmailRequired: boolean;
newPlexLogin: boolean;
}
@@ -159,6 +160,7 @@ export interface NotificationAgentSlack extends NotificationAgentConfig {
export interface NotificationAgentEmail extends NotificationAgentConfig {
options: {
userEmailRequired: boolean;
emailFrom: string;
smtpHost: string;
smtpPort: number;
@@ -255,6 +257,7 @@ interface JobSettings {
export type JobId =
| 'plex-recently-added-scan'
| 'plex-full-scan'
| 'plex-watchlist-sync'
| 'radarr-scan'
| 'sonarr-scan'
| 'download-sync'
@@ -335,6 +338,7 @@ class Settings {
email: {
enabled: false,
options: {
userEmailRequired: false,
emailFrom: '',
smtpHost: '',
smtpPort: 587,
@@ -342,7 +346,7 @@ class Settings {
ignoreTls: false,
requireTls: false,
allowSelfSigned: false,
senderName: 'Overseerr',
senderName: 'Jellyseerr',
},
},
discord: {
@@ -421,6 +425,9 @@ class Settings {
'plex-full-scan': {
schedule: '0 0 3 * * *',
},
'plex-watchlist-sync': {
schedule: '0 */10 * * * *',
},
'radarr-scan': {
schedule: '0 0 4 * * *',
},
@@ -529,6 +536,8 @@ class Settings {
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
locale: this.data.main.locale,
emailEnabled: this.data.notifications.agents.email.enabled,
userEmailRequired:
this.data.notifications.agents.email.options.userEmailRequired,
newPlexLogin: this.data.main.newPlexLogin,
};
}

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;

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