Compare commits

...

299 Commits

Author SHA1 Message Date
semantic-release-bot
eceedbbaad chore(release): 1.1.0 2022-05-21 01:58:03 +00:00
Fallenbagel
29f06a965c Merge branch 'develop' 2022-05-21 06:43:52 +05:00
Fallenbagel
54067e0457 style: rebranded applicationTitle to default to Jellyseerr 2022-05-21 06:06:14 +05:00
Fallenbagel
1e53139162 style: added mediaServerType to reflect which mediaserver is being used in the play on button
added mediaServerType to reflect which mediaserver is being used in the play on button in issues
page
2022-05-20 06:33:08 +05:00
Fallenbagel
0dd8b15109 style: added mediaServerType to reflect which mediaserver is being used in the enable new sign in
added mediaServerType to reflect which mediaserver is being used in the enable new sign in so it
displays either plex or jellyfin in the ui
2022-05-17 01:25:33 +05:00
Fallenbagel
f286818e30 rebranded to jellyseerr 2022-05-12 08:55:56 +05:00
notfakie
72ca694f21 fix: don't allow login for unimported Jellyfin users if not set in settings 2022-04-28 18:47:28 +12:00
notfakie
36c3c9d7c6 fix: jellyfin user signin after manual user import 2022-04-27 19:52:59 +12:00
Fallenbagel
b52d4a771c style: removed created user error during log in with imported user
removed created user error during log in with imported user
2022-04-27 11:51:26 +05:00
notfakie
9d54776a2c fix: plex Login 2022-04-27 08:08:32 +12:00
notfakie
9fbc4074e4 feat: allow Jellyfin to set a playback URL different to the Jellyfin host specified during setup 2022-04-27 08:07:57 +12:00
notfakie
9e2f3f0639 feat: implement import users from Jellyfin button 2022-04-27 08:07:39 +12:00
Fallenbagel
9ec05d3ba4 Fixed the link for the jellyseerr logo 2022-04-24 13:22:47 +05:00
notfakie
103350fe14 feat: initialize Jellyfin/Emby users with local login 2022-04-24 18:59:28 +12:00
notfakie
3e1e11d9d9 feat: remove email requirement for jellyfin/emby non-admin users 2022-04-24 18:59:21 +12:00
notfakie
791106a7f5 fix: only run scheduled mediaserver jobs that apply to the current mediaserver 2022-04-24 18:58:51 +12:00
Fallenbagel
4470b65563 Adds Github_token as an env
Adds Github_token as an env
2022-04-20 06:22:42 +05:00
semantic-release-bot
ee14ff5a51 chore(release): 1.0.2 2022-04-20 00:06:57 +00:00
Fallenbagel
6b62d4b862 Merge pull request #82 from Fallenbagel/workFlowfix
ci: adds GITHUB_TOKEN as an env
2022-04-20 05:02:17 +05:00
Fallenbagel
706fea0e97 ci: adds GITHUB_TOKEN as an env
adds GITHUB_TOKEN as an env to fix the github_token missing error during release workflow
2022-04-20 05:00:29 +05:00
Fallenbagel
80956d1a83 Merge pull request #81 from Fallenbagel/fixMediaServerType
fix: fix usertype from local user to mediaServerType
2022-04-20 04:58:20 +05:00
Fallenbagel
6d530d9028 fix: fix usertype from local user to mediaServerType
Fixes usertype from appearing as local user even if the mediaServerType is jellyfin
2022-04-20 04:52:39 +05:00
Fallenbagel
f12237565f Merge pull request #80 from Fallenbagel/packagejsonChanges
update tags and the branch to jellyseerr
2022-04-20 03:49:16 +05:00
Fallenbagel
11f5594ed4 update tags and the branch to jellyseerr 2022-04-20 03:47:46 +05:00
Fallenbagel
e4e58bee05 Merge pull request #79 from Fallenbagel/githubChanges
update workflows and discord locations for jellyseerr
2022-04-20 03:32:46 +05:00
Fallenbagel
13ee3a836c update workflows and discord locations for jellyseerr 2022-04-20 03:29:19 +05:00
Fallenbagel
3f16a353f5 Merge pull request #78 from Fallenbagel/urlValidationFix
fix: relax jellyfin url validation to allow local domains
2022-04-20 03:25:41 +05:00
Fallenbagel
9c43ba95e6 fix: relax jellyfin url validation to allow local domains
Relaxes jellyfin url validation so that http://localhost:8096 and http://jellyfin:8096 urls are
accepted in addition to full urls like https://example.com

fix #123
2022-04-20 03:12:01 +05:00
Fallenbagel
25bee8b9f7 fix: fix usertype from local user to mediaServerType
Fixes usertype from appearing as local user even if the mediaServerType is jellyfin
2022-04-20 03:03:56 +05:00
Fallenbagel
2f9c068a9b chore: replace stable with unstable by replacing the version number with v0.1.0
replace stable with unstable by replacing the version number with v0.1.0 for testing purposes
2022-04-20 01:40:33 +05:00
Fallenbagel
f09b86aa87 fix: replaced unknown job with jellyfin in jobsandcache and added translations for it
Replaced unknown job with jellyfin library full scan and jellyfin recently added and added locales
to reflect it
2022-04-19 21:19:51 +05:00
notfakie
4db8e5464d fix: disable user-import from mediaserver for non-plex mediaservers until implemented 2022-04-19 22:28:56 +12:00
notfakie
d0c5481d22 fix: play on Jellyfin for TV shows 2022-04-19 22:28:56 +12:00
notfakie
3a010f8211 fix: relax jellyfin url validation to allow local domains 2022-04-19 22:28:56 +12:00
Shilong Jiang
88c2c5ebcd feat: add emby detail url support 2022-04-19 22:25:12 +12:00
Fallenbagel
13fb6fd1a7 Updated the docker tags
Updated the docker tags to point to fallenbagel docker repo
2022-04-18 07:27:21 +05:00
Fallenbagel
16e8e3a38e update workflow to test for jellyseerr
update workflow and discord locations to test the docker pipeline
2022-04-18 07:17:16 +05:00
notfakie
da88771da5 update README & repo links throughout repo. Fixes outdated message displayed in app 2022-04-16 13:56:36 +12:00
notfakie
aab1b2d4c6 update workflow and discord locations for jellyseerr 2022-04-16 13:22:27 +12:00
Fallenbagel
c3ccb00b86 rebrand fork of Overseerr as Jellyseerr by replacing images and visual references 2022-04-16 09:35:23 +12:00
Juan D. Jara
3661eea8bb added Support for Jellyfin Media Server 2022-04-16 09:35:09 +12:00
semantic-release-bot
5125abdbf0 chore(release): 1.29.1 2022-04-06 23:34:24 +00:00
Ryan Cohen
724f8bb538 Merge branch 'develop' 2022-04-07 08:33:04 +09:00
TheCatLady
b75fc7b238 fix(auth): resolve local/password authentication issues (#2677)
* fix(auth): only add Plex ID to user after verifying server access

* fix(auth): do not fail local auth if fetching Plex users is unsuccessful
2022-04-06 08:31:14 +09:00
semantic-release-bot
512f6e51e4 chore(release): 1.29.0 2022-04-01 09:03:58 +00:00
Ryan Cohen
1b36dded3f Merge branch 'develop' 2022-04-01 18:02:32 +09:00
Ryan Cohen
77511bf199 ci: disable husky for release ci 2022-04-01 18:01:22 +09:00
Ryan Cohen
26d77af3a4 Merge branch 'develop' 2022-04-01 17:45:51 +09:00
Weblate (bot)
341e3b8f06 feat(lang): translations update from Hosted Weblate (#2645)
* feat(lang): translated using Weblate (Chinese (Traditional))

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

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

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1042 of 1042 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: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 99.8% (1040 of 1042 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: JoKerIsCraZy <jokeriscrazy@yahoo.com>
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 100.0% (1042 of 1042 strings)

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

Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
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/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: JoKerIsCraZy <jokeriscrazy@yahoo.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-03-31 15:34:37 -04:00
allcontributors[bot]
ca112d33ad docs: add Maxentr as a contributor for translation (#2643) [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>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2022-03-24 00:40:39 +00:00
Weblate (bot)
418a533588 fix(lang): translations update from Hosted Weblate (#2639)
* feat(lang): translated using Weblate (Serbian)

Currently translated at 49.7% (518 of 1042 strings)

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

* 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: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1042 of 1042 strings)

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

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

Currently translated at 78.4% (817 of 1042 strings)

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
2022-03-23 20:34:12 -04:00
Weblate (bot)
1d0cbd2e76 feat(lang): translations update from Hosted Weblate (#2629)
* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1042 of 1042 strings)

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

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

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1042 of 1042 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/pl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1042 of 1042 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (1042 of 1042 strings)

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

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

Currently translated at 100.0% (1042 of 1042 strings)

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

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: exentler <gurandsrud@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-03-17 21:40:36 -04:00
TheCatLady
d4f9650cd0 fix(plex): include 'Overseerr' in X-Plex-Device-Name header (#2635) 2022-03-17 00:42:05 +00:00
Danshil Kokil Mungur
26a3d7c4d2 docs(notif): add missing link for gotify (#2634) [skip ci]
On Gitbook, the link to the Gotify page would point to a markdown file in the repository instead of
the page on Gitbook.
2022-03-17 01:38:07 +04:00
Weblate (bot)
19cdedd2a6 fix(lang): translations update from Hosted Weblate (#2625)
* feat(lang): translated using Weblate (Greek)

Currently translated at 74.7% (779 of 1042 strings)

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

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

Currently translated at 99.8% (1040 of 1042 strings)

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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.8% (1040 of 1042 strings)

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

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

Currently translated at 93.1% (971 of 1042 strings)

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

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

Currently translated at 94.5% (985 of 1042 strings)

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

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

Currently translated at 95.0% (990 of 1042 strings)

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

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

Currently translated at 95.6% (997 of 1042 strings)

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

Currently translated at 99.8% (1040 of 1042 strings)

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

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

Currently translated at 99.8% (1040 of 1042 strings)

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

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

Currently translated at 86.5% (902 of 1042 strings)

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

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

Currently translated at 100.0% (1042 of 1042 strings)

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

Currently translated at 78.4% (817 of 1042 strings)

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

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

Currently translated at 78.5% (818 of 1042 strings)

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

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

Currently translated at 100.0% (1042 of 1042 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1042 of 1042 strings)

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

Currently translated at 100.0% (1042 of 1042 strings)

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.8% (1040 of 1042 strings)

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

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

Currently translated at 99.4% (1036 of 1042 strings)

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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.8% (1040 of 1042 strings)

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
2022-03-15 17:10:14 -04:00
Danshil Kokil Mungur
0c7373c7e8 feat(about): show config directory (#2600)
* feat(about): show config directory

* feat(about): run yarn i18n:extract

* refactor(about): use existing appdata path method

* feat(about): suggested changes

* refactor(logs): rename variable to be more consistent

* feat: suggested changes
2022-03-15 09:01:11 -04:00
allcontributors[bot]
d92f169375 docs: add Andersborrits as a contributor for translation (#2617) [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-03-14 17:47:09 +00:00
Weblate (bot)
81c75c800e feat(lang): translations update from Hosted Weblate (#2611)
* feat(lang): translated using Weblate (Danish)

Currently translated at 95.7% (997 of 1041 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 95.7% (997 of 1041 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 95.5% (995 of 1041 strings)

Co-authored-by: Coxe <andersborrits@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/da/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 86.6% (902 of 1041 strings)

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

Co-authored-by: Coxe <andersborrits@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
2022-03-14 17:39:23 +00:00
TheCatLady
7d19de6a4a fix(tautulli): fetch additional user history as necessary to return 20 unique media (#2446)
* fix(tautulli): fetch additional user history as necessary to return 20 unique media

* refactor: rename var for clarity

* refactor: make single DB query for recently watched media

* fix: resolve query builder weirdness

* refactor: use find instead of qb

* refactor: minor refactor

* fix: also find 4K rating keys
2022-03-14 21:31:11 +04:00
Danshil Kokil Mungur
72c825d2a5 feat(search): filter search results by year (#2460)
* feat(search): filter search results by year

* fix: typo in endpoint, blame it on new brand of coffee

* feat(search): suggested changes
2022-03-14 13:37:58 +00:00
TheCatLady
30644f65ea fix(plex): find TV series in addition to movies from IMDb IDs (#1830) 2022-03-14 13:29:58 +00:00
TheCatLady
4c50727a32 ci: only lint/test PRs (#2437) 2022-03-14 16:16:45 +04:00
Danshil Kokil Mungur
d06f2cdb08 fix(plex): correctly generate uuid for safari (#2614) 2022-03-14 20:19:11 +09:00
TheCatLady
086f0b6ce2 fix(css): rename form-input to form-input-area (#2613) 2022-03-14 11:24:39 +04:00
dependabot[bot]
1f964b576a build(deps): bump actions/checkout from 2 to 3 (#2589) [skip ci]
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-13 19:34:25 +00:00
TheCatLady
a229b15e7a build(deps): bump dependencies (#2584)
* build(deps): bump dependencies

* build(ci): disable broken snap builds
2022-03-13 19:27:39 +00:00
TheCatLady
fac809b18b chore(github): update CODEOWNERS (#2612) [skip ci] 2022-03-13 19:18:45 +00:00
TheCatLady
c3dbd0d691 fix(frontend): various fixes (#2524)
* fix(frontend): various fixes

* fix: service URL does not require media type
2022-03-13 17:06:04 +00:00
TheCatLady
85bb30e252 feat: verify Plex server access during auth for existing users with Plex IDs (#2458)
* feat: if local sign-in disabled, verify Plex server access during auth for existing users

* fix: disable local/password login by default

* fix: set localLogin to disabled in getInitialProps

* fix: verify Plex server access on local logins as well
2022-03-13 17:53:50 +04:00
allcontributors[bot]
aa79dc1c42 docs: add tomgacz as a contributor for translation (#2607) [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-03-12 22:41:00 +00:00
allcontributors[bot]
e40e024132 docs: add deniscerri as a contributor for translation (#2606) [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-03-12 22:32:44 +00:00
TheCatLady
3d32462f50 feat(lang): add Albanian display language (#2605) 2022-03-12 17:17:03 -05:00
Weblate (bot)
4549ed389e feat(lang): translations update from Hosted Weblate (#2541)
* feat(lang): translated using Weblate (Japanese)

Currently translated at 46.5% (485 of 1041 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/ja/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 62.6% (652 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 50.7% (528 of 1041 strings)

feat(lang): translated using Weblate (Albanian)

Currently translated at 16.3% (170 of 1041 strings)

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

Co-authored-by: Denis Çerri <deniscerri3@gmail.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/sq/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1041 of 1041 strings)

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

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

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1041 of 1041 strings)

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

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

Currently translated at 100.0% (1041 of 1041 strings)

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

Currently translated at 98.5% (1026 of 1041 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 (Italian)

Currently translated at 100.0% (1041 of 1041 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 99.8% (1039 of 1041 strings)

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Denis Çerri <deniscerri3@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: Nackophilz <zrv4flra@anonaddy.me>
Co-authored-by: tomgacz <tomgacz@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
2022-03-12 11:25:51 -05:00
Alex Cortelyou
82d16177bf fix(frontend): disable autocomplete on search field (#2592) 2022-03-12 05:43:07 +00:00
TheCatLady
223c1f03e6 chore: gitignore gzipped JSON logs (#2550) [skip ci] 2022-03-12 05:21:16 +00:00
Jakob Ankarhem
648b346cbe fix(plex): use unique client identifier (#2602)
* fix(plex): use unique client identifier

As noted by SwiftPanda each client needs a unique identifier
https://discord.com/channels/783137440809746482/793885156569514046/950083089575583824

* fix(typo): fix character casing

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2022-03-12 05:03:19 +00:00
TheCatLady
74d5c1ca67 chore(github): update CODEOWNERS (#2604) [skip ci] 2022-03-12 13:28:11 +09:00
TheCatLady
aa062d921c fix(sonarr): monitor existing series upon request approval (#2553) 2022-03-05 18:50:19 +09:00
Weblate (bot)
54b32ebfd6 feat(lang): translations update from Hosted Weblate (#2531)
* feat(lang): translated using Weblate (French)

Currently translated at 99.9% (1040 of 1041 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1041 of 1041 strings)

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

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

Currently translated at 100.0% (1041 of 1041 strings)

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

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

Currently translated at 86.6% (902 of 1041 strings)

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

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

Currently translated at 100.0% (1041 of 1041 strings)

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

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

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

Currently translated at 100.0% (1041 of 1041 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1041 of 1041 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/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Nackophilz <zrv4flra@anonaddy.me>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-02-13 16:43:18 -05:00
TheCatLady
eff665ef4b fix: add Discord ID setting to general user settings page (#2406) 2022-02-10 07:31:01 +00:00
Weblate (bot)
9f4ae34da7 feat(lang): translations update from Hosted Weblate (#2508)
* feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1038 of 1038 strings)

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

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

Currently translated at 100.0% (1038 of 1038 strings)

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

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

Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
2022-02-10 03:02:42 +00:00
TheCatLady
1dc900d5ce feat(tautulli): validate upon saving settings (#2511) 2022-02-10 02:56:00 +00:00
TheCatLady
a76b608ab7 fix(email): enclose PGP encryption logic in try/catch (#2519) 2022-02-10 02:48:53 +00:00
TheCatLady
61681857b1 fix(sonarr): only scan seasons that exist in TMDb (#2523) 2022-02-10 02:42:20 +00:00
TheCatLady
d863a55de2 build(deps): bump dependencies & only specify major version for GH actions (#2525)
* build(deps): bump dependencies & only specify major version for GH actions

* build(deps): bump dependencies again
2022-02-10 11:27:37 +09:00
Weblate (bot)
ec08fa6793 feat(lang): translations update from Hosted Weblate (#2489)
* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1038 of 1038 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1038 of 1038 strings)

Co-authored-by: Clément Wigy <clement.wigy@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/fr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 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/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Clément Wigy <clement.wigy@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-02-04 13:03:29 -05:00
Danshil Kokil Mungur
5d4b06bbcc fix(radarr): correctly check for existing movies (#2490) 2022-02-03 10:14:04 +09:00
Weblate (bot)
92b2d32d2e feat(lang): translations update from Hosted Weblate (#2457)
* feat(lang): translated using Weblate (Serbian)

Currently translated at 49.9% (518 of 1038 strings)

feat(lang): translated using Weblate (Serbian)

Currently translated at 47.3% (492 of 1038 strings)

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1038 of 1038 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (1038 of 1038 strings)

Co-authored-by: Ben <ben.david.wallner@gmail.com>
Co-authored-by: Ben Wallner <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

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 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/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Ben <ben.david.wallner@gmail.com>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-01-27 09:08:38 -05:00
Ryan Cohen
cbcd22dfda build(deps): bump dependencies (#2482) 2022-01-27 22:41:31 +09:00
TheCatLady
8cba486249 fix: address unhandled promise rejections & bump node to v16.13 (#2398)
* fix: unhandled promise rejections

* build(deps): bump node from 14.18 to 16.13

* fix: unhandled promise rejection in new Plex users endpoint

* fix: build error

Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-27 20:00:30 +09:00
Danshil Kokil Mungur
ca184728e9 docs: update open api spec (#2478) [skip ci] 2022-01-26 21:01:28 +09:00
Ryan Cohen
822ae9eec7 style: add new tailwind prettier plugin (#2465) 2022-01-25 21:09:41 +09:00
Danshil Kokil Mungur
5b2a8f682b build(deps): bump dependencies (#2454)
* build(deps): bump react-select from 4.3.1 to 5.2.2

* build(deps): bump axios from 0.21.4 to 0.25.0

* build(deps-dev): bump lint-staged from 12.2.1 to 12.2.2

Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.2.1 to 12.2.2.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.2.1...v12.2.2)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* build(deps-dev): bump typescript from 4.5.4 to 4.5.5

Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.4 to 4.5.5.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* build(deps-dev): bump eslint-plugin-formatjs from 2.20.3 to 2.20.4

Bumps [eslint-plugin-formatjs](https://github.com/formatjs/formatjs) from 2.20.3 to 2.20.4.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/eslint-plugin-formatjs@2.20.3...eslint-plugin-formatjs@2.20.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-formatjs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* build(deps-dev): bump @commitlint/cli from 16.0.3 to 16.1.0

Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 16.0.3 to 16.1.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v16.1.0/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* build(deps): bump @tanem/react-nprogress from 4.0.3 to 4.0.4

Bumps [@tanem/react-nprogress](https://github.com/tanem/react-nprogress) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/tanem/react-nprogress/releases)
- [Changelog](https://github.com/tanem/react-nprogress/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tanem/react-nprogress/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: "@tanem/react-nprogress"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* fix: cleanup comments

* build(deps-dev): bump lint-staged from 12.2.1 to 12.3.1

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-25 02:10:37 +00:00
Danshil Kokil Mungur
d7779408d1 fix(logs): handle log message nested extra properties (#2459) 2022-01-25 11:00:53 +09:00
Weblate (bot)
b5bd6ee78f feat(lang): translations update from Hosted Weblate (#2452)
* feat(lang): translated using Weblate (Serbian)

Currently translated at 45.9% (477 of 1038 strings)

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 94.9% (986 of 1038 strings)

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

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

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

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

Currently translated at 100.0% (1038 of 1038 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1038 of 1038 strings)

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

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 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/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-01-23 14:05:09 -05:00
Weblate (bot)
99c04072e9 feat(lang): translations update from Hosted Weblate (#2436)
* 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

* 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

* 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

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

Currently translated at 100.0% (1038 of 1038 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (1016 of 1016 strings)

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

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

Currently translated at 86.9% (902 of 1037 strings)

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

Currently translated at 86.8% (901 of 1037 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>
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 (Chinese (Traditional))

Currently translated at 99.8% (1036 of 1038 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.

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

* 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

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 100.0% (1038 of 1038 strings)

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

Currently translated at 99.3% (1030 of 1037 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 100.0% (1016 of 1016 strings)

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

Currently translated at 100.0% (1016 of 1016 strings)

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

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shjosan <shjosan@kakmix.co>
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: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-01-20 20:40:38 -05:00
Danshil Kokil Mungur
f9200b7977 feat(notif): add Pushbullet channel tag (#2198)
* feat(notif): add pushbullet channel tag to server notif settings

* feat(notif): suggested changes

* docs(notif): add pushbullet channel tag
2022-01-20 11:38:24 +00:00
Danshil Kokil Mungur
eb9ca2e86f fix(requests): check for existing media of same type when requesting (#2445)
Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-20 10:42:43 +00:00
TheCatLady
0842c233d0 feat: Tautulli integration (#2230)
* feat: media/user watch history data via Tautulli

* fix(frontend): only display slideover cog button if there is media to manage

* fix(lang): tweak permission denied messages

* refactor: reorder Media section in slideover

* refactor: use new Tautulli stats API

* fix(frontend): do not attempt to fetch data when user lacks req perms

* fix: remove unneccessary get_user requests

* feat(frontend): display user avatars

* feat: add external URL setting

* feat: add play counts for past week/month

* fix(lang): tweak strings

Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-20 10:36:59 +00:00
TheCatLady
86dff12cde fix(plex): user import (#2442) 2022-01-20 06:48:35 +00:00
Danshil Kokil Mungur
ce31bef8a1 feat(logs): use separate json file to parse logs for log viewer (#2399)
Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-20 05:17:03 +00:00
TheCatLady
1f5785d6c5 feat(frontend): open media management slideover on status badge click (#2407)
* feat(frontend): open media management slideover on status badge click

* fix(frontend): use Link component for in-app badge links

* fix: check for query param value of '1'

* fix: correct query param check

* fix: available badges should still link to Plex
2022-01-20 12:40:10 +09:00
TheCatLady
114366fa4b build(deps): bump dependencies (#2427)
* build(deps): bump dependencies

* build(deps): bump next to 12.0.8

* build(deps): bump swr to 1.1.2

* build(deps): bump more dependencies

* build(deps): bump husky to 7.0.4

* fix: remove user list button outlines

* build(deps): bump dependencies again

* build(deps): bump dependencies once more
2022-01-20 10:04:34 +09:00
Danshil Kokil Mungur
5707566cf7 docs: add contribution, support and install clarifications (#2395) [skip ci]
* docs(contrib): clarify when to squash commits

* docs(support): ask for more details when requesting support

* docs(install): specify more container details

* docs(contrib): suggested changes
2022-01-19 17:02:58 +00:00
Weblate (bot)
f8b1bccda4 feat(lang): translations update from Hosted Weblate (#2428)
* feat(lang): translated using Weblate (French)

Currently translated at 99.8% (1014 of 1016 strings)

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

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

Currently translated at 98.8% (1004 of 1016 strings)

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

Currently translated at 97.9% (995 of 1016 strings)

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

Currently translated at 93.7% (952 of 1016 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/nb_NO/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: xrths <ottawas@protonmail.ch>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-01-19 11:46:28 -05:00
Weblate (bot)
e9d4b6327b feat(lang): translations update from Hosted Weblate (#2425)
* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1016 of 1016 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1016 of 1016 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Matthew Machivenyika <accounts@machivenyika.ch>
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 68.9% (701 of 1016 strings)

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

Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Matthew Machivenyika <accounts@machivenyika.ch>
Co-authored-by: exentler <gurandsrud@gmail.com>
2022-01-16 09:57:36 -05:00
TheCatLady
2535edcc7f feat(api): add additional request counts (#2426) 2022-01-16 17:13:35 +09:00
Danshil Kokil Mungur
d4438c82e3 fix(notif): show event in pop up notification for slack (#2413)
* fix(notif): show event in pop up notification for slack

fix #2408

* fix(notifs): suggested changes

* fix(notif): add fallback text to slack embed builder

Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-15 00:33:27 +00:00
TheCatLady
10651baa67 fix(notif): duplicate notification check logic (#2424) 2022-01-15 00:20:29 +00:00
TheCatLady
ff28c9bfeb fix(frontend): theme-color meta tag (#2420)
* fix(frontend): theme-color meta tag

* fix(frontend): add theme-color to offline.html

* fix(frontend): reorder application-name meta tag instead

* fix(lang): regenerate strings

* refactor: optimize Trakt logo SVG
2022-01-15 09:12:28 +09:00
Weblate (bot)
88536b1f9d feat(lang): translations update from Hosted Weblate (#2414)
* feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1001 of 1001 strings)

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

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

Currently translated at 100.0% (1013 of 1013 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patryk <byakurau1@gmail.com>
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% (1016 of 1016 strings)

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

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

Currently translated at 88.8% (903 of 1016 strings)

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

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

Currently translated at 99.9% (1015 of 1016 strings)

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

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

Currently translated at 100.0% (1016 of 1016 strings)

Co-authored-by: Kobe <kobaubarr@gmail.com>
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.3% (999 of 1016 strings)

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

Currently translated at 100.0% (1001 of 1001 strings)

Co-authored-by: Ben Wallner <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: dtalens <databio@gmail.com>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
2022-01-14 14:05:33 -05:00
TheCatLady
9cb97db13c feat(plex): selective user import (#2188)
* feat(api): allow importing of only selected Plex users

* feat(frontend): modal for importing Plex users

* feat: add alert if 'Enable New Plex Sign-In' setting is enabled

* refactor: fetch all existing Plex users in a single DB query

Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-14 19:32:53 +09:00
allcontributors[bot]
256163971f docs: add schambers as a contributor for code (#2419) [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-01-14 16:53:29 +09:00
Danshil Kokil Mungur
b31cdbf074 feat(search): search by id (#2082)
* feat(search): search by id

This adds the ability to search by ID (starting with TMDb ID).

Since there doesn't seem to be way of searching across movies, tv and persons,
I have to search through all 3 and use the first one in the order: movie -> tv -> person

Searching by ID is triggered using a 'prefix' just like in the *arrs.

* fix: missed some refactoring

* feat(search): use locale language

* feat(search): search using imdb id

* feat(search): search using tvdb id

* fix: alias type import

* fix: missed some refactoring

* fix(search): account for id being a string

* feat(search): account for movies/tvs/persons with the same id

* feat(search): remove non-null assertion

Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-14 07:52:10 +00:00
Sean Chambers
e0b6abe479 feat(notif): add Gotify agent (#2196)
* feat(notifications): adds gotify notifications

adds new settings screen for gotify notifications including url, token and types settings

fix #2183

* feat(notif): add Gotify agent
addresses PR comments, runs i18n:extract

fix #2183

* reword validationTokenRequired

change wording to indicate presence, not validity

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

* feat(notifications): gotify notifications fix

applies changes from #2077 in which Yup validation was failing for types

fix #2183

* feat(notifications): adds gotify notifications

adds new settings screen for gotify notifications including url, token and types settings

fix #2183

* feat(notif): add Gotify agent
addresses PR comments, runs i18n:extract

fix #2183

* reword validationTokenRequired

change wording to indicate presence, not validity

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

* feat(notifications): gotify notifications fix

applies changes from #2077 in which Yup validation was failing for types

fix #2183

* feat(notifications): incorporate issue feature into gotify notifications

* feat(notifications): adds gotify notifications

adds new settings screen for gotify notifications including url, token and types settings

fix #2183

* feat(notif): add Gotify agent
addresses PR comments, runs i18n:extract

fix #2183

* reword validationTokenRequired

change wording to indicate presence, not validity

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

* feat: add missing ts field

include notifyAdmin in test notification endpoint

* feat: apply formatting/line break items

add addition line break before conditional, change ordering of notifyAdmin/notifyUser in test
endpoint

* feat: remove duplicated endpoints

during rebase, notification endpoints were duplicated upon rebasing. remove duplicate routes

* feat: correct linting quirks

* feat: formatting improvements

* feat(gotify): refactor axios post to leverage 'getNotificationPayload'

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2022-01-14 01:04:25 +00:00
Weblate (bot)
879df20022 feat(lang): translations update from Hosted Weblate (#2405)
* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1000 of 1000 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1000 of 1000 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1000 of 1000 strings)

Co-authored-by: Dylan <dylan35.pub@outlook.fr>
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/fr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1001 of 1001 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1000 of 1000 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patryk <byakurau1@gmail.com>
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% (1001 of 1001 strings)

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

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

Currently translated at 90.3% (903 of 1000 strings)

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

Currently translated at 90.3% (903 of 1000 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/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1001 of 1001 strings)

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

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

Currently translated at 100.0% (1001 of 1001 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1000 of 1000 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1001 of 1001 strings)

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 100.0% (1000 of 1000 strings)

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

Co-authored-by: Dylan <dylan35.pub@outlook.fr>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2022-01-11 10:07:31 -05:00
TheCatLady
5f7538ae2b feat(discord): add 'Enable Mentions' setting (#1779) 2022-01-11 01:39:12 +00:00
Weblate (bot)
1b29b15d7c feat(lang): translations update from Hosted Weblate (#2404)
* feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (1000 of 1000 strings)

Co-authored-by: Patryk <byakurau1@gmail.com>
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% (1000 of 1000 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/sv/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1000 of 1000 strings)

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

Currently translated at 100.0% (1000 of 1000 strings)

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

Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
2022-01-09 18:00:18 +00:00
TheCatLady
399b037918 fix(lang): rename 'Media' notification types for clarity (#2400) 2022-01-09 14:46:33 +00:00
Danshil Kokil Mungur
4e56bae985 feat(ui): add trakt external link (#2367)
* feat(ui): add trakt external link

* feat(ui): move trakt to end of list of external links

Co-authored-by: Ryan Cohen <ryan@sct.dev>
2022-01-09 14:26:06 +00:00
TheCatLady
b4b2acd4fc build(docker): reduce image size (#2392) 2022-01-09 23:16:50 +09:00
Weblate (bot)
d2241a4187 feat(lang): translations update from Hosted Weblate (#2389)
* feat(lang): translated using Weblate (Serbian)

Currently translated at 44.5% (445 of 1000 strings)

feat(lang): translated using Weblate (Serbian)

Currently translated at 31.8% (318 of 1000 strings)

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1000 of 1000 strings)

feat(lang): translated using Weblate (Russian)

Currently translated at 100.0% (1000 of 1000 strings)

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

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

Currently translated at 100.0% (1000 of 1000 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (1000 of 1000 strings)

Co-authored-by: Ben Wallner <ben.david.wallner@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/de/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
2022-01-04 14:42:18 -05:00
Weblate (bot)
bd93168ba1 feat(lang): translations update from Hosted Weblate (#2379)
* feat(lang): translated using Weblate (Danish)

Currently translated at 99.6% (994 of 997 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/da/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1000 of 1000 strings)

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

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

Currently translated at 90.3% (903 of 1000 strings)

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

Currently translated at 90.3% (901 of 997 strings)

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

Currently translated at 90.5% (903 of 997 strings)

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

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

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

Currently translated at 100.0% (1000 of 1000 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1000 of 1000 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (997 of 997 strings)

Co-authored-by: DJScias <djscias@gmail.com>
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/nl/
Translation: Overseerr/Overseerr Frontend

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

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

Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: wolong gl <wolong98@gmail.com>
Co-authored-by: DJScias <djscias@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
2022-01-03 15:05:01 -05:00
TheCatLady
3e5eb4e148 fix(radarr): remove PreDB minimum availability option (#2386) 2022-01-03 13:56:19 +09:00
semantic-release-bot
2b111f1c34 chore(release): 1.28.0 2022-01-01 04:20:46 +00:00
Ryan Cohen
a6392cb2db Merge branch 'develop' 2022-01-01 13:11:57 +09:00
Weblate (bot)
b9bedac7d7 feat(lang): translations update from Hosted Weblate (#2374)
* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (997 of 997 strings)

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

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

Currently translated at 100.0% (997 of 997 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (995 of 995 strings)

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-12-31 22:07:55 -05:00
TheCatLady
1b3797cf6e fix: sort collection parts by release date (#2368)
* fix: order collection parts by release date

* feat(frontend): add posters & release years to collection request modal

* fix(frontend): wrap movie titles in collection request modal
2021-12-31 06:06:12 +00:00
Danshil Kokil Mungur
340f1a2119 feat(ui): allow admins to edit & approve request from advanced request modal (#2067)
* feat(ui): allow admins to edit & accept request from request modal

This changes the "Edit Request" button in the advanced request modal to "Edit & Accept Request"
since the only thing an admin would do next is to accept the request.
The button would stay the same for users without the manage request permissions;
users who are editing their own requests or requesting content.

* feat(ui): suggested changes
2021-12-31 05:39:04 +00:00
Weblate (bot)
cc2b2bc7a8 feat(lang): translations update from Hosted Weblate (#2366)
* feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (994 of 994 strings)

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

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

Currently translated at 100.0% (995 of 995 strings)

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

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

Currently translated at 90.0% (896 of 995 strings)

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

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

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

Currently translated at 83.7% (832 of 994 strings)

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

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

Currently translated at 100.0% (995 of 995 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (994 of 994 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (995 of 995 strings)

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

Currently translated at 100.0% (994 of 994 strings)

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

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

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (994 of 994 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (994 of 994 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/it/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: f3rr31 <5920873@disroot.org>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-12-27 12:42:10 -05:00
TheCatLady
d36c1d2929 fix(lang): add missing string (#2370) 2021-12-27 09:22:43 +09:00
Danshil Kokil Mungur
5af06bd872 fix(logs): lazily parse log message label (#2359) 2021-12-26 07:52:57 +00:00
Weblate (bot)
d437cc2539 feat(lang): translations update from Hosted Weblate (#2364)
* 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

* 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

* 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.

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% (994 of 994 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.

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

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

Currently translated at 100.0% (994 of 994 strings)

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

Currently translated at 92.3% (918 of 994 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
2021-12-25 06:38:51 -08:00
TheCatLady
30b20df37a feat: add production countries to movie/TV detail pages (#2170)
* feat: add production countries to movie/TV detail pages

* feat: add country flags to production countries
2021-12-25 01:18:06 +00:00
TheCatLady
af40212a73 feat: add quotas, advanced options, and toggles to collection request modal (#1742)
* feat: add quotas, advanced options, and toggles to collection request modal

* fix: use correct requiredquota strings

* refactor: clean up collection part request status logic

* revert: undo changes to effect dependencies

* fix(lang): tweak TV request modal request button strings

* fix: don't try to fetch other users' quotas without MANAGE_USERS perm
2021-12-25 01:10:33 +00:00
Danshil Kokil Mungur
488874fc17 fix(servarr): handle servaarr server being unavailable when scanning downloads (#2358) 2021-12-25 01:02:03 +00:00
Dan Hand
2ded8f5484 feat(frontend): add Discovery+ to network slider (#2345)
* feat(frontend): add Discovery+ to network slider

* Make filter consistent with other networks, and update image path
2021-12-25 00:56:02 +00:00
Weblate (bot)
50dc9341dd feat(lang): translations update from Hosted Weblate (#2346)
* feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (992 of 992 strings)

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

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

Currently translated at 99.1% (984 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 99.1% (984 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 99.1% (984 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 90.7% (900 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 88.9% (882 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 72.1% (716 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 56.9% (565 of 992 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
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 90.7% (900 of 992 strings)

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

Currently translated at 90.6% (899 of 992 strings)

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

Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Jassy lin <linjiaxinme@gmail.com>
2021-12-23 14:59:38 -05:00
TheCatLady
fcb0dcf5be fix(notif): only send MEDIA_AVAILABLE notifications for non-declined requests (#2343) 2021-12-12 11:25:26 +09:00
Weblate (bot)
33fe0bdd1e feat(lang): translations update from Hosted Weblate (#2341)
* feat(lang): translated using Weblate (Greek)

Currently translated at 80.3% (797 of 992 strings)

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

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

Currently translated at 98.8% (981 of 992 strings)

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

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

Currently translated at 100.0% (992 of 992 strings)

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

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

Currently translated at 98.0% (973 of 992 strings)

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

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

Currently translated at 43.4% (431 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 43.4% (431 of 992 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
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 (Polish)

Currently translated at 100.0% (992 of 992 strings)

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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% (992 of 992 strings)

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

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

Currently translated at 91.4% (907 of 992 strings)

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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 100.0% (992 of 992 strings)

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
2021-12-11 19:00:06 +00:00
Weblate (bot)
3f7ef7af97 feat(lang): translations update from Hosted Weblate (#2336)
* feat(lang): translated using Weblate (Russian)

Currently translated at 100.0% (992 of 992 strings)

Co-authored-by: Romans Pokrovskis <motivated.it@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 27.7% (275 of 992 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 27.7% (275 of 992 strings)

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

Co-authored-by: Romans Pokrovskis <motivated.it@gmail.com>
Co-authored-by: Simon Thyregod <p2v2w8ar@simon.thyregod.eu>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
2021-12-10 09:37:08 -05:00
TheCatLady
91bfff71b7 fix(requests): do not fail request edits if acting user lacks Manage Users permission (#2338)
* fix(api): fix requestedBy logic in request edits

* fix(frontend): do not display empty advanced request options box

* fix(frontend): set max height on modal backdrop
2021-12-10 19:14:14 +09:00
TheCatLady
dc7f959cb4 fix(notif): correct issue notif action URLs (#2333) 2021-12-09 13:49:50 +00:00
Danshil Kokil Mungur
93b5ea20ca fix(servarr): handle baseurl error when testing connection (#2294)
* fix(servarr): handle base url error when testing servarr connection

* fix(servarr): suggested changes
2021-12-08 04:38:17 +04:00
Danshil Kokil Mungur
f284e4ab97 fix(logs): handle unexpected log messages (#2303)
* fix(logs): handle unexpected log messages

* fix(logs): update regexp for json to appease the lgtm gods
2021-12-08 00:20:47 +00:00
Weblate (bot)
febf0677b8 feat(lang): translations update from Hosted Weblate (#2325)
* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (992 of 992 strings)

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

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

Currently translated at 100.0% (992 of 992 strings)

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

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

Currently translated at 100.0% (992 of 992 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patryk <byakurau1@gmail.com>
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% (992 of 992 strings)

Co-authored-by: thu3n <elias.thuen@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (992 of 992 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (992 of 992 strings)

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

Co-authored-by: Comore <comore@como.re>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: thu3n <elias.thuen@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-12-08 00:12:26 +00:00
TheCatLady
7f330aff2e fix: secure session cookie (#2308) 2021-12-08 00:05:39 +00:00
TheCatLady
88a8c1aa59 feat(notif): 4K media notifications (#2324) 2021-12-08 08:56:41 +09:00
Weblate (bot)
68112faefb feat(lang): translations update from Hosted Weblate (#2320)
* feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (987 of 987 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patryk <byakurau1@gmail.com>
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% (987 of 987 strings)

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

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

Currently translated at 100.0% (992 of 992 strings)

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

Currently translated at 100.0% (992 of 992 strings)

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

Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Kobe <kobaubarr@gmail.com>
2021-12-04 23:48:45 +00:00
TheCatLady
f2375c902b fix(ui): request badge styling in request list (#2302) 2021-12-04 23:43:03 +00:00
TheCatLady
ed53810fb3 fix: handle Plex library settings migration failure gracefully (#2254)
* fix: handle Plex library settings migration failure gracefully

* fix: handle failure in syncLibraries() instead
2021-12-05 02:42:15 +09:00
TheCatLady
c9ffac33f7 feat(notif): issue notifications (#2242)
* feat(notif): issue notifications

* refactor: dedupe test notification strings

* fix: webhook key parsing

* fix(notif): skip send for admin who requested on behalf of another user

* fix(notif): send comment notifs to admins when other admins reply

* fix(notif): also send resolved notifs to admins, and reopened notifs to issue creator

* fix: don't send duplicate notifications

* fix(lang): tweak notification description strings

* fix(notif): tweak Slack notification styling

* fix(notif): tweak Pushbullet & Telegram notification styling

* docs: reformat webhooks page

* fix(notif): add missing issue_type & issue_status variables to LunaSea notif payloads

* fix: explicitly attach media & issue objects where applicable

* fix(notif): correctly notify both notifyUser and managers where applicable

* fix: update default webhook payload for new installs

* fix(notif): add missing comment_message to LunaSea notif payload

* refactor(sw): simplify notificationclick event listener logic

* fix(notif): add missing event description for MEDIA_AVAILABLE notifications
2021-12-04 21:24:26 +09:00
Weblate (bot)
6245be1e10 feat(lang): translations update from Hosted Weblate (#2315)
* feat(lang): translated using Weblate (Russian)

Currently translated at 99.7% (985 of 987 strings)

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

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

Currently translated at 100.0% (987 of 987 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 100.0% (987 of 987 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/pl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (987 of 987 strings)

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

Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-12-02 02:04:34 +00:00
TheCatLady
c760ceaa5f feat(lang): add Polish display language (#2261) 2021-12-02 01:58:46 +00:00
Weblate (bot)
92732fcb42 feat(lang): translations update from Hosted Weblate (#2277)
* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (987 of 987 strings)

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

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

Currently translated at 50.1% (495 of 987 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 41.0% (405 of 987 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 39.2% (387 of 987 strings)

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

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

Currently translated at 61.2% (605 of 987 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 61.2% (605 of 987 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 53.2% (526 of 987 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/pl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 87.1% (860 of 987 strings)

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

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

Currently translated at 100.0% (987 of 987 strings)

Co-authored-by: Kobe <kobaubarr@gmail.com>
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 92.0% (909 of 987 strings)

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

Currently translated at 89.6% (885 of 987 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 (Italian)

Currently translated at 100.0% (987 of 987 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (987 of 987 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/it/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: 0x0f40f0 <dark@3xd.de>
Co-authored-by: Core Intel <rideonit@seznam.cz>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-11-29 17:39:08 -05:00
TheCatLady
d48a7ba518 fix: allow basic HTTP auth in hostname validation (#2307) 2021-11-29 21:27:02 +09:00
Weblate (bot)
d401e33249 feat(lang): translated using Weblate (Chinese (Traditional)) (#2272)
Currently translated at 100.0% (987 of 987 strings)

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

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
2021-11-11 18:53:46 +00:00
Danshil Kokil Mungur
b423dc167d feat(search): close search bar when hitting return (#2260) 2021-11-11 18:46:56 +00:00
TheCatLady
f1cd0878a5 fix(docker): explicitly install python3 (#2273) [skip ci] 2021-11-11 13:08:22 +09:00
Weblate (bot)
b1b367aac6 feat(lang): translations update from Weblate (#2265)
* feat(lang): translated using Weblate (French)

Currently translated at 96.1% (949 of 987 strings)

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

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

Currently translated at 98.2% (970 of 987 strings)

feat(lang): translated using Weblate (German)

Currently translated at 97.7% (965 of 987 strings)

Co-authored-by: Ben Wallner <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

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

Currently translated at 99.7% (985 of 987 strings)

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

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

Currently translated at 94.5% (933 of 987 strings)

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

Co-authored-by: Sullivan Leduc <susu.leduc@gmail.com>
Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-11-08 11:07:03 -05:00
Weblate (bot)
99d50004e5 feat(lang): translations update from Weblate (#2252)
* 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

* 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

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

Currently translated at 100.0% (987 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 99.7% (985 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
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

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

Currently translated at 23.5% (232 of 987 strings)

feat(lang): translated using Weblate (Polish)

Currently translated at 17.9% (177 of 987 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patryk <byakurau1@gmail.com>
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% (987 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 87.1% (860 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Eric <spice2wolf@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/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (987 of 987 strings)

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

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

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

Currently translated at 100.0% (987 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Patryk <byakurau1@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Kobe <kobaubarr@gmail.com>
2021-11-04 17:23:18 -04:00
TheCatLady
c79dc9f70f fix: add missing route guards to issues pages (#2235)
* fix: users should always be able to view their own issues

* fix: apply route guards to issues pages instead

* fix(api): only allow users w/ issue perms to edit comments / delete issues
2021-10-31 15:56:59 +00:00
TheCatLady
3ec4a9c76e fix(frontend): more issues-related fixes (#2234)
* fix(frontend): more issues-related fixes

* fix: permission VIEW_ISSUES is also sufficient for viewing issues in slideover

* fix(frontend): only display issue notif types user is eligible to receive

* fix: don't display issues block in slideover if no open issues

* fix: move year out of link in issue details header

* fix: use 'view' global string for issue block button

* fix: issue/request/user list sort options
2021-10-31 15:45:15 +00:00
Weblate (bot)
8c49309c35 feat(lang): translations update from Weblate (#2247)
* feat(lang): translated using Weblate (German)

Currently translated at 91.5% (904 of 987 strings)

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

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

Currently translated at 100.0% (987 of 987 strings)

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

Co-authored-by: doob187 <amderkum@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
2021-10-31 15:39:08 +00:00
TheCatLady
78a8091bcd fix(frontend): setup page backdrops (#2251) 2021-10-31 15:32:57 +00:00
TheCatLady
bba09d69c1 fix(issues): only allow edit of own comments & do not allow non-admin delete of issues with comments (#2248) 2021-10-31 08:54:01 +09:00
Weblate (bot)
0828b008ba feat(lang): translations update from Weblate (#2244)
* feat(lang): translated using Weblate (French)

Currently translated at 96.7% (955 of 987 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 (Spanish)

Currently translated at 100.0% (987 of 987 strings)

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

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

Currently translated at 87.7% (866 of 987 strings)

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

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

Currently translated at 100.0% (987 of 987 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 94.8% (936 of 987 strings)

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

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

Currently translated at 91.9% (908 of 987 strings)

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

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

Currently translated at 100.0% (987 of 987 strings)

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

Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
2021-10-29 05:07:12 -04:00
TheCatLady
05d2dac3ab docs: add Overseerr Assistant to third-party integrations (#2243) [skip ci] 2021-10-28 14:21:54 +04:00
Weblate (bot)
2b0b8e05d9 feat(lang): translations update from Weblate (#2241)
* feat(lang): translated using Weblate (Serbian)

Currently translated at 14.3% (142 of 987 strings)

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 87.7% (866 of 987 strings)

Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 94.2% (930 of 987 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 94.2% (930 of 987 strings)

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

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

Currently translated at 100.0% (987 of 987 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 90.1% (890 of 987 strings)

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

Currently translated at 88.9% (878 of 987 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

Co-authored-by: Dalibor Radovanović <darkobg@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
2021-10-27 22:24:35 -04:00
Weblate (bot)
62b3dc5471 feat(lang): translations update from Weblate (#2226)
* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 51.0% (504 of 987 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 50.2% (490 of 976 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 96.4% (952 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 98.9% (966 of 976 strings)

feat(lang): translated using Weblate (French)

Currently translated at 98.2% (959 of 976 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 100.0% (987 of 987 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (987 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Spanish)

Currently translated at 99.5% (985 of 989 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 90.1% (880 of 976 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ricardo González <notorius28@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/es/
Translation: Overseerr/Overseerr Frontend

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 86.0% (849 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 88.3% (862 of 976 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Eric <spice2wolf@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/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 99.5% (983 of 987 strings)

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

Currently translated at 99.5% (983 of 987 strings)

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

Currently translated at 99.0% (978 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 99.5% (985 of 989 strings)

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

Currently translated at 99.5% (972 of 976 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 95.1% (930 of 977 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.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

Currently translated at 100.0% (987 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (989 of 989 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (890 of 890 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 89.8% (877 of 976 strings)

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

Currently translated at 89.8% (877 of 976 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: costaht <habnertc@gmail.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

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

Currently translated at 93.4% (922 of 987 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Italian)

Currently translated at 96.3% (941 of 977 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: costaht <habnertc@gmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
2021-10-26 11:43:20 -04:00
TheCatLady
8d2968572a fix(email): use decrypted private key (#2232) 2021-10-26 18:49:27 +09:00
TheCatLady
216447121b fix(frontend): use consistent formatting & strings (#2231)
* fix(frontend): use consistent formatting & strings

* fix(lang): remove duplicated status strings

* fix(frontend): reduce height of items in request & issue lists

* fix(frontend): issue description textarea label should be a label element

* refactor: remove unnecessary reduce

* fix: remove small avatar underneath issue comments

* fix(frontend): don't hide Pushover app token tip
2021-10-26 00:07:00 +09:00
TheCatLady
aeb7a48d72 feat(notif): add Pushbullet and Pushover agents to user notification settings (#1740)
* feat(notif): add Pushbullet and Pushover agents to user notification settings

* docs(notif): add "hint" about user notifications to Pushbullet and Pushover pages

* fix: regenerate DB migration
2021-10-25 19:46:05 +09:00
TheCatLady
ab20c21184 fix(lang): string edits (#2229)
* fix(lang): string edits

* fix: correct notif type ordering

* fix(lang): a few more edits

* fix(frontend): align & wrap media attributes properly

* fix(lang): use consistent cache names
2021-10-25 01:08:03 +09:00
TheCatLady
a35209c739 chore(github): add stalebot-exempt labels (#2225) [skip ci]
* chore(github): add stalebot-exempt labels

* chore(github): also never stale priority:medium
2021-10-24 15:42:52 +00:00
Ryan Cohen
e402c42aaa feat: issues (#2180) 2021-10-24 12:44:20 +00:00
TheCatLady
6565c7dd9b docs: add FAQ entry about mobile apps (#2228) [skip ci]
* docs: grammar correction

* docs: add FAQ about mobile apps

* docs: remove Overseerr-Extension for now
2021-10-24 16:35:36 +04:00
TheCatLady
dca363809d docs: add Overseerr-Extension to 3rd-party integrations (#2227) [skip ci] 2021-10-24 07:58:40 -04:00
Weblate (bot)
85aec4f892 fix(lang): translations update from Weblate (#2212)
* feat(lang): translated using Weblate (Japanese)

Currently translated at 56.8% (506 of 890 strings)

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

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

Currently translated at 99.7% (888 of 890 strings)

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

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

Currently translated at 98.9% (881 of 890 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_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (890 of 890 strings)

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

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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
2021-10-23 18:50:36 -04:00
semantic-release-bot
6d55b50e5e chore(release): 1.27.0 2021-10-19 14:01:17 +00:00
sct
f7dc6256ba Merge branch 'develop' 2021-10-19 22:54:07 +09:00
Weblate (bot)
0a6ef6cc81 feat(lang): translations update from Weblate (#2210)
* feat(lang): translated using Weblate (Greek)

Currently translated at 93.8% (835 of 890 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 (Italian)

Currently translated at 98.6% (878 of 890 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 (Japanese)

Currently translated at 56.8% (506 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Japanese)

Currently translated at 54.5% (485 of 889 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/ja/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 16.1% (144 of 890 strings)

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

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

Currently translated at 74.8% (666 of 890 strings)

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

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

Currently translated at 98.7% (879 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (French)

Currently translated at 99.3% (883 of 889 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/fr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 98.6% (878 of 890 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 (Spanish)

Currently translated at 99.5% (886 of 890 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 99.5% (886 of 890 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 98.8% (880 of 890 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 98.8% (880 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ricardo González <notorius28@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/es/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 99.7% (888 of 890 strings)

feat(lang): translated using Weblate (Russian)

Currently translated at 99.7% (888 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
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 (Catalan)

Currently translated at 98.4% (876 of 890 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 44.3% (395 of 890 strings)

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

Currently translated at 5.5% (49 of 890 strings)

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

Currently translated at 0.0% (0 of 890 strings)

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
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% (890 of 890 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (890 of 890 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (890 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shjosan <shjosan@kakmix.co>
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 (Chinese (Simplified))

Currently translated at 98.9% (881 of 890 strings)

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

Currently translated at 98.9% (881 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 99.1% (881 of 889 strings)

Co-authored-by: Eric <spice2wolf@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/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (890 of 890 strings)

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

Currently translated at 100.0% (890 of 890 strings)

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

Currently translated at 100.0% (890 of 890 strings)

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

Currently translated at 99.8% (888 of 889 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

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

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

Currently translated at 98.0% (873 of 890 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 (Hungarian)

Currently translated at 98.3% (875 of 890 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 100.0% (890 of 890 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (890 of 890 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (890 of 890 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (889 of 889 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobaubarr@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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 99.5% (886 of 890 strings)

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

Currently translated at 99.5% (886 of 890 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: 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

Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
2021-10-18 10:45:58 -04:00
TheCatLady
b3b421a674 fix(email): do not attempt to display logo if app URL not configured (#2125)
* fix(email): do not attempt to display logo if app URL not configured

* fix(email): prevent Gmail from turning usernames with periods into hyperlinks

* fix(email): fix(email): use displayName instead of username/plexUserName and improve Gmail link fix
2021-10-18 14:08:50 +00:00
TheCatLady
032c14a226 feat(ui): link processing/requested status badges to service URL (#1761)
* feat(ui): link processing/requested status badges to service URL where available

* refactor: add URL prop to Badge component

* fix(css): tweak font weight of media rating values and request card link hover effect

* fix: only set StatusBadge serviceUrl for admins
2021-10-16 15:53:38 +00:00
TheCatLady
084a842a4f fix(ui): refinements for 'About' page (#2173)
* fix(ui): refinements for 'About' page

* fix: remove unneeded GithubLink function

* fix: display/link badges appropriately
2021-10-16 15:46:18 +00:00
Danshil Kokil Mungur
739f667b54 feat(servarr): auto fill base url when testing service if missing (#1995)
* feat(servarr): auto fill base url when testing service if missing

This will fill the base URL of the *arr service only if it's missing and the base URL hasn't been
provided beforehand

* fix(servarr): replace redundant check

* fix(servarr): suggested changes
2021-10-16 15:27:03 +00:00
TheCatLady
54e9071e90 build(deps): bump node to 14.18 (#2209) [skip ci] 2021-10-16 22:17:43 +09:00
Danshil Kokil Mungur
d0ac74ea4b fix(scripts): update migration scripts (#2208) [skip ci] 2021-10-16 20:15:50 +09:00
allcontributors[bot]
54e9514489 docs: add Nackophilz as a contributor for translation (#2203) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-16 01:31:29 +00:00
TheCatLady
1e402f710b feat(frontend): add Hulu to network slider (#2204)
* feat(frontend): add Hulu to network slider

* fix: move Hulu after Apple TV+
2021-10-16 01:11:11 +00:00
TheCatLady
2f204b9952 fix(frontend): notification type validation (#2207) 2021-10-16 01:02:02 +00:00
TheCatLady
3486d0bf55 feat: dynamically fetch login screen backdrop images (#2206)
* feat: dynamically fetch login screen backdrop images

* fix: remove media check from backdrops endpoint

* fix: remove mapping and work with TMDb data directly
2021-10-16 09:54:15 +09:00
Weblate (bot)
492d8e3daa feat(lang): translations update from Weblate (#2202)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (887 of 889 strings)

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

Currently translated at 100.0% (883 of 883 strings)

Co-authored-by: Clément Wigy <clement.wigy@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 44.9% (397 of 883 strings)

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

Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Clément Wigy <clement.wigy@gmail.com>
Co-authored-by: Core Intel <rideonit@seznam.cz>
2021-10-15 16:30:15 +00:00
TheCatLady
0b1c2b1745 chore(github): desired-behavior field should be textarea (#2201) [skip ci] 2021-10-16 01:23:25 +09:00
TheCatLady
c767f5254c chore(github): issue forms (#2200)
* chore(github): issue forms

* chore(github): remove now-unneeded 'Invalid Template' action
2021-10-16 01:11:38 +09:00
Danshil Kokil Mungur
82614ca441 feat(jobs): allow modifying job schedules (#1440)
* feat(jobs): backend implementation

* feat(jobs): initial frontend implementation

* feat(jobs): store job settings as Record

* feat(jobs): use heroicons/react instead of inline svgs

* feat(jobs): use presets instead of cron expressions

* feat(jobs): ran `yarn i18n:extract`

* feat(jobs): suggested changes

- use job ids in settings
- add intervalDuration to jobs to allow choosing only minutes or hours for the job schedule
- move job schedule defaults to settings.json
- better TS types for jobs in settings cache component
- make suggested changes to wording
- plural form for label when job schedule can be defined in minutes
- add fixed job interval duration
- add predefined interval choices for minutes and hours
- add new schema for job to overseerr api

* feat(jobs): required change for CI to not fail

* feat(jobs): suggested changes

* fix(jobs): revert offending type refactor
2021-10-15 21:23:39 +09:00
allcontributors[bot]
5683f55ebf docs: add sr093906 as a contributor for translation (#2192) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-13 11:05:11 -04:00
Weblate (bot)
dce10f743f feat(lang): translations update from Weblate (#2185)
* feat(lang): translated using Weblate (German)

Currently translated at 99.8% (882 of 883 strings)

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

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

Currently translated at 100.0% (883 of 883 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (883 of 883 strings)

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

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

Currently translated at 5.4% (48 of 883 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 (Swedish)

Currently translated at 100.0% (883 of 883 strings)

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

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

Currently translated at 99.7% (881 of 883 strings)

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

Currently translated at 99.7% (881 of 883 strings)

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

Currently translated at 99.7% (881 of 883 strings)

Co-authored-by: Eric <spice2wolf@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/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (883 of 883 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 (Portuguese (Portugal))

Currently translated at 99.3% (877 of 883 strings)

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

Currently translated at 99.2% (876 of 883 strings)

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

Co-authored-by: JoKerIsCraZy <jokeriscrazy@yahoo.com>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Eric <spice2wolf@gmail.com>
2021-10-13 10:54:01 -04:00
TheCatLady
b7ba90d67b docs: improve Docker documentation (#2109) [skip ci]
* docs: improve Docker documentation

* docs: suggested changes

* docs: minor formatting changes

* docs: add Doplarr to list of third-party integrations

* docs: rename docker run tab

* docs: add link to official Docker CLI docs

* docs: minor edits
2021-10-10 03:29:27 +04:00
allcontributors[bot]
00ee535077 docs: add skafte1990 as a contributor for translation (#2182) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-09 23:13:55 +00:00
Weblate (bot)
e3312cef33 feat(lang): translations update from Weblate (#2179)
* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (883 of 883 strings)

Co-authored-by: Kobe <kobaubarr@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% (883 of 883 strings)

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

Currently translated at 100.0% (883 of 883 strings)

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

Currently translated at 100.0% (883 of 883 strings)

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

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

Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
2021-10-09 19:04:18 -04:00
allcontributors[bot]
2957e156a5 docs: add ty4ko as a contributor for translation (#2178) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-09 22:51:41 +00:00
TheCatLady
daf64532bf docs: add FAQ entry about broken logos in email notifications (#2134) [skip ci] 2021-10-09 18:44:07 -04:00
allcontributors[bot]
1286d6f0f9 docs: add adan89lion as a contributor for translation (#2177) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-08 15:56:48 +00:00
TheCatLady
8d8db6cf5d feat(lang): add Czech and Danish display languages (#2176) 2021-10-08 15:47:30 +00:00
allcontributors[bot]
82d5d18ee7 docs: add Simoneu01 as a contributor for translation (#2175) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-08 15:23:20 +00:00
Weblate (bot)
c73cf7b19c feat(lang): translations update from Weblate (#2101)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (883 of 883 strings)

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

Currently translated at 100.0% (883 of 883 strings)

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

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

Currently translated at 100.0% (883 of 883 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/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (883 of 883 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (883 of 883 strings)

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

Currently translated at 98.4% (869 of 883 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 (Russian)

Currently translated at 99.7% (881 of 883 strings)

feat(lang): translated using Weblate (Russian)

Currently translated at 99.7% (881 of 883 strings)

feat(lang): translated using Weblate (Russian)

Currently translated at 64.3% (568 of 883 strings)

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

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

Currently translated at 43.9% (388 of 883 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 43.7% (386 of 883 strings)

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

Co-authored-by: Core Intel <rideonit@seznam.cz>
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/cs/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 5.5% (49 of 883 strings)

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

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

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

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

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

Currently translated at 100.0% (883 of 883 strings)

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

Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: costaht <habnertc@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Sergey Moiseev <ty4ko@bk.ru>
Co-authored-by: Core Intel <rideonit@seznam.cz>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: Nicolai Skafte <skafte1990@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
2021-10-08 11:16:48 -04:00
allcontributors[bot]
4f36ca718f docs: add GoByeBye as a contributor for translation (#2172) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-08 15:06:45 +00:00
TheCatLady
0edb1f452b fix(api): return queried user's requests instead of own requests (#2174) 2021-10-08 23:59:21 +09:00
allcontributors[bot]
63789918c5 docs: add JoKerIsCraZy as a contributor for translation (#2171) [skip ci]
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-10-08 13:43:09 +00:00
TheCatLady
a4dca2356b feat: display release dates for theatrical, digital, and physical release types (#1492)
* feat: display release dates for theatrical, digital, and physical release types

* fix(ui): use disc icon for physical release

* style: reformat to make new version of Prettier happy
2021-10-08 13:27:07 +00:00
TheCatLady
50ce198471 fix: apply request overrides iff override & selected servers match (#2164) 2021-10-08 13:19:47 +00:00
TheCatLady
a20f395c94 fix(api): use query builder for user requests endpoint (#2119) 2021-10-08 13:14:20 +00:00
allcontributors[bot]
cbfe9beb31 docs: add sootylunatic as a contributor for translation (#2102) [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>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
2021-10-08 12:52:40 +00:00
TheCatLady
2333a81cfc chore(github): update CODEOWNERS to allow collaborators to review/merge all-contributors PRs (#2165) [skip ci] 2021-10-08 21:46:18 +09:00
semantic-release-bot
f1df1caea5 chore(release): 1.26.1 2021-09-20 00:22:06 +00:00
sct
d23c120dc0 Merge branch 'develop' 2021-09-20 09:14:29 +09:00
semantic-release-bot
353efa39d0 chore(release): 1.26.0 2021-09-19 12:15:52 +00:00
sct
21cdf7d113 Merge branch 'develop' 2021-09-19 21:08:16 +09:00
semantic-release-bot
f858f025fe chore(release): 1.25.0 2021-06-10 10:25:49 +00:00
sct
c92bb134d4 Merge branch 'develop' 2021-06-10 19:18:12 +09:00
semantic-release-bot
7e0e2a92e3 chore(release): 1.24.0 2021-05-05 08:57:48 +00:00
sct
e840489e57 Merge branch 'develop' 2021-05-05 17:50:27 +09:00
sct
800f4668ec chore: fix gitattributes for images [skip ci] 2021-05-05 17:49:55 +09:00
semantic-release-bot
f2c37babeb chore(release): 1.23.2 2021-04-21 12:28:28 +00:00
sct
24095eac03 Merge branch 'develop' 2021-04-21 21:20:07 +09:00
semantic-release-bot
8c782e0c93 chore(release): 1.23.1 2021-04-16 14:19:53 +00:00
sct
08ce7f24b8 Merge branch 'develop' 2021-04-16 23:11:54 +09:00
semantic-release-bot
67e794aca4 chore(release): 1.23.0 2021-04-16 12:38:05 +00:00
sct
c9c429d027 Merge branch 'develop' 2021-04-16 21:30:30 +09:00
semantic-release-bot
31ac11f4eb chore(release): 1.22.0 2021-04-01 06:33:50 +00:00
sct
f072a4e11d Merge branch 'develop' 2021-04-01 15:27:10 +09:00
semantic-release-bot
b24c23e549 chore(release): 1.21.1 2021-03-15 08:04:26 +00:00
sct
e602e9f758 Merge branch 'develop' 2021-03-15 07:56:33 +00:00
semantic-release-bot
afd46ebee8 chore(release): 1.21.0 2021-03-15 02:00:59 +00:00
sct
be47d1e56c Merge branch 'develop' 2021-03-15 01:52:10 +00:00
semantic-release-bot
0e612f01f1 chore(release): 1.20.1 2021-02-28 04:10:48 +00:00
sct
cd9d9ae8d2 Merge branch 'develop' 2021-02-28 04:03:43 +00:00
semantic-release-bot
ea2765f8c7 chore(release): 1.20.0 2021-02-23 12:33:56 +00:00
sct
448e7c3a7d Merge branch 'develop' 2021-02-23 12:26:58 +00:00
sct
6ab3cd77a7 Merge branch 'develop' 2021-02-23 12:09:08 +00:00
semantic-release-bot
3dfbb0e0a5 chore(release): 1.19.1 2021-02-06 05:14:36 +00:00
TheCatLady
726f62b9b6 fix(ui): Fix webhook URL validation regex (#864) 2021-02-06 14:08:24 +09:00
sct
81c30347f6 Merge branch 'develop' 2021-02-05 14:15:02 +00:00
semantic-release-bot
a397893217 chore(release): 1.19.0 2021-02-05 10:32:05 +00:00
sct
12c5bbd778 Merge branch 'develop' 2021-02-05 10:25:43 +00:00
sct
06261c1118 ci(snapcraft): remove --unshallow argument from fetch in Prepare step 2021-01-30 06:31:34 +00:00
semantic-release-bot
686fe6fdf8 chore(release): 1.18.0 2021-01-30 06:13:16 +00:00
sct
c80a1fa261 Merge branch 'develop' 2021-01-30 06:06:49 +00:00
semantic-release-bot
6167011ee5 chore(release): 1.17.2 2021-01-20 07:27:33 +00:00
sct
bc399d95a0 Merge branch 'develop' 2021-01-20 07:21:48 +00:00
semantic-release-bot
519dd17d37 chore(release): 1.17.1 2021-01-19 10:24:57 +00:00
sct
0761593411 Merge branch 'develop' 2021-01-19 10:18:51 +00:00
semantic-release-bot
caa45415b1 chore(release): 1.17.0 2021-01-19 07:58:37 +00:00
sct
ed297d9d6c Merge branch 'develop' 2021-01-19 07:52:47 +00:00
semantic-release-bot
c6486c3643 chore(release): 1.16.0 2021-01-07 09:58:09 +00:00
sct
f9695068a8 Merge branch 'develop' 2021-01-07 09:53:04 +00:00
semantic-release-bot
fd90adba4d chore(release): 1.15.0 2021-01-04 12:01:25 +00:00
sct
970aa612fe Merge pull request #570 from sct/release/release-20210104
Release
2021-01-04 20:56:34 +09:00
sct
71169944dc Merge branch 'develop' into release/release-20210104 2021-01-04 11:54:45 +00:00
semantic-release-bot
d77f9eb44e chore(release): 1.14.1 2021-01-02 08:57:24 +00:00
sct
22f2037ea6 fix(holiday): remove special holiday slider 2021-01-02 08:33:24 +00:00
semantic-release-bot
2cdf755c20 chore(release): 1.14.0 2020-12-25 11:52:44 +00:00
sct
f99ab47c01 Merge branch 'develop' 2020-12-25 20:47:19 +09:00
semantic-release-bot
7e4175ff21 chore(release): 1.13.0 2020-12-23 12:44:01 +00:00
sct
4157432aa7 Merge branch 'develop' 2020-12-23 21:39:00 +09:00
semantic-release-bot
741b2c4e9f chore(release): 1.12.1 2020-12-22 03:17:58 +00:00
sct
6dbe9ea814 Merge branch 'develop' 2020-12-22 12:12:25 +09:00
semantic-release-bot
96328ab5c6 chore(release): 1.12.0 2020-12-22 02:26:31 +00:00
sct
c31709681a Merge branch 'develop' 2020-12-22 11:21:19 +09:00
semantic-release-bot
0a3d7c5915 chore(release): 1.11.0 2020-12-20 01:18:39 +00:00
sct
912c63e562 Merge branch 'develop' 2020-12-20 10:12:59 +09:00
semantic-release-bot
077513151e chore(release): 1.10.0 2020-12-19 04:54:22 +00:00
sct
191f39e8bd Merge branch 'develop' 2020-12-19 13:49:05 +09:00
semantic-release-bot
048b6c1d90 chore(release): 1.9.1 2020-12-18 16:08:58 +00:00
sct
a6a065ac0b Merge branch 'develop' 2020-12-19 01:03:48 +09:00
semantic-release-bot
abb0b2d763 chore(release): 1.9.0 2020-12-18 12:05:35 +00:00
sct
d2668d3f49 Merge branch 'develop' 2020-12-18 21:00:48 +09:00
semantic-release-bot
bee6eb288f chore(release): 1.8.0 2020-12-17 15:54:40 +00:00
sct
3473aed27e Merge branch 'develop' 2020-12-17 15:49:18 +00:00
semantic-release-bot
a9144a2167 chore(release): 1.7.0 2020-12-17 14:09:58 +00:00
sct
59822d6e42 Merge branch 'develop' 2020-12-17 14:05:24 +00:00
semantic-release-bot
0bd1bee1fa chore(release): 1.6.0 2020-12-16 05:56:57 +00:00
sct
46326f9d16 Merge branch 'develop' 2020-12-16 05:52:26 +00:00
semantic-release-bot
54b96cc451 chore(release): 1.5.0 2020-12-15 09:05:37 +00:00
sct
7d2a187865 Merge branch 'develop' 2020-12-15 09:00:44 +00:00
semantic-release-bot
17df5dd072 chore(release): 1.4.0 2020-12-15 00:53:05 +00:00
sct
4b48a36ad3 Merge branch 'develop' 2020-12-15 00:48:28 +00:00
semantic-release-bot
16bd3e9fd5 chore(release): 1.3.2 2020-12-14 13:11:15 +00:00
sct
6360c82898 Merge branch 'develop' 2020-12-14 13:06:22 +00:00
semantic-release-bot
f1df9eeb2f chore(release): 1.3.1 2020-12-14 08:03:51 +00:00
sct
a3973791fb Merge branch 'develop' 2020-12-14 07:59:18 +00:00
semantic-release-bot
157ab80be1 chore(release): 1.3.0 2020-12-14 05:32:31 +00:00
sct
ac0f95b773 Merge branch 'develop' 2020-12-14 05:27:01 +00:00
semantic-release-bot
45ebfb937e chore(release): 1.2.0 2020-12-11 22:55:12 +00:00
sct
85325cda7f Merge branch 'develop' 2020-12-11 22:50:33 +00:00
semantic-release-bot
5fc02d0d94 chore(release): 1.1.0 2020-12-08 08:21:58 +00:00
sct
16902f85fb Merge branch 'develop' 2020-12-08 08:16:24 +00:00
semantic-release-bot
17b5a0bf9c chore(release): 1.0.0 2020-12-06 01:58:28 +00:00
322 changed files with 29767 additions and 15140 deletions

View File

@@ -539,23 +539,138 @@
"contributions": [
"code"
]
}
},
{
"login": "Fallenbagel",
"name": "Mohamed Nuvaas",
"avatar_url": "https://avatars.githubusercontent.com/u/98979876?s=96&v=4",
"profile": "https://github.com/nicospz",
"login": "sootylunatic",
"name": "sootylunatic",
"avatar_url": "https://avatars.githubusercontent.com/u/36486087?v=4",
"profile": "https://github.com/sootylunatic",
"contributions": [
"code",
"logo",
"design"
"translation"
]
},
{
"login": "JoKerIsCraZy",
"name": "JoKerIsCraZy",
"avatar_url": "https://avatars.githubusercontent.com/u/47474211?v=4",
"profile": "https://github.com/JoKerIsCraZy",
"contributions": [
"translation"
]
},
{
"login": "GoByeBye",
"name": "Daddie0",
"avatar_url": "https://avatars.githubusercontent.com/u/33762262?v=4",
"profile": "https://daddie.dev",
"contributions": [
"translation"
]
},
{
"login": "Simoneu01",
"name": "Simone",
"avatar_url": "https://avatars.githubusercontent.com/u/43807696?v=4",
"profile": "http://ungaro.me",
"contributions": [
"translation"
]
},
{
"login": "adan89lion",
"name": "Seohyun Joo",
"avatar_url": "https://avatars.githubusercontent.com/u/6585644?v=4",
"profile": "https://github.com/adan89lion",
"contributions": [
"translation"
]
},
{
"login": "ty4ko",
"name": "Sergey",
"avatar_url": "https://avatars.githubusercontent.com/u/21213535?v=4",
"profile": "https://github.com/ty4ko",
"contributions": [
"translation"
]
},
{
"login": "skafte1990",
"name": "Shaaft",
"avatar_url": "https://avatars.githubusercontent.com/u/31465453?v=4",
"profile": "https://github.com/skafte1990",
"contributions": [
"translation"
]
},
{
"login": "sr093906",
"name": "sr093906",
"avatar_url": "https://avatars.githubusercontent.com/u/8369201?v=4",
"profile": "https://github.com/sr093906",
"contributions": [
"translation"
]
},
{
"login": "Nackophilz",
"name": "Nackophilz",
"avatar_url": "https://avatars.githubusercontent.com/u/61667226?v=4",
"profile": "https://github.com/Nackophilz",
"contributions": [
"translation"
]
},
{
"login": "schambers",
"name": "Sean Chambers",
"avatar_url": "https://avatars.githubusercontent.com/u/31563?v=4",
"profile": "https://github.com/schambers",
"contributions": [
"code"
]
},
{
"login": "deniscerri",
"name": "deniscerri",
"avatar_url": "https://avatars.githubusercontent.com/u/64997243?v=4",
"profile": "https://github.com/deniscerri",
"contributions": [
"translation"
]
},
{
"login": "tomgacz",
"name": "tomgacz",
"avatar_url": "https://avatars.githubusercontent.com/u/14138209?v=4",
"profile": "https://github.com/tomgacz",
"contributions": [
"translation"
]
},
{
"login": "Andersborrits",
"name": "Andersborrits",
"avatar_url": "https://avatars.githubusercontent.com/u/29452218?v=4",
"profile": "https://github.com/Andersborrits",
"contributions": [
"translation"
]
},
{
"login": "Maxentr",
"name": "Maxent",
"avatar_url": "https://avatars.githubusercontent.com/u/67283154?v=4",
"profile": "http://maxentrouault.fr",
"contributions": [
"translation"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
"contributorsPerLine": 7,
"projectName": "jellyseerr",
"projectOwner": "Fallenbagel",
"projectName": "overseerr",
"projectOwner": "sct",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true

View File

@@ -10,6 +10,7 @@
.gitconfig
.github
.gitignore
.husky
.next
.prettierignore
config/db/*

View File

@@ -7,6 +7,7 @@ module.exports = {
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
parserOptions: {
ecmaVersion: 6,
@@ -25,6 +26,7 @@ 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'],
@@ -38,7 +40,7 @@ module.exports = {
},
},
],
plugins: ['jsx-a11y', 'react-hooks', 'formatjs'],
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
settings: {
react: {
pragma: 'React',

91
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: 🐛 Bug Report
description: Report a problem
labels: ['type:bug', 'awaiting-triage']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please note that we use GitHub issues exclusively for bug reports and feature requests. For support requests, please use our other support channels to get help.
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and concise description of the bug or issue.
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of Overseerr are you running? (You can find this in Settings → About → Version.)
validations:
required: true
- type: textarea
id: repro-steps
attributes:
label: Steps to Reproduce
description: Please tell us how we can reproduce the undesired behavior.
placeholder: |
1. Go to [...]
2. Click on [...]
3. Scroll down to [...]
4. See error in [...]
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, please provide screenshots depicting the problem.
- type: textarea
id: logs
attributes:
label: Logs
description: Please copy and paste any relevant log output. (This will be automatically formatted into code, so no need for backticks.)
render: shell
- type: dropdown
id: platform
attributes:
label: Platform
options:
- desktop
- smartphone
- tablet
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: e.g., iPhone X, Surface Pro, Samsung Galaxy Tab
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
description: e.g., iOS 8.1, Windows 10, Android 11
validations:
required: true
- type: input
id: browser
attributes:
label: Browser
description: e.g., Chrome, Safari, Edge, Firefox
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Please provide any additional information that may be relevant or helpful.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow Overseerr's Code of Conduct
required: true

View File

@@ -1,45 +0,0 @@
---
name: Bug report
about: Submit a report to help us improve
title: ''
labels: 'awaiting-triage, type:bug'
assignees: ''
---
#### Description
Please provide a clear and concise description of the bug or issue.
#### Version
What version of Overseerr are you running? (You can find this in Settings → About → Version.)
#### Steps to Reproduce
Please tell us how we can reproduce the undesired behavior.
1. Go to [...]
2. Click on [...]
3. Scroll down to [...]
4. See error in [...]
#### Expected Behavior
Please provide a clear and concise description of what you expected to happen.
#### Screenshots
If applicable, please provide screenshots depicting the problem.
#### Device
What device were you using when you encountered this issue? Please provide this information to help us reproduce and investigate the bug.
- **Platform:** [e.g., desktop, smartphone, tablet]
- **Device:** [e.g., iPhone X, Surface Pro, Samsung Galaxy Tab]
- **OS:** [e.g., iOS 8.1, Windows 10, Android 11]
- **Browser:** [e.g., Chrome, Safari, Edge, Firefox]
#### Additional Context
Please provide any additional information that may be relevant or helpful.

View File

@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Support via Discord
url: https://discord.gg/overseerr
about: Chat with users and devs on support and setup related topics.
- name: Support via GitHub Discussions
url: https://github.com/sct/overseerr/discussions
- name: 💬 Support via Discord
url: https://discord.gg/ckbvBtDJgC
about: Chat with other users and the Overseerr dev team
- name: 💬 Support via GitHub Discussions
url: https://github.com/fallenbagel/jellyseerr/discussions
about: Ask questions and discuss with other community members

37
.github/ISSUE_TEMPLATE/enhancement.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: ✨ Feature Request
description: Suggest an idea
labels: ['type:enhancement', 'awaiting-triage']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
Please note that we use GitHub issues exclusively for bug reports and feature requests. For support requests, please use our other support channels to get help.
- type: textarea
id: description
attributes:
label: Description
description: Is your feature request related to a problem? If so, please provide a clear and concise description of the problem; e.g., "I'm always frustrated when [...]."
validations:
required: true
- type: textarea
id: desired-behavior
attributes:
label: Desired Behavior
description: Provide a clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Provide any additional information or screenshots that may be relevant or helpful.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow Overseerr's Code of Conduct
required: true

View File

@@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'awaiting-triage, type:enhancement'
assignees: ''
---
#### Description
Is your feature request related to a problem? If so, please provide a clear and concise description of the problem. E.g., "I'm always frustrated when [...]."
#### Desired Behavior
Provide a clear and concise description of what you want to happen.
#### Additional Context
Provide any additional information or screenshots that may be relevant or helpful.

View File

@@ -4,4 +4,10 @@
#### To-Dos
- [ ] Successful build `yarn build`
- [ ] Translation keys `yarn i18n:extract`
- [ ] Database migration (if required)
#### Issues Fixed or Closed
- Fixes #XXXX

40
.github/stale.yml vendored
View File

@@ -1,18 +1,44 @@
# Number of days of inactivity before an issue becomes stale
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Issues with these labels will never be considered stale
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- pinned
- security
- dependencies
# Label to use when marking an issue as stale
- never-stale
- priority:high
- priority:medium
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
pulls:
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.

View File

@@ -1,9 +1,9 @@
name: Overseerr CI
name: Jellyseerr CI
on:
pull_request:
branches:
- '*'
- "*"
push:
branches:
- develop
@@ -11,11 +11,12 @@ on:
jobs:
test:
name: Lint & Test Build
if: github.event_name == 'pull_request'
runs-on: ubuntu-20.04
container: node:14.17-alpine
container: node:16.14-alpine
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v3
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
@@ -27,36 +28,29 @@ jobs:
build_and_push:
name: Build & Publish Docker Images
needs: test
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.3.0
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2.1.6
uses: actions/cache@v2
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.9.0
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1.9.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2.5.0
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
@@ -65,10 +59,7 @@ jobs:
build-args: |
COMMIT_TAG=${{ github.sha }}
tags: |
sctx/overseerr:develop
sctx/overseerr:${{ github.sha }}
ghcr.io/sct/overseerr:develop
ghcr.io/sct/overseerr:${{ github.sha }}
fallenbagel/jellyseerr:develop
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temporary fix
@@ -86,7 +77,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2.1.6
uses: technote-space/workflow-conclusion-action@v2
- name: Combine Job Status
id: status
run: |

View File

@@ -1,23 +0,0 @@
name: Deploy API Docs
on:
push:
branches:
- develop
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Generate Swagger UI
uses: Legion2/swagger-ui-action@v1.1.2
with:
output: swagger-ui
spec-file: overseerr-api.yml
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3.8.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: swagger-ui
cname: api-docs.overseerr.dev

View File

@@ -1,19 +0,0 @@
name: 'Invalid Template'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-20.04
steps:
- uses: dessant/support-requests@v2.0.1
with:
github-token: ${{ github.token }}
support-label: 'invalid:template-incomplete'
issue-comment: >
:wave: @{issue-author}, please follow the template provided.
close-issue: true
lock-issue: true
issue-lock-reason: 'resolved'

View File

@@ -1,9 +1,9 @@
name: Overseerr Preview
name: Jellyseerr Preview
on:
push:
tags:
- 'preview-*'
- "preview-*"
jobs:
build_and_push:
@@ -11,27 +11,21 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v3
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.3.0
uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub
uses: docker/login-action@v1.9.0
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1.9.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2.5.0
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
@@ -40,5 +34,4 @@ jobs:
build-args: |
COMMIT_TAG=${{ github.sha }}
tags: |
sctx/overseerr:${{ steps.get_version.outputs.VERSION }}
ghcr.io/sct/overseerr:${{ steps.get_version.outputs.VERSION }}
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}

View File

@@ -1,118 +1,40 @@
name: Overseerr Release
name: Jellyseer Release
on:
push:
branches:
- master
on: workflow_dispatch
jobs:
test:
name: Lint & Test Build
runs-on: ubuntu-20.04
container: node:14.17-alpine
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: Lint
run: yarn lint
- name: Build
run: yarn build
semantic-release:
name: Tag and release latest version
needs: test
runs-on: ubuntu-20.04
env:
HUSKY: 0
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 14
node-version: 16
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.3.0
uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub
uses: docker/login-action@v1.9.0
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1.9.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: yarn
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: npx semantic-release
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: semantic-release
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
- armhf
steps:
- name: Checkout Code
uses: actions/checkout@v2.3.4
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.2.0
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.3.0
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: semantic-release
@@ -120,7 +42,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2.1.6
uses: technote-space/workflow-conclusion-action@v2
- name: Combine Job Status
id: status
run: |

View File

@@ -1,107 +0,0 @@
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.9.0
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
test:
name: Lint & Test Build
needs: jobs
runs-on: ubuntu-20.04
container: node:14.17-alpine
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: Lint
run: yarn lint
- name: Build
run: yarn build
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: test
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
- armhf
steps:
- name: Checkout Code
uses: actions/checkout@v2.3.4
- 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@v1.2.0
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.3.0
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: 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@v2.1.6
- 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

View File

@@ -8,7 +8,7 @@ jobs:
support:
runs-on: ubuntu-20.04
steps:
- uses: dessant/support-requests@v2.0.1
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: 'support'
@@ -16,11 +16,10 @@ jobs:
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please use our support channels
to get help with Overseerr.
to get help with Jellyseerr.
- [Discord](https://discord.gg/overseerr)
- [Discord](https://discord.gg/ckbvBtDJgC)
- [GitHub Discussions](https://github.com/sct/overseerr/discussions)
close-issue: true
lock-issue: true
issue-lock-reason: 'off-topic'

1
.gitignore vendored
View File

@@ -39,6 +39,7 @@ config/settings.json
config/logs/*.log*
config/logs/*.json
config/logs/*.log.gz
config/logs/*.json.gz
config/logs/*-audit.json
# anidb mapping file

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS
[[ -n $HUSKY_BYPASS ]] || npx commitlint --edit $1

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm test
npx lint-staged

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec < /dev/tty && git cz --hook || true
exec < /dev/tty && npx cz --hook || true

View File

@@ -19,9 +19,6 @@
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
// https://marketplace.visualstudio.com/items?itemName=heybourn.headwind
"heybourn.headwind"
"bradlc.vscode-tailwindcss"
]
}

View File

@@ -15,7 +15,6 @@
"database": "./config/db/db.sqlite3"
}
],
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locale"],
"editor.codeActionsOnSave": {
"source.organizeImports": true
},

1348
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
2. Add the remote `upstream`:
```bash
git remote add upstream https://github.com/sct/overseerr.git
git remote add upstream https://github.com/fallenbagel/jellyseerr.git
```
3. Create a new branch:
@@ -66,17 +66,17 @@ All help is welcome and greatly appreciated! If you would like to contribute to
### Contributing Code
- If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/sct/overseerr/issues) to avoid multiple people working on the same thing.
- If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/fallenbagel/jellyseerr/issues) to avoid multiple people working on the same thing.
- All commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- It is okay to squash your pull request down into a single commit that fits this standard.
- Pull requests with commits not following this standard will **not** be merged.
- Please make meaningful commits, or squash them.
- Please make meaningful commits, or squash them prior to opening a pull request.
- Do not squash commits once people have begun reviewing your changes.
- Always rebase your commit to the latest `develop` branch. Do **not** merge `develop` into your branch.
- It is your responsibility to keep your branch up-to-date. Your work will **not** be merged unless it is rebased off the latest `develop` branch.
- You can create a "draft" pull request early to get feedback on your work.
- Your code **must** be formatted correctly, or the tests will fail.
- We use Prettier to format our code base. It should automatically run with a Git hook, but it is recommended to have the Prettier extension installed in your editor and format on save.
- If you have questions or need help, you can reach out via [Discussions](https://github.com/sct/overseerr/discussions) or our [Discord server](https://discord.gg/overseerr).
- If you have questions or need help, you can reach out via [Discussions](https://github.com/fallenbagel/jellyseerr/discussions) or our [Discord server](https://discord.gg/ckbvBtDJgC).
- Only open pull requests to `develop`, never `master`! Any pull requests opened to `master` will be closed.
### UI Text Style
@@ -97,7 +97,7 @@ When adding new UI text, please try to adhere to the following guidelines:
## Translation
We use [Weblate](https://hosted.weblate.org/engage/overseerr/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/sct/overseerr/issues/new/choose).
We use [Weblate](https://hosted.weblate.org/engage/overseerr/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
<a href="https://hosted.weblate.org/engage/overseerr/"><img src="https://hosted.weblate.org/widgets/overseerr/-/overseerr-frontend/multi-auto.svg" alt="Translation status" /></a>

View File

@@ -1,4 +1,4 @@
FROM node:14.17-alpine AS BUILD_IMAGE
FROM node:16.14-alpine AS BUILD_IMAGE
WORKDIR /app
@@ -7,8 +7,10 @@ ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
RUN \
case "${TARGETPLATFORM}" in \
'linux/arm64') apk add --no-cache python make g++ ;; \
'linux/arm/v7') apk add --no-cache python make g++ ;; \
'linux/arm64' | 'linux/arm/v7') \
apk add --no-cache python3 make g++ && \
ln -s /usr/bin/python3 /usr/bin/python \
;; \
esac
COPY package.json yarn.lock ./
@@ -24,18 +26,18 @@ RUN yarn build
# remove development dependencies
RUN yarn install --production --ignore-scripts --prefer-offline
RUN rm -rf src server
RUN rm -rf src server .next/cache
RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:14.17-alpine
FROM node:16.14-alpine
WORKDIR /app
RUN apk add --no-cache tzdata tini
RUN apk add --no-cache tzdata tini && rm -rf /tmp/*
# copy from build image
COPY --from=BUILD_IMAGE /app ./

View File

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

View File

@@ -1,51 +1,59 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/stable/public/logo.png" alt="Overseerr" style="margin: 20px 0;">
<img src="./public/logo_full.svg" alt="Jellyseerr" style="margin: 20px 0;">
</p>
<p align="center">
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
</p>
**Jellyseerr** is a free and open source fork of Overseerr for managing requests for your media library. It integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**, and **[Jellyfin](https://jellyfin.org/)**!
**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!
## Current Features
- Jellyfin support
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr.
- Jellyfin library scan, to keep track of the titles which are already available.
- Jellyfin Support
- Emby Support
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.
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
- Granular permission system.
- Support for various notification agents.
- Mobile-friendly design, for when you need to approve requests on the go!
Check out our [issue tracker](https://github.com/Fallenbagel/jellyseerr/issues).
## Supported Architectures
Jellyseerr image support multiple architectures such as x86-64, arm64 and armv7.
**NOTE: `:arm` and `:armv7` tag has been deprecated and replaced with `:latest`.**
| **Architecture** | **Tag** |
| ---------------- | ------- |
| x86-64 | latest |
| ARM64 | latest |
| ARMv7 | latest |
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
## Getting Started
Check out our dockerhub for instructions on how to install and run Jellyseerr:
https://hub.docker.com/r/fallenbagel/jellyseerr
## Preview
<img src="./public/preview.jpg">
## Support
- You can get support on [Discord](https://discord.gg/ckbvBtDJgC).
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/fallenbagel/jellyseerr/discussions).
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/fallenbagel/jellyseerr/issues).
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
## API Documentation
## Buy me a Coffee!
You can access the API documentation from your local Jellyseerr install at http://localhost:5055/api-docs
If you like jellyseerr and want to help maintain it, please buy me a coffee as it would help me out a lot!
## Community
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/fallen.bagel)
You can ask questions, share ideas, and more in [GitHub Discussions](https://github.com/fallenbagel/jellyseerr/discussions).
If you would like to chat with other members of our growing community, [join the Jellyseerr Discord server](https://discord.gg/ckbvBtDJgC)!
Our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Jellyseerr community channels.
## Contributing
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.

View File

@@ -21,4 +21,4 @@ The primary motivation for starting this project was to have an incredibly perfo
Overseerr is an ambitious project. We have already poured a lot of work into this, and have a lot more to do. We need your valuable feedback and help to find and fix bugs. Also, with Overseerr being an open-source project, anyone is welcome to contribute. Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
If you would like to contribute, please be sure to review our [contribution guidelines](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md).
If you would like to contribute, please be sure to review our [contribution guidelines](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md).

View File

@@ -14,6 +14,7 @@
- [Email](using-overseerr/notifications/email.md)
- [Web Push](using-overseerr/notifications/webpush.md)
- [Discord](using-overseerr/notifications/discord.md)
- [Gotify](using-overseerr/notifications/gotify.md)
- [LunaSea](using-overseerr/notifications/lunasea.md)
- [Pushbullet](using-overseerr/notifications/pushbullet.md)
- [Pushover](using-overseerr/notifications/pushover.md)

View File

@@ -145,8 +145,7 @@ location ^~ /overseerr {
sub_filter '/android-' '/$app/android-';
sub_filter '/apple-' '/$app/apple-';
sub_filter '/favicon' '/$app/favicon';
sub_filter '/logo_full.svg' '/$app/logo_full.svg';
sub_filter '/logo_stacked.svg' '/$app/logo_stacked.svg';
sub_filter '/logo_' '/$app/logo_';
sub_filter '/site.webmanifest' '/$app/site.webmanifest';
}
```

View File

@@ -1,13 +1,15 @@
# Third-Party Integrations
{% hint style="warning" %}
We do not officially support these third-party integrations. If you run into any issues, please seek help on the appropriate support channels for the integration itself!
**We do not officially support these third-party integrations.** If you run into any issues, please seek help on the appropriate support channels for the integration itself!
{% endhint %}
- [Organizr](https://organizr.app/), a HTPC/homelab services organizer
- [Heimdall](https://github.com/linuxserver/Heimdall), an application dashboard and launcher
- [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
- [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

@@ -1,7 +1,7 @@
# Installation
{% hint style="danger" %}
**Overseerr is currently in BETA.** If you would like to help test the bleeding edge, please use the image **`sctx/overseerr:develop`**!
**Overseerr is currently in BETA.** If you would like to help test the bleeding edge, please use the image **`fallenbagel/jellyseerr:develop`**!
{% endhint %}
{% hint style="info" %}
@@ -10,8 +10,18 @@ After running Overseerr for the first time, configure it by visiting the web UI
## Docker
{% hint style="warning" %}
Be sure to replace `/path/to/appdata/config` in the below examples with a valid host directory path. If this volume mount is not configured correctly, your Overseerr settings/data will not be persisted when the container is recreated (e.g., when updating the image or rebooting your machine).
The `TZ` environment variable value should also be set to the [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) of your time zone!
{% endhint %}
{% tabs %}
{% tab title="Basic" %}
{% tab title="Docker CLI" %}
For details on the Docker CLI, please [review the official `docker run` documentation](https://docs.docker.com/engine/reference/run/).
**Installation:**
```bash
docker run -d \
@@ -21,14 +31,44 @@ docker run -d \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
fallenbagel/jellyseerr
```
To run the container as a specific user/group, you may optionally add `--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]` to the above command.
**Updating:**
Stop and remove the existing container:
```bash
docker stop overseerr && docker rm overseerr
```
Pull the latest image:
```bash
docker pull fallenbagel/jellyseerr
```
Finally, run the container with the same parameters originally used to create the container:
```bash
docker run -d ...
```
{% hint style="info" %}
You may alternatively use a third-party updating mechanism, such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros), to keep Overseerr up-to-date automatically.
{% endhint %}
{% endtab %}
{% tab title="Compose" %}
{% tab title="Docker Compose" %}
**docker-compose.yml:**
For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/).
**Installation:**
Define the `overseerr` service in your `docker-compose.yml` as follows:
```yaml
---
@@ -36,7 +76,7 @@ version: '3'
services:
overseerr:
image: sctx/overseerr:latest
image: fallenbagel/jellyseerr:latest
container_name: overseerr
environment:
- LOG_LEVEL=debug
@@ -48,47 +88,29 @@ services:
restart: unless-stopped
```
{% endtab %}
{% tab title="UID/GID" %}
```text
docker run -d \
--name overseerr \
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
-e LOG_LEVEL=debug \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
```
{% endtab %}
{% tab title="Manual Update" %}
Then, start all services defined in the your Compose file:
```bash
# Stop the Overseerr container
docker stop overseerr
docker-compose up -d
```
# Remove the Overseerr container
docker rm overseerr
**Updating:**
# Pull the latest update
docker pull sctx/overseerr
Pull the latest image:
# Run the Overseerr container with the same parameters as before
docker run -d ...
```bash
docker-compose pull overseerr
```
Then, restart all services defined in the Compose file:
```bash
docker-compose up -d
```
{% endtab %}
{% endtabs %}
{% hint style="info" %}
Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically.
{% endhint %}
## Unraid
1. Ensure you have the **Community Applications** plugin installed.
@@ -121,7 +143,7 @@ or the Docker Desktop app:
Then, create and start the Overseerr container:
```bash
docker run -d -e LOG_LEVEL=debug -e TZ=Asia/Tokyo -p 5055:5055 -v "overseerr-data:/app/config" --restart unless-stopped sctx/overseerr
docker run -d --name overseerr -e LOG_LEVEL=debug -e TZ=Asia/Tokyo -p 5055:5055 -v "overseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
```
If using a named volume like above, you can safely ignore the warning about the `/app/config` folder being incorrectly mounted on the setup page.
@@ -144,29 +166,24 @@ The [Overseerr snap](https://snapcraft.io/overseerr) is the only officially supp
Currently, the listening port cannot be changed, so port `5055` will need to be available on your host. To install `snapd`, please refer to the [Snapcraft documentation](https://snapcraft.io/docs/installing-snapd).
{% endhint %}
**To install:**
**Installation:**
```
sudo snap install overseerr
```
{% hint style="danger" %}
To install the development build, add the `--edge` argument to the above command (i.e., `sudo snap install overseerr --edge`). However, note that this version can break any moment. Be prepared to troubleshoot any issues that arise!
{% endhint %}
**Updating:**
Snap will keep Overseerr up-to-date automatically. You can force a refresh by using the following command.
```
```bash
sudo snap refresh
```
**To install the development build:**
```
sudo snap install overseerr --edge
```
{% hint style="danger" %}
This version can break any moment. Be prepared to troubleshoot any issues that arise!
{% endhint %}
## Third-Party
{% tabs %}

View File

@@ -1,7 +1,7 @@
# Frequently Asked Questions (FAQ)
{% hint style="info" %}
If you can't find the solution to your problem here, please read [Need Help?](./need-help.md) and reach out to us on [Discord](https://discord.gg/overseerr).
If you can't find the solution to your problem here, please read [Need Help?](./need-help.md) and reach out to us on [Discord](https://discord.gg/ckbvBtDJgC).
_Please do not post questions or support requests on the GitHub issue tracker!_
{% endhint %}
@@ -20,6 +20,12 @@ A more advanced, user-friendly, and secure (if using SSL) method is to set up a
The most secure method (but also the most inconvenient method) is to set up a VPN tunnel to your home server. You would then be able to access Overseerr as if you were on your local network, via `http://LOCAL-IP-ADDRESS:5055`.
### Are there mobile apps for Overseerr?
Since Overseerr has an almost native app experience when installed as a Progressive Web App (PWA), there are no plans to develop mobile apps for Overseerr.
Out of the box, Overseerr already fulfills most of the [PWA install criteria](https://web.dev/install-criteria/). You simply need to make sure that your Overseerr instance is being served over HTTPS (e.g., via a [reverse proxy](../extending-overseerr/reverse-proxy.md)).
### Overseerr is amazing! But it is not translated in my language yet! Can I help with translations?
You sure can! We are using [Weblate](https://hosted.weblate.org/engage/overseerr/) for translations. If your language is not listed, please [open a feature request on GitHub](https://github.com/sct/overseerr/issues/new/choose).
@@ -28,7 +34,7 @@ You sure can! We are using [Weblate](https://hosted.weblate.org/engage/overseerr
You can find the changelog for your version (stable/`latest`,s or `develop`) in the **Settings &rarr; About** page in your Overseerr instance.
You can alternatively review the [stable release history](https://github.com/sct/overseerr/releases) and [`develop` branch commit history](https://github.com/sct/overseerr/commits/develop) on GitHub.
You can alternatively review the [stable release history](https://github.com/fallenbagel/jellyseerr/releases) and [`develop` branch commit history](https://github.com/fallenbagel/jellyseerr/commits/develop) on GitHub.
### Some media is missing from Overseerr that I know is in Plex!
@@ -82,7 +88,7 @@ Yes! Please see the [documentation for creating local users](../using-overseerr/
### Is is possible to set user roles in Overseerr?
Permissions can be configured for each user via the **User List** or their **User Settings** page. The list of assignable permissions is still growing, so if you have any suggestions, [submit a feature request](https://github.com/sct/overseerr/issues/new/choose)!
Permissions can be configured for each user via the **User List** or their **User Settings** page. The list of assignable permissions is still growing, so if you have any suggestions, [submit a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose)!
## Requests
@@ -112,10 +118,16 @@ If you configured a URL base in Sonarr, make sure you have also configured the [
Also, check that you are using Sonarr v3 and that you have configured a default language profile in Overseerr.
Language profile support for Sonarr was added in [v1.20.0](https://github.com/sct/overseerr/releases/tag/v1.20.0) along with a new, _required_ **Language Profile** setting. If series requests are failing, make sure that you have a default language profile configured for each of your Sonarr servers in **Settings &rarr; Services**.
Language profile support for Sonarr was added in [v1.20.0](https://github.com/fallenbagel/jellyseerr/releases/tag/v1.20.0) along with a new, _required_ **Language Profile** setting. If series requests are failing, make sure that you have a default language profile configured for each of your Sonarr servers in **Settings &rarr; Services**.
## Notifications
### I am getting "Username and Password not accepted" when attempting to send email notifications via Gmail!
If you have 2-Step Verification enabled on your account, you will need to create an [app password](https://support.google.com/mail/answer/185833).
### The logo image in email notifications is broken!
This may be an issue with how you are proxying your Overseerr instance. A good first troubleshooting step is to verify that the [`Content-Security-Policy` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) being set by your proxy (if any) is configured appropriately to allow external embedding of the image.
For Gmail users, another possible issue is that Google's image URL proxy is being blocked from fetching the image. If using Cloudflare, overzealous firewall rules could be the culprit.

View File

@@ -9,7 +9,7 @@ Before seeking assistance, please make sure you have first tried these following
- **Analyzing** your logs, you just might find the solution yourself!
- **Searching** the [documentation](../README.md), [installation guide](../getting-started/installation.md), and [FAQs](./faq.md).
If you still have questions after troubleshooting on your own, feel free to ask on [Discord](https://discord.gg/overseerr)! (Please review our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) before posting.)
If you still have questions after troubleshooting on your own, feel free to ask on [Discord](https://discord.gg/ckbvBtDJgC)! (Please review our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md) before posting.)
Be sure to also include a link to your logs. (Please see [How can I share my logs?](#how-can-i-share-my-logs) below.)
@@ -19,6 +19,11 @@ Please try to include as much information as possible. A vague statement like "i
Try to answer the following questions:
- What version of Overseerr are you running? (You can find this in Settings → About → Version.)
- How did you install Overseerr? Are you using the official Docker or snap images, or images published by a third-party?
- How are you accessing Overseerr?
- Are you accessing Overseerr through your reverse proxy or via a local IP address?
- What browser are you using? What browser extensions are enabled?
- What were you trying to do, and how did you attempt it?
- What command did you enter?
- What did you click on?
@@ -37,4 +42,4 @@ Try to answer the following questions:
1. Locate the current log file at `<your Overseerr config directory>/logs/overseerr.log`.
2. Open the log file and **copy its contents** into a [**secret gist** on GitHub](https://gist.github.com/). If you upload your logs elsewhere, we may ask you to share them again via GitHub Gist.
3. **Share the link/URL to your secret gist** in the [`#support` channel in our Discord server](https://discord.gg/overseerr).
3. **Share the link/URL to your secret gist** in the [`#support` channel in our Discord server](https://discord.gg/ckbvBtDJgC).

View File

@@ -7,6 +7,7 @@ Overseerr currently supports the following notification agents:
- [Email](./email.md)
- [Web Push](./webpush.md)
- [Discord](./discord.md)
- [Gotify](./gotify.md)
- [LunaSea](./lunasea.md)
- [Pushbullet](./pushbullet.md)
- [Pushover](./pushover.md)

View File

@@ -0,0 +1,15 @@
# Gotify
## Configuration
### Server URL
Set this to the URL of your Gotify server.
### Application Token
Add an application to your Gotify server, and set this field to the generated application token.
{% hint style="info" %}
Please refer to the [Gotify API documentation](https://gotify.net/docs) for more details on configuring these notifications.
{% endhint %}

View File

@@ -1,7 +1,17 @@
# Pushbullet
{% hint style="info" %}
Users can optionally configure personal notifications in their user settings.
User notifications are separate from system notifications, and the available notification types are dependent on user permissions.
{% endhint %}
## Configuration
### Access Token
[Create an access token](https://www.pushbullet.com/#settings) and set it here to grant Overseerr access to the Pushbullet API.
### Channel Tag (optional)
Optionally, [create a channel](https://www.pushbullet.com/my-channel) to allow other users to follow the notification feed using the specified channel tag.

View File

@@ -1,10 +1,16 @@
# Pushover
{% hint style="info" %}
Users can optionally configure personal notifications in their user settings.
User notifications are separate from system notifications, and the available notification types are dependent on user permissions.
{% endhint %}
## Configuration
### Application/API Token
[Register an application](https://pushover.net/apps/build) and enter the API token in this field. (You can use one of the [official icons in our GitHub repository](https://github.com/sct/overseerr/tree/develop/public) when configuring the application.)
[Register an application](https://pushover.net/apps/build) and enter the API token in this field. (You can use one of the [official icons in our GitHub repository](https://github.com/fallenbagel/jellyseerr/tree/develop/public) when configuring the application.)
For more details on registering applications or the API token, please see the [Pushover API documentation](https://pushover.net/api#registration).

View File

@@ -1,7 +1,9 @@
# Telegram
{% hint style="info" %}
Users can optionally configure their own notifications in their user settings.
Users can optionally configure personal notifications in their user settings.
User notifications are separate from system notifications, and the available notification types are dependent on user permissions.
{% endhint %}
## Configuration

View File

@@ -24,33 +24,38 @@ Customize the JSON payload to suit your needs. Overseerr provides several [templ
### General
- `{{notification_type}}` The type of notification. (Ex. `MEDIA_PENDING` or `MEDIA_APPROVED`)
- `{{subject}}` The notification subject message. (For request notifications, this is the media title)
- `{{message}}` Notification message body. (For request notifications, this is the media's overview/synopsis)
- `{{image}}` Associated image with the request. (For request notifications, this is the media's poster)
| Variable | Value |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `{{notification_type}}` | The type of notification (e.g. `MEDIA_PENDING` or `ISSUE_COMMENT`) |
| `{{event}}` | A friendly description of the notification event |
| `{{subject}}` | The notification subject (typically the media title) |
| `{{message}}` | The notification message body (the media overview/synopsis for request notifications; the issue description for issue notificatons) |
| `{{image}}` | The notification image (typically the media poster) |
### User
### Notify User
These variables are for the target recipient of the notification.
- `{{notifyuser_username}}` Target user's username.
- `{{notifyuser_email}}` Target user's email address.
- `{{notifyuser_avatar}}` Target user's avatar URL.
- `{{notifyuser_settings_discordId}}` Target user's Discord ID (if one is set).
- `{{notifyuser_settings_telegramChatId}}` Target user's Telegram Chat ID (if one is set).
| Variable | Value |
| ---------------------------------------- | ------------------------------------------------------------- |
| `{{notifyuser_username}}` | The target notification recipient's username |
| `{{notifyuser_email}}` | The target notification recipient's email address |
| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL |
| `{{notifyuser_settings_discordId}}` | The target notification recipient's Discord ID (if set) |
| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) |
{% hint style="info" %}
The `notifyuser` variables are not set for the following notification types, as they are intended for application administrators rather than end users:
The `notifyuser` variables are not defined for the following request notification types, as they are intended for application administrators rather than end users:
- Media Requested
- Media Automatically Approved
- Media Failed
- Request Pending Approval
- Request Automatically Approved
- Request Processing Failed
On the other hand, the `notifyuser` variables _will_ be replaced with the requesting user's information for the below notification types:
- Media Approved
- Media Declined
- Media Available
- Request Approved
- Request Declined
- Request Available
If you would like to use the requesting user's information in your webhook, please instead include the relevant variables from the [Request](#request) section below.
{% endhint %}
@@ -59,28 +64,69 @@ If you would like to use the requesting user's information in your webhook, plea
The following variables must be used as a key in the JSON payload (e.g., `"{{extra}}": []`).
- `{{request}}` This object will be `null` if there is no relevant request object for the notification.
- `{{media}}` This object will be `null` if there is no relevant media object for the notification.
- `{{extra}}` This object will contain the "extra" array of additional data for certain notifications.
| Variable | Value |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `{{media}}` | The relevant media object |
| `{{request}}` | The relevant request object |
| `{{issue}}` | The relevant issue object |
| `{{comment}}` | The relevant issue comment object |
| `{{extra}}` | The "extra" array of additional data for certain notifications (e.g., season/episode numbers for series-related notifications) |
#### Media
These `{{media}}` special variables are only included in media-related notifications, such as requests.
The `{{media}}` will be `null` if there is no relevant media object for the notification.
- `{{media_type}}` Media type (`movie` or `tv`).
- `{{media_tmdbid}}` Media's TMDb ID.
- `{{media_imdbid}}` Media's IMDb ID.
- `{{media_tvdbid}}` Media's TVDB ID.
- `{{media_status}}` Media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`).
- `{{media_status4k}}` Media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`)
These following special variables are only included in media-related notifications, such as requests.
| Variable | Value |
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
| `{{media_type}}` | The media type (`movie` or `tv`) |
| `{{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`) |
#### Request
The `{{request}}` special variables are only included in request-related notifications.
The `{{request}}` will be `null` if there is no relevant media object for the notification.
- `{{request_id}}` Request ID.
- `{{requestedBy_username}}` Requesting user's username.
- `{{requestedBy_email}}` Requesting user's email address.
- `{{requestedBy_avatar}}` Requesting user's avatar URL.
- `{{requestedBy_settings_discordId}}` Requesting user's Discord ID (if set).
- `{{requestedBy_settings_telegramChatId}}` Requesting user's Telegram Chat ID (if set).
The following special variables are only included in request-related notifications.
| Variable | Value |
| ----------------------------------------- | ----------------------------------------------- |
| `{{request_id}}` | The request ID |
| `{{requestedBy_username}}` | The requesting user's username |
| `{{requestedBy_email}}` | The requesting user's email address |
| `{{requestedBy_avatar}}` | The requesting user's avatar URL |
| `{{requestedBy_settings_discordId}}` | The requesting user's Discord ID (if set) |
| `{{requestedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) |
#### Issue
The `{{issue}}` will be `null` if there is no relevant media object for the notification.
The following special variables are only included in issue-related notifications.
| Variable | Value |
| ---------------------------------------- | ----------------------------------------------- |
| `{{issue_id}}` | The issue ID |
| `{{reportedBy_username}}` | The requesting user's username |
| `{{reportedBy_email}}` | The requesting user's email address |
| `{{reportedBy_avatar}}` | The requesting user's avatar URL |
| `{{reportedBy_settings_discordId}}` | The requesting user's Discord ID (if set) |
| `{{reportedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) |
#### Comment
The `{{comment}}` will be `null` if there is no relevant media object for the notification.
The following special variables are only included in issue comment-related notifications.
| Variable | Value |
| ----------------------------------------- | ----------------------------------------------- |
| `{{comment_message}}` | The comment message |
| `{{commentedBy_username}}` | The commenting user's username |
| `{{commentedBy_email}}` | The commenting user's email address |
| `{{commentedBy_avatar}}` | The commenting user's avatar URL |
| `{{commentedBy_settings_discordId}}` | The commenting user's Discord ID (if set) |
| `{{commentedBy_settings_telegramChatId}}` | The commenting user's Telegram Chat ID (if set) |

View File

@@ -8,9 +8,9 @@ The user account created during Overseerr setup is the "Owner" account, which ca
There are currently two methods to add users to Overseerr: importing Plex users and creating "local users." All new users are created with the [default permissions](../settings/README.md#default-permissions) defined in **Settings &rarr; Users**.
### Importing Users from Plex
### Importing Plex Users
Clicking the **Import Users from Plex** button on the **User List** page will fetch the list of users with access to the Plex server from [plex.tv](https://www.plex.tv/), and add them to Overseerr automatically.
Clicking the **Import Plex Users** button on the **User List** page will fetch the list of users with access to the Plex server from [plex.tv](https://www.plex.tv/), and add them to Overseerr automatically.
Importing Plex users is not required, however. Any user with access to the Plex server can log in to Overseerr even if they have not been imported, and will be assigned the configured [default permissions](../settings/README.md#default-permissions) upon their first login.

1
next-env.d.ts vendored
View File

@@ -1,5 +1,4 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited

View File

@@ -171,6 +171,9 @@ components:
port:
type: number
example: 32400
useSsl:
type: boolean
nullable: true
libraries:
type: array
readOnly: true
@@ -178,6 +181,7 @@ components:
$ref: '#/components/schemas/PlexLibrary'
webAppUrl:
type: string
nullable: true
example: 'https://app.plex.tv/desktop'
required:
- name
@@ -329,6 +333,9 @@ components:
hostname:
type: string
example: 'http://my.jellyfin.host'
externalHostname:
type: string
example: 'http://my.jellyfin.host'
adminUser:
type: string
example: 'admin'
@@ -343,8 +350,26 @@ components:
serverID:
type: string
readOnly: true
required:
- hostname
TautulliSettings:
type: object
properties:
hostname:
type: string
nullable: true
example: 'tautulli.example.com'
port:
type: number
nullable: true
example: 8181
useSsl:
type: boolean
nullable: true
apiKey:
type: string
nullable: true
externalUrl:
type: string
nullable: true
RadarrSettings:
type: object
properties:
@@ -956,6 +981,15 @@ components:
type: array
items:
$ref: '#/components/schemas/ProductionCompany'
productionCountries:
type: array
items:
type: object
properties:
iso_3166_1:
type: string
name:
type: string
spokenLanguages:
type: array
items:
@@ -1176,6 +1210,8 @@ components:
type: string
webhookUrl:
type: string
enableMentions:
type: boolean
SlackSettings:
type: object
properties:
@@ -1251,6 +1287,9 @@ components:
properties:
accessToken:
type: string
channelTag:
type: string
nullable: true
PushoverSettings:
type: object
properties:
@@ -1267,6 +1306,22 @@ components:
type: string
userToken:
type: string
GotifySettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
url:
type: string
token:
type: string
LunaSeaSettings:
type: object
properties:
@@ -1325,7 +1380,28 @@ components:
allowSelfSigned:
type: boolean
example: false
PersonDetail:
Job:
type: object
properties:
id:
type: string
example: job-name
type:
type: string
enum: [process, command]
interval:
type: string
enum: [short, long, fixed]
name:
type: string
example: A Job Name
nextExecutionTime:
type: string
example: '2020-09-02T05:02:23.000Z'
running:
type: boolean
example: false
PersonDetails:
type: object
properties:
id:
@@ -1656,6 +1732,15 @@ components:
discordId:
type: string
nullable: true
pushbulletAccessToken:
type: string
nullable: true
pushoverApplicationToken:
type: string
nullable: true
pushoverUserKey:
type: string
nullable: true
telegramEnabled:
type: boolean
telegramBotUsername:
@@ -1713,6 +1798,36 @@ components:
type: number
name:
type: string
Issue:
type: object
properties:
id:
type: number
example: 1
issueType:
type: number
example: 1
media:
$ref: '#/components/schemas/MediaInfo'
createdBy:
$ref: '#/components/schemas/User'
modifiedBy:
$ref: '#/components/schemas/User'
comments:
type: array
items:
$ref: '#/components/schemas/IssueComment'
IssueComment:
type: object
properties:
id:
type: number
example: 1
user:
$ref: '#/components/schemas/User'
message:
type: string
example: A comment
securitySchemes:
cookieAuth:
type: apiKey
@@ -1870,6 +1985,20 @@ paths:
type: array
items:
$ref: '#/components/schemas/JellyfinLibrary'
/settings/jellyfin/users:
get:
summary: Get Jellyfin Users
description: Returns a list of Jellyfin Users in a JSON array.
tags:
- settings
- users
responses:
'200':
description: Jellyfin users returned
content:
application/json:
schema:
type: array
/settings/jellyfin/sync:
get:
summary: Get status of full Jellyfin library sync
@@ -2084,6 +2213,67 @@ paths:
type: array
items:
$ref: '#/components/schemas/PlexDevice'
/settings/plex/users:
get:
summary: Get Plex users
description: |
Returns a list of Plex users in a JSON array.
Requires the `MANAGE_USERS` permission.
tags:
- settings
- users
responses:
'200':
description: Plex users
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
title:
type: string
username:
type: string
email:
type: string
thumb:
type: string
/settings/tautulli:
get:
summary: Get Tautulli settings
description: Retrieves current Tautulli settings.
tags:
- settings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TautulliSettings'
post:
summary: Update Tautulli settings
description: Updates Tautulli settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TautulliSettings'
responses:
'200':
description: 'Values were successfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/TautulliSettings'
/settings/radarr:
get:
summary: Get Radarr settings
@@ -2391,23 +2581,7 @@ paths:
schema:
type: array
items:
type: object
properties:
id:
type: string
example: job-name
name:
type: string
example: A Job Name
type:
type: string
enum: [process, command]
nextExecutionTime:
type: string
example: '2020-09-02T05:02:23.000Z'
running:
type: boolean
example: false
$ref: '#/components/schemas/Job'
/settings/jobs/{jobId}/run:
post:
summary: Invoke a specific job
@@ -2426,23 +2600,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
id:
type: string
example: job-name
type:
type: string
enum: [process, command]
name:
type: string
example: A Job Name
nextExecutionTime:
type: string
example: '2020-09-02T05:02:23.000Z'
running:
type: boolean
example: false
$ref: '#/components/schemas/Job'
/settings/jobs/{jobId}/cancel:
post:
summary: Cancel a specific job
@@ -2461,23 +2619,36 @@ paths:
content:
application/json:
schema:
type: object
properties:
id:
type: string
example: job-name
type:
type: string
enum: [process, command]
name:
type: string
example: A Job Name
nextExecutionTime:
type: string
example: '2020-09-02T05:02:23.000Z'
running:
type: boolean
example: false
$ref: '#/components/schemas/Job'
/settings/jobs/{jobId}/schedule:
post:
summary: Modify job schedule
description: Re-registers the job with the schedule specified. Will return the job in JSON format.
tags:
- settings
parameters:
- in: path
name: jobId
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
schedule:
type: string
example: '0 */5 * * * *'
responses:
'200':
description: Rescheduled job
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
/settings/cache:
get:
summary: Get a list of active caches
@@ -2575,7 +2746,7 @@ paths:
example: Server ready on port 5055
timestamp:
type: string
example: 2020-12-15T16:20:00.069Z
example: '2020-12-15T16:20:00.069Z'
/settings/notifications/email:
get:
summary: Get email notification settings
@@ -2806,6 +2977,52 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/gotify:
get:
summary: Get Gotify notification settings
description: Returns current Gotify notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Gotify settings
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
post:
summary: Update Gotify notification settings
description: Update Gotify notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
/settings/notifications/gotify/test:
post:
summary: Test Gotify settings
description: Sends a test notification to the Gotify agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/slack:
get:
summary: Get Slack notification settings
@@ -3017,6 +3234,9 @@ paths:
type: string
nullable: true
example: Asia/Tokyo
appDataPath:
type: string
example: /app/config
/auth/me:
get:
summary: Get logged-in user
@@ -3169,6 +3389,13 @@ paths:
security: []
tags:
- users
parameters:
- in: path
name: guid
required: true
schema:
type: number
example: 1
responses:
'200':
description: OK
@@ -3291,11 +3518,51 @@ paths:
post:
summary: Import all users from Plex
description: |
Requests users from the Plex Server and creates a new user for each of them
Fetches and imports users from the Plex server. If a list of Plex IDs is provided in the request body, only the specified users will be imported. Otherwise, all users will be imported.
Requires the `MANAGE_USERS` permission.
tags:
- users
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
plexIds:
type: array
items:
type: string
responses:
'201':
description: A list of the newly created users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
/user/import-from-jellyfin:
post:
summary: Import all users from Jellyfin
description: |
Fetches and imports users from the Jellyfin server.
Requires the `MANAGE_USERS` permission.
tags:
- users
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
jellyfinIds:
type: array
items:
type: string
responses:
'201':
description: A list of the newly created users
@@ -3697,6 +3964,35 @@ paths:
permissions:
type: number
example: 2
/user/{userId}/watch_data:
get:
summary: Get watch data
description: |
Returns play count, play duration, and recently watched media.
Requires the `ADMIN` permission to fetch results for other users.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
responses:
'200':
description: Users
content:
application/json:
schema:
type: object
properties:
recentlyWatched:
type: array
items:
$ref: '#/components/schemas/MediaInfo'
playCount:
type: number
/search:
get:
summary: Search for movies, TV shows, or people
@@ -4476,21 +4772,22 @@ paths:
schema:
type: object
properties:
total:
type: number
movie:
type: number
tv:
type: number
pending:
type: number
example: 0
approved:
type: number
example: 10
declined:
type: number
processing:
type: number
example: 4
available:
type: number
example: 6
required:
- pending
- approved
/request/{requestId}:
get:
summary: Get MediaRequest
@@ -4966,8 +5263,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/PersonDetail'
$ref: '#/components/schemas/PersonDetails'
/person/{personId}/combined_credits:
get:
summary: Get combined credits
@@ -5104,6 +5400,57 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MediaInfo'
/media/{mediaId}/watch_data:
get:
summary: Get watch data
description: |
Returns play count, play duration, and users who have watched the media.
Requires the `ADMIN` permission.
tags:
- media
parameters:
- in: path
name: mediaId
description: Media ID
required: true
example: '1'
schema:
type: string
responses:
'200':
description: Users
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
playCount7Days:
type: number
playCount30Days:
type: number
playCount:
type: number
users:
type: array
items:
$ref: '#/components/schemas/User'
data4k:
type: object
properties:
playCount7Days:
type: number
playCount30Days:
type: number
playCount:
type: number
users:
type: array
items:
$ref: '#/components/schemas/User'
/collection/{collectionId}:
get:
summary: Get collection details
@@ -5374,7 +5721,267 @@ paths:
name:
type: string
example: Drama
/backdrops:
get:
summary: Get backdrops of trending items
description: Returns a list of backdrop image paths in a JSON array.
security: []
tags:
- tmdb
responses:
'200':
description: Results
content:
application/json:
schema:
type: array
items:
type: string
/issue:
get:
summary: Get all issues
description: |
Returns a list of issues in JSON format.
tags:
- issue
parameters:
- in: query
name: take
schema:
type: number
nullable: true
example: 20
- in: query
name: skip
schema:
type: number
nullable: true
example: 0
- in: query
name: sort
schema:
type: string
enum: [added, modified]
default: added
- in: query
name: filter
schema:
type: string
enum: [all, open, resolved]
default: open
- in: query
name: requestedBy
schema:
type: number
nullable: true
example: 1
responses:
'200':
description: Issues returned
content:
application/json:
schema:
type: object
properties:
pageInfo:
$ref: '#/components/schemas/PageInfo'
results:
type: array
items:
$ref: '#/components/schemas/Issue'
post:
summary: Create new issue
description: |
Creates a new issue
tags:
- issue
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
issueType:
type: number
message:
type: string
mediaId:
type: number
responses:
'201':
description: Succesfully created the issue
content:
application/json:
schema:
$ref: '#/components/schemas/Issue'
/issue/{issueId}:
get:
summary: Get issue
description: |
Returns a single issue in JSON format.
tags:
- issue
parameters:
- in: path
name: issueId
required: true
schema:
type: number
example: 1
responses:
'200':
description: Issues returned
content:
application/json:
schema:
$ref: '#/components/schemas/Issue'
delete:
summary: Delete issue
description: Removes an issue. If the user has the `MANAGE_ISSUES` permission, any issue can be removed. Otherwise, only a users own issues can be removed.
tags:
- issue
parameters:
- in: path
name: issueId
description: Issue ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed issue
/issue/{issueId}/comment:
post:
summary: Create a comment
description: |
Creates a comment and returns associated issue in JSON format.
tags:
- issue
parameters:
- in: path
name: issueId
required: true
schema:
type: number
example: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
message:
type: string
required:
- message
responses:
'200':
description: Issue returned with new comment
content:
application/json:
schema:
$ref: '#/components/schemas/Issue'
/issueComment/{commentId}:
get:
summary: Get issue comment
description: |
Returns a single issue comment in JSON format.
tags:
- issue
parameters:
- in: path
name: commentId
required: true
schema:
type: string
example: 1
responses:
'200':
description: Comment returned
content:
application/json:
schema:
$ref: '#/components/schemas/IssueComment'
put:
summary: Update issue comment
description: |
Updates and returns a single issue comment in JSON format.
tags:
- issue
parameters:
- in: path
name: commentId
required: true
schema:
type: string
example: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
message:
type: string
responses:
'200':
description: Comment updated
content:
application/json:
schema:
$ref: '#/components/schemas/IssueComment'
delete:
summary: Delete issue comment
description: |
Deletes an issue comment. Only users with `MANAGE_ISSUES` or the user who created the comment can perform this action.
tags:
- issue
parameters:
- in: path
name: commentId
description: Issue Comment ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed issue comment
/issue/{issueId}/{status}:
post:
summary: Update an issue's status
description: |
Updates an issue's status to approved or declined. Also returns the issue in a JSON object.
Requires the `MANAGE_ISSUES` permission or `ADMIN`.
tags:
- issue
parameters:
- in: path
name: issueId
description: Issue ID
required: true
schema:
type: string
example: '1'
- in: path
name: status
description: New status
required: true
schema:
type: string
enum: [open, resolved]
responses:
'200':
description: Issue status changed
content:
application/json:
schema:
$ref: '#/components/schemas/Issue'
security:
- cookieAuth: []
- apiKey: []

View File

@@ -1,6 +1,6 @@
{
"name": "jellyseerr",
"version": "1.0.1",
"version": "1.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",
@@ -10,155 +10,151 @@
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
"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/.bin/typeorm migration:generate",
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:create",
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:run",
"format": "prettier --write ."
"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"
},
"repository": {
"type": "git",
"url": "https://github.com/fallenbagel/jellyseerr.git"
},
"license": "MIT",
"dependencies": {
"@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4",
"@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.79",
"ace-builds": "^1.4.12",
"axios": "^0.21.4",
"@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.5",
"cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.4.10",
"country-flag-icons": "^1.4.21",
"csurf": "^1.11.0",
"email-templates": "^8.0.8",
"express": "^4.17.1",
"express-openapi-validator": "^4.13.1",
"express-rate-limit": "^5.3.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",
"gravatar-url": "^3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.21",
"next": "11.1.2",
"next": "12.1.0",
"node-cache": "^5.1.2",
"node-schedule": "^2.0.0",
"nodemailer": "^6.6.3",
"openpgp": "^5.0.0-3",
"plex-api": "^5.3.1",
"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.3.0",
"react-ace": "^9.5.0",
"react-animate-height": "^2.0.23",
"react-dom": "17.0.2",
"react-intersection-observer": "^8.32.1",
"react-intl": "5.20.10",
"react-markdown": "^6.0.2",
"react-select": "^4.3.1",
"react-spring": "^9.2.4",
"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.1.6",
"swr": "^0.5.6",
"typeorm": "0.2.37",
"uuid": "^8.3.2",
"swagger-ui-express": "^4.3.0",
"swr": "^1.2.2",
"typeorm": "0.2.45",
"web-push": "^3.4.5",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5",
"winston": "^3.6.0",
"winston-daily-rotate-file": "^4.6.1",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.32.9"
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/cli": "^7.15.7",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@fullhuman/postcss-purgecss": "3.0.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/commit-analyzer": "^9.0.1",
"@semantic-release/exec": "^5.0.0",
"@semantic-release/git": "^9.0.1",
"@tailwindcss/aspect-ratio": "^0.2.1",
"@tailwindcss/forms": "^0.3.3",
"@tailwindcss/typography": "^0.4.1",
"@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-rate-limit": "^5.1.3",
"@types/express-session": "^1.17.3",
"@types/lodash": "^4.14.173",
"@types/node": "^15.6.1",
"@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.22",
"@types/react-dom": "^17.0.9",
"@types/react-select": "^4.0.17",
"@types/react-toast-notifications": "^2.4.1",
"@types/react-transition-group": "^4.4.3",
"@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/uuid": "^8.3.1",
"@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": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"autoprefixer": "^10.3.4",
"@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": "^7.32.0",
"eslint-config-next": "^11.1.2",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-formatjs": "^2.17.6",
"eslint-plugin-jsx-a11y": "^6.4.1",
"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.25.3",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-react-hooks": "^4.3.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "4.3.8",
"lint-staged": "^11.1.2",
"nodemon": "^2.0.12",
"postcss": "^8.3.6",
"prettier": "^2.4.1",
"semantic-release": "^18.0.0",
"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": "^2.2.15",
"ts-node": "^10.2.1",
"typescript": "^4.4.3"
"tailwindcss": "^3.0.23",
"ts-node": "^10.7.0",
"typescript": "^4.6.2"
},
"resolutions": {
"sqlite3/node-gyp": "^5.1.0"
"sqlite3/node-gyp": "^8.4.1"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
"commit-msg": "[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"**/*.{ts,tsx,js}": [
"prettier --write",
"eslint"
],
"**/*.{json,md}": [
"**/*.{json,md,css}": [
"prettier --write"
]
},
@@ -201,7 +197,7 @@
]
],
"branches": [
"master"
"main"
],
"npmPublish": false,
"publish": [
@@ -211,8 +207,7 @@
"COMMIT_TAG": "$GITHUB_SHA"
},
"imageNames": [
"sctx/overseerr",
"ghcr.io/sct/overseerr"
"fallenbagel/jellyseerr"
],
"platforms": [
"linux/amd64",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#1f2937" />
<title>You are offline</title>

View File

@@ -90,8 +90,8 @@ self.addEventListener('push', (event) => {
if (payload.actionUrl){
options.actions.push(
{
action: 'viewmedia',
title: 'View Media',
action: 'view',
title: payload.actionUrlTitle ?? 'View',
}
);
}
@@ -119,21 +119,17 @@ self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'viewmedia') {
clients.openWindow(notificationData.actionUrl);
} else if (event.action === 'approve') {
if (event.action === 'approve') {
fetch(`/api/v1/request/${notificationData.requestId}/approve`, {
method: 'POST',
});
clients.openWindow(notificationData.actionUrl);
} else if (event.action === 'decline') {
fetch(`/api/v1/request/${notificationData.requestId}/decline`, {
method: 'POST',
});
clients.openWindow(notificationData.actionUrl);
} else if (notificationData.actionUrl) {
}
if (notificationData.actionUrl) {
clients.openWindow(notificationData.actionUrl);
}
}, false);

View File

@@ -83,7 +83,7 @@ class GithubAPI extends ExternalAPI {
} = {}): Promise<GitHubRelease[]> {
try {
const data = await this.get<GitHubRelease[]>(
'/repos/Fallenbagel/jellyseerr/releases',
'/repos/fallenbagel/jellyseerr/releases',
{
params: {
per_page: take,
@@ -110,7 +110,7 @@ class GithubAPI extends ExternalAPI {
} = {}): Promise<GithubCommit[]> {
try {
const data = await this.get<GithubCommit[]>(
'/repos/Fallenbagel/jellyseerr/commits',
'/repos/fallenbagel/jellyseerr/commits',
{
params: {
per_page: take,
@@ -122,7 +122,7 @@ class GithubAPI extends ExternalAPI {
return data;
} catch (e) {
logger.warn(
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
{ label: 'GitHub API', errorMessage: e.message }
);
return [];

View File

@@ -15,6 +15,10 @@ export interface JellyfinLoginResponse {
AccessToken: string;
}
export interface JellyfinUserListResponse {
users: Array<JellyfinUserResponse>;
}
export interface JellyfinLibrary {
type: 'show' | 'movie';
key: string;
@@ -81,9 +85,9 @@ class JellyfinAPI {
let authHeaderVal = '';
if (this.authToken) {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
} else {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
}
this.axios = axios.create({
@@ -122,7 +126,7 @@ class JellyfinAPI {
public async getServerName(): Promise<string> {
try {
const account = await this.axios.get<JellyfinUserResponse>(
`/System/Info/Public'}`
"/System/Info/Public'}"
);
return account.data.ServerName;
} catch (e) {
@@ -134,6 +138,19 @@ class JellyfinAPI {
}
}
public async getUsers(): Promise<JellyfinUserListResponse> {
try {
const account = await this.axios.get(`/Users`);
return { users: account.data };
} catch (e) {
logger.error(
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' }
);
throw new Error('Invalid auth token');
}
}
public async getUser(): Promise<JellyfinUserResponse> {
try {
const account = await this.axios.get<JellyfinUserResponse>(

View File

@@ -1,5 +1,6 @@
import NodePlexAPI from 'plex-api';
import { getSettings, Library, PlexSettings } from '../lib/settings';
import logger from '../logger';
export interface PlexLibraryItem {
ratingKey: string;
@@ -122,9 +123,9 @@ class PlexAPI {
// },
options: {
identifier: settings.clientId,
product: 'Jellyseerr',
deviceName: 'Jellyseerr',
platform: 'Jellyseerr',
product: 'Overseerr',
deviceName: 'Overseerr',
platform: 'Overseerr',
},
});
}
@@ -145,28 +146,40 @@ class PlexAPI {
public async syncLibraries(): Promise<void> {
const settings = getSettings();
const libraries = await this.getLibraries();
try {
const libraries = await this.getLibraries();
const newLibraries: Library[] = libraries
// Remove libraries that are not movie or show
.filter((library) => library.type === 'movie' || library.type === 'show')
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
.filter((library) => library.agent !== 'com.plexapp.agents.none')
.map((library) => {
const existing = settings.plex.libraries.find(
(l) => l.id === library.key && l.name === library.title
);
const newLibraries: Library[] = libraries
// Remove libraries that are not movie or show
.filter(
(library) => library.type === 'movie' || library.type === 'show'
)
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
.filter((library) => library.agent !== 'com.plexapp.agents.none')
.map((library) => {
const existing = settings.plex.libraries.find(
(l) => l.id === library.key && l.name === library.title
);
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
type: library.type,
lastScan: existing?.lastScan,
};
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
type: library.type,
lastScan: existing?.lastScan,
};
});
settings.plex.libraries = newLibraries;
} catch (e) {
logger.error('Failed to fetch Plex libraries', {
label: 'Plex API',
message: e.message,
});
settings.plex.libraries = newLibraries;
settings.plex.libraries = [];
}
settings.save();
}

View File

@@ -224,7 +224,7 @@ class PlexTvAPI {
const users = friends.MediaContainer.User;
const user = users.find((u) => Number(u.$.id) === userId);
const user = users.find((u) => parseInt(u.$.id) === userId);
if (!user) {
throw new Error(

View File

@@ -2,6 +2,35 @@ import cacheManager, { AvailableCacheIds } from '../../lib/cache';
import { DVRSettings } from '../../lib/settings';
import ExternalAPI from '../externalapi';
export interface SystemStatus {
version: string;
buildTime: Date;
isDebug: boolean;
isProduction: boolean;
isAdmin: boolean;
isUserInteractive: boolean;
startupPath: string;
appData: string;
osName: string;
osVersion: string;
isNetCore: boolean;
isMono: boolean;
isLinux: boolean;
isOsx: boolean;
isWindows: boolean;
isDocker: boolean;
mode: string;
branch: string;
authentication: string;
sqliteVersion: string;
migrationVersion: number;
urlBase: string;
runtimeVersion: string;
runtimeName: string;
startTime: Date;
packageUpdateMechanism: string;
}
export interface RootFolder {
id: number;
path: string;
@@ -81,6 +110,18 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
this.apiName = apiName;
}
public getSystemStatus = async (): Promise<SystemStatus> => {
try {
const response = await this.axios.get<SystemStatus>('/system/status');
return response.data;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
);
}
};
public getProfiles = async (): Promise<QualityProfile[]> => {
try {
const data = await this.getRolling<QualityProfile[]>(

View File

@@ -1,7 +1,7 @@
import logger from '../../logger';
import ServarrBase from './base';
interface RadarrMovieOptions {
export interface RadarrMovieOptions {
title: string;
qualityProfileId: number;
minimumAvailability: string;
@@ -27,7 +27,6 @@ export interface RadarrMovie {
profileId: number;
qualityProfileId: number;
added: string;
downloaded: boolean;
hasFile: boolean;
}
@@ -85,7 +84,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
try {
const movie = await this.getMovieByTmdbId(options.tmdbId);
if (movie.downloaded) {
if (movie.hasFile) {
logger.info(
'Title already exists and is available. Skipping add and returning success',
{

View File

@@ -63,7 +63,7 @@ export interface SonarrSeries {
};
}
interface AddSeriesOptions {
export interface AddSeriesOptions {
tvdbid: number;
title: string;
profileId: number;
@@ -149,6 +149,7 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
// If the series already exists, we will simply just update it
if (series.id) {
series.monitored = options.monitored ?? series.monitored;
series.tags = options.tags ?? series.tags;
series.seasons = this.buildSeasonList(options.seasons, series.seasons);

293
server/api/tautulli.ts Normal file
View File

@@ -0,0 +1,293 @@
import axios, { AxiosInstance } 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;
duration: number;
friendly_name: string;
full_title: string;
grandparent_rating_key: number;
grandparent_title: string;
original_title: string;
group_count: number;
group_ids?: string;
guid: string;
ip_address: string;
live: number;
machine_id: string;
media_index: number;
media_type: string;
originally_available_at: string;
parent_media_index: number;
parent_rating_key: number;
parent_title: string;
paused_counter: number;
percent_complete: number;
platform: string;
product: string;
player: string;
rating_key: number;
reference_id?: number;
row_id?: number;
session_key?: string;
started: number;
state?: string;
stopped: number;
thumb: string;
title: string;
transcode_decision: string;
user: string;
user_id: number;
watched_status: number;
year: number;
}
interface TautulliHistoryResponse {
response: {
result: string;
message?: string;
data: {
draw: number;
recordsTotal: number;
recordsFiltered: number;
total_duration: string;
filter_duration: string;
data: TautulliHistoryRecord[];
};
};
}
interface TautulliWatchStats {
query_days: number;
total_time: number;
total_plays: number;
}
interface TautulliWatchStatsResponse {
response: {
result: string;
message?: string;
data: TautulliWatchStats[];
};
}
interface TautulliWatchUser {
friendly_name: string;
user_id: number;
user_thumb: string;
username: string;
total_plays: number;
total_time: number;
}
interface TautulliWatchUsersResponse {
response: {
result: string;
message?: string;
data: TautulliWatchUser[];
};
}
interface TautulliInfo {
tautulli_install_type: string;
tautulli_version: string;
tautulli_branch: string;
tautulli_commit: string;
tautulli_platform: string;
tautulli_platform_release: string;
tautulli_platform_version: string;
tautulli_platform_linux_distro: string;
tautulli_platform_device_name: string;
tautulli_python_version: string;
}
interface TautulliInfoResponse {
response: {
result: string;
message?: string;
data: TautulliInfo;
};
}
class TautulliAPI {
private axios: AxiosInstance;
constructor(settings: TautulliSettings) {
this.axios = axios.create({
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
settings.port
}${settings.urlBase ?? ''}`,
params: { apikey: settings.apiKey },
});
}
public async getInfo(): Promise<TautulliInfo> {
try {
return (
await this.axios.get<TautulliInfoResponse>('/api/v2', {
params: { cmd: 'get_tautulli_info' },
})
).data.response.data;
} catch (e) {
logger.error('Something went wrong fetching Tautulli server info', {
label: 'Tautulli API',
errorMessage: e.message,
});
throw new Error(
`[Tautulli] Failed to fetch Tautulli server info: ${e.message}`
);
}
}
public async getMediaWatchStats(
ratingKey: string
): Promise<TautulliWatchStats[]> {
try {
return (
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
params: {
cmd: 'get_item_watch_time_stats',
rating_key: ratingKey,
grouping: 1,
},
})
).data.response.data;
} catch (e) {
logger.error(
'Something went wrong fetching media watch stats from Tautulli',
{
label: 'Tautulli API',
errorMessage: e.message,
ratingKey,
}
);
throw new Error(
`[Tautulli] Failed to fetch media watch stats: ${e.message}`
);
}
}
public async getMediaWatchUsers(
ratingKey: string
): Promise<TautulliWatchUser[]> {
try {
return (
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
params: {
cmd: 'get_item_user_stats',
rating_key: ratingKey,
grouping: 1,
},
})
).data.response.data;
} catch (e) {
logger.error(
'Something went wrong fetching media watch users from Tautulli',
{
label: 'Tautulli API',
errorMessage: e.message,
ratingKey,
}
);
throw new Error(
`[Tautulli] Failed to fetch media watch users: ${e.message}`
);
}
}
public async getUserWatchStats(user: User): Promise<TautulliWatchStats> {
try {
if (!user.plexId) {
throw new Error('User does not have an associated Plex ID');
}
return (
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
params: {
cmd: 'get_user_watch_time_stats',
user_id: user.plexId,
query_days: 0,
grouping: 1,
},
})
).data.response.data[0];
} catch (e) {
logger.error(
'Something went wrong fetching user watch stats from Tautulli',
{
label: 'Tautulli API',
errorMessage: e.message,
user: user.displayName,
}
);
throw new Error(
`[Tautulli] Failed to fetch user watch stats: ${e.message}`
);
}
}
public async getUserWatchHistory(
user: User
): Promise<TautulliHistoryRecord[]> {
let results: TautulliHistoryRecord[] = [];
try {
if (!user.plexId) {
throw new Error('User does not have an associated Plex ID');
}
const take = 100;
let start = 0;
while (results.length < 20) {
const tautulliData = (
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
params: {
cmd: 'get_history',
grouping: 1,
order_column: 'date',
order_dir: 'desc',
user_id: user.plexId,
media_type: 'movie,episode',
length: take,
start,
},
})
).data.response.data.data;
if (!tautulliData.length) {
return results;
}
results = uniqWith(results.concat(tautulliData), (recordA, recordB) =>
recordA.grandparent_rating_key && recordB.grandparent_rating_key
? recordA.grandparent_rating_key === recordB.grandparent_rating_key
: recordA.parent_rating_key && recordB.parent_rating_key
? recordA.parent_rating_key === recordB.parent_rating_key
: recordA.rating_key === recordB.rating_key
);
start += take;
}
return results.slice(0, 20);
} catch (e) {
logger.error(
'Something went wrong fetching user watch history from Tautulli',
{
label: 'Tautulli API',
errorMessage: e.message,
user: user.displayName,
}
);
throw new Error(
`[Tautulli] Failed to fetch user watch history: ${e.message}`
);
}
}
}
export default TautulliAPI;

View File

@@ -10,7 +10,7 @@ import {
TmdbMovieDetails,
TmdbNetwork,
TmdbPersonCombinedCredits,
TmdbPersonDetail,
TmdbPersonDetails,
TmdbProductionCompany,
TmdbRegion,
TmdbSearchMovieResponse,
@@ -28,6 +28,10 @@ interface SearchOptions {
language?: string;
}
interface SingleSearchOptions extends SearchOptions {
year?: number;
}
interface DiscoverMovieOptions {
page?: number;
includeAdult?: boolean;
@@ -116,15 +120,67 @@ class TheMovieDb extends ExternalAPI {
}
};
public searchMovies = async ({
query,
page = 1,
includeAdult = false,
language = 'en',
year,
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
try {
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
params: { query, page, include_adult: includeAdult, language, year },
});
return data;
} catch (e) {
return {
page: 1,
results: [],
total_pages: 1,
total_results: 0,
};
}
};
public searchTvShows = async ({
query,
page = 1,
includeAdult = false,
language = 'en',
year,
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
try {
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
params: {
query,
page,
include_adult: includeAdult,
language,
first_air_date_year: year,
},
});
return data;
} catch (e) {
return {
page: 1,
results: [],
total_pages: 1,
total_results: 0,
};
}
};
public getPerson = async ({
personId,
language = 'en',
}: {
personId: number;
language?: string;
}): Promise<TmdbPersonDetail> => {
}): Promise<TmdbPersonDetails> => {
try {
const data = await this.get<TmdbPersonDetail>(`/person/${personId}`, {
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
params: { language },
});
@@ -561,13 +617,13 @@ class TheMovieDb extends ExternalAPI {
}
}
public async getMovieByImdbId({
public async getMediaByImdbId({
imdbId,
language = 'en',
}: {
imdbId: string;
language?: string;
}): Promise<TmdbMovieDetails> {
}): Promise<TmdbMovieDetails | TmdbTvDetails> {
try {
const extResponse = await this.getByExternalId({
externalId: imdbId,
@@ -583,12 +639,19 @@ class TheMovieDb extends ExternalAPI {
return movie;
}
throw new Error(
'[TMDb] Failed to find a title with the provided IMDB id'
);
if (extResponse.tv_results[0]) {
const tvshow = await this.getTvShow({
tvId: extResponse.tv_results[0].id,
language,
});
return tvshow;
}
throw new Error(`No movie or show returned from API for ID ${imdbId}`);
} catch (e) {
throw new Error(
`[TMDb] Failed to get movie by external imdb ID: ${e.message}`
`[TMDb] Failed to find media using external IMDb ID: ${e.message}`
);
}
}

View File

@@ -67,6 +67,7 @@ export interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
export interface TmdbExternalIdResponse {
movie_results: TmdbMovieResult[];
tv_results: TmdbTvResult[];
person_results: TmdbPersonResult[];
}
export interface TmdbCreditCast {
@@ -251,6 +252,10 @@ export interface TmdbTvDetails {
name: string;
origin_country: string;
}[];
production_countries: {
iso_3166_1: string;
name: string;
}[];
spoken_languages: {
english_name: string;
iso_639_1: string;
@@ -311,7 +316,7 @@ export interface TmdbKeyword {
name: string;
}
export interface TmdbPersonDetail {
export interface TmdbPersonDetails {
id: number;
name: string;
birthday: string;
@@ -320,7 +325,7 @@ export interface TmdbPersonDetail {
also_known_as?: string[];
gender: number;
biography: string;
popularity: string;
popularity: number;
place_of_birth?: string;
profile_path?: string;
adult: boolean;

18
server/constants/issue.ts Normal file
View File

@@ -0,0 +1,18 @@
export enum IssueType {
VIDEO = 1,
AUDIO = 2,
SUBTITLES = 3,
OTHER = 4,
}
export enum IssueStatus {
OPEN = 1,
RESOLVED = 2,
}
export const IssueTypeName = {
[IssueType.AUDIO]: 'Audio',
[IssueType.VIDEO]: 'Video',
[IssueType.SUBTITLES]: 'Subtitle',
[IssueType.OTHER]: 'Other',
};

68
server/entity/Issue.ts Normal file
View File

@@ -0,0 +1,68 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { IssueStatus, IssueType } from '../constants/issue';
import IssueComment from './IssueComment';
import Media from './Media';
import { User } from './User';
@Entity()
class Issue {
@PrimaryGeneratedColumn()
public id: number;
@Column({ type: 'int' })
public issueType: IssueType;
@Column({ type: 'int', default: IssueStatus.OPEN })
public status: IssueStatus;
@Column({ type: 'int', default: 0 })
public problemSeason: number;
@Column({ type: 'int', default: 0 })
public problemEpisode: number;
@ManyToOne(() => Media, (media) => media.issues, {
eager: true,
onDelete: 'CASCADE',
})
public media: Media;
@ManyToOne(() => User, (user) => user.createdIssues, {
eager: true,
onDelete: 'CASCADE',
})
public createdBy: User;
@ManyToOne(() => User, {
eager: true,
onDelete: 'CASCADE',
nullable: true,
})
public modifiedBy?: User;
@OneToMany(() => IssueComment, (comment) => comment.issue, {
cascade: true,
eager: true,
})
public comments: IssueComment[];
@CreateDateColumn()
public createdAt: Date;
@UpdateDateColumn()
public updatedAt: Date;
constructor(init?: Partial<Issue>) {
Object.assign(this, init);
}
}
export default Issue;

View File

@@ -0,0 +1,42 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import Issue from './Issue';
import { User } from './User';
@Entity()
class IssueComment {
@PrimaryGeneratedColumn()
public id: number;
@ManyToOne(() => User, {
eager: true,
onDelete: 'CASCADE',
})
public user: User;
@ManyToOne(() => Issue, (issue) => issue.comments, {
onDelete: 'CASCADE',
})
public issue: Issue;
@Column({ type: 'text' })
public message: string;
@CreateDateColumn()
public createdAt: Date;
@UpdateDateColumn()
public updatedAt: Date;
constructor(init?: Partial<IssueComment>) {
Object.assign(this, init);
}
}
export default IssueComment;

View File

@@ -17,6 +17,7 @@ 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';
@@ -55,7 +56,7 @@ class Media {
try {
const media = await mediaRepository.findOne({
where: { tmdbId: id, mediaType },
relations: ['requests'],
relations: ['requests', 'issues'],
});
return media;
@@ -98,6 +99,9 @@ class Media {
})
public seasons: Season[];
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
public issues: Issue[];
@CreateDateColumn()
public createdAt: Date;
@@ -148,27 +152,55 @@ class Media {
public mediaUrl?: string;
public mediaUrl4k?: string;
public tautulliUrl?: string;
public tautulliUrl4k?: string;
constructor(init?: Partial<Media>) {
Object.assign(this, init);
}
@AfterLoad()
public setMediaUrls(): void {
const settings = getSettings();
if (settings.main.mediaServerType == MediaServerType.PLEX) {
public setPlexUrls(): void {
const { machineId, webAppUrl } = getSettings().plex;
const { externalUrl: tautulliUrl } = getSettings().tautulli;
if (getSettings().main.mediaServerType == MediaServerType.PLEX) {
if (this.ratingKey) {
this.mediaUrl = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`;
this.mediaUrl = `${
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
this.ratingKey
}`;
if (tautulliUrl) {
this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`;
}
}
if (this.ratingKey4k) {
this.mediaUrl4k = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${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}`;
}
}
} else {
const pageName = process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
const pageName =
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
const { serverId, hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
if (this.jellyfinMediaId) {
this.mediaUrl = `${settings.jellyfin.hostname}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverId}`;
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
}
if (this.jellyfinMediaId4k) {
this.mediaUrl4k = `${settings.jellyfin.hostname}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverId}`;
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
}
}
}

View File

@@ -13,8 +13,11 @@ import {
RelationCount,
UpdateDateColumn,
} from 'typeorm';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI, { SonarrSeries } from '../api/servarr/sonarr';
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';
@@ -135,51 +138,15 @@ export class MediaRequest {
where: { id: this.media.id },
});
if (!media) {
logger.error('No parent media!', { label: 'Media Request' });
logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return;
}
const tmdb = new TheMovieDb();
if (this.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, {
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
media,
request: this,
});
}
if (this.type === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, {
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media,
extra: [
{
name: 'Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: this,
});
}
this.sendNotification(media, Notification.MEDIA_PENDING);
}
}
@@ -200,74 +167,30 @@ export class MediaRequest {
where: { id: this.media.id },
});
if (!media) {
logger.error('No parent media!', { label: 'Media Request' });
logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return;
}
if (media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE) {
logger.warn(
'Media became available before request was approved. Approval notification will be skipped.',
{ label: 'Media Request' }
'Media became available before request was approved. Skipping approval notification',
{ label: 'Media Request', requestId: this.id, mediaId: this.media.id }
);
return;
}
const tmdb = new TheMovieDb();
if (this.media.mediaType === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
notificationManager.sendNotification(
this.status === MediaRequestStatus.APPROVED
? autoApproved
? Notification.MEDIA_AUTO_APPROVED
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
request: this,
}
);
} else if (this.media.mediaType === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: this.media.tmdbId });
notificationManager.sendNotification(
this.status === MediaRequestStatus.APPROVED
? autoApproved
? Notification.MEDIA_AUTO_APPROVED
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
extra: [
{
name: 'Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: this,
}
);
}
this.sendNotification(
media,
this.status === MediaRequestStatus.APPROVED
? autoApproved
? Notification.MEDIA_AUTO_APPROVED
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED
);
}
}
@@ -287,7 +210,11 @@ export class MediaRequest {
relations: ['requests'],
});
if (!media) {
logger.error('No parent media!', { label: 'Media Request' });
logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return;
}
const seasonRequestRepository = getRepository(SeasonRequest);
@@ -375,8 +302,12 @@ export class MediaRequest {
const settings = getSettings();
if (settings.radarr.length === 0 && !settings.radarr[0]) {
logger.info(
'Skipped Radarr request as there is no Radarr server configured',
{ label: 'Media Request' }
'No Radarr server configured, skipping request processing',
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
return;
}
@@ -395,18 +326,26 @@ export class MediaRequest {
);
logger.info(
`Request has an override server: ${radarrSettings?.name}`,
{ label: 'Media Request' }
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
}
if (!radarrSettings) {
logger.info(
logger.warn(
`There is no default ${
this.is4k ? '4K ' : ''
}Radarr server configured. Did you set any of your ${
this.is4k ? '4K ' : ''
}Radarr servers as default?`,
{ label: 'Media Request' }
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
return;
}
@@ -423,6 +362,8 @@ export class MediaRequest {
rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
}
@@ -431,15 +372,22 @@ export class MediaRequest {
this.profileId !== radarrSettings.activeProfileId
) {
qualityProfile = this.profileId;
logger.info(`Request has an override profile id: ${qualityProfile}`, {
label: 'Media Request',
});
logger.info(
`Request has an override quality profile ID: ${qualityProfile}`,
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
}
if (this.tags && !isEqual(this.tags, radarrSettings.tags)) {
tags = this.tags;
logger.info(`Request has override tags`, {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
tagIds: tags,
});
}
@@ -456,7 +404,11 @@ export class MediaRequest {
});
if (!media) {
logger.error('Media not present');
logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return;
}
@@ -466,20 +418,22 @@ export class MediaRequest {
throw new Error('Media already available');
}
const radarrMovieOptions: RadarrMovieOptions = {
profileId: qualityProfile,
qualityProfileId: qualityProfile,
rootFolderPath: rootFolder,
minimumAvailability: radarrSettings.minimumAvailability,
title: movie.title,
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
tags,
searchNow: !radarrSettings.preventSearch,
};
// Run this asynchronously so we don't wait for it on the UI side
radarr
.addMovie({
profileId: qualityProfile,
qualityProfileId: qualityProfile,
rootFolderPath: rootFolder,
minimumAvailability: radarrSettings.minimumAvailability,
title: movie.title,
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
tags,
searchNow: !radarrSettings.preventSearch,
})
.addMovie(radarrMovieOptions)
.then(async (radarrMovie) => {
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
@@ -487,7 +441,7 @@ export class MediaRequest {
});
if (!media) {
throw new Error('Media data is missing');
throw new Error('Media data not found');
}
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
@@ -501,34 +455,30 @@ export class MediaRequest {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added movie request failed to add to Radarr, marking as unknown',
'Something went wrong sending movie request to Radarr, marking status as UNKNOWN',
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
radarrMovieOptions,
}
);
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: this,
});
this.sendNotification(media, Notification.MEDIA_FAILED);
});
logger.info('Sent request to Radarr', { label: 'Media Request' });
} catch (e) {
const errorMessage = `Request failed to send to Radarr: ${e.message}`;
logger.error('Request failed to send to Radarr', {
logger.info('Sent request to Radarr', {
label: 'Media Request',
errorMessage,
requestId: this.id,
mediaId: this.media.id,
});
throw new Error(errorMessage);
} catch (e) {
logger.error('Something went wrong sending request to Radarr', {
label: 'Media Request',
errorMessage: e.message,
requestId: this.id,
mediaId: this.media.id,
});
throw new Error(e.message);
}
}
}
@@ -542,9 +492,13 @@ export class MediaRequest {
const mediaRepository = getRepository(Media);
const settings = getSettings();
if (settings.sonarr.length === 0 && !settings.sonarr[0]) {
logger.info(
'Skipped Sonarr request as there is no Sonarr server configured',
{ label: 'Media Request' }
logger.warn(
'No Sonarr server configured, skipping request processing',
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
return;
}
@@ -563,18 +517,26 @@ export class MediaRequest {
);
logger.info(
`Request has an override server: ${sonarrSettings?.name}`,
{ label: 'Media Request' }
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
}
if (!sonarrSettings) {
logger.info(
logger.warn(
`There is no default ${
this.is4k ? '4K ' : ''
}Sonarr server configured. Did you set any of your ${
this.is4k ? '4K ' : ''
}Sonarr servers as default?`,
{ label: 'Media Request' }
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
return;
}
@@ -585,7 +547,7 @@ export class MediaRequest {
});
if (!media) {
throw new Error('Media data is missing');
throw new Error('Media data not found');
}
if (
@@ -606,7 +568,7 @@ export class MediaRequest {
const requestRepository = getRepository(MediaRequest);
await mediaRepository.remove(media);
await requestRepository.remove(this);
throw new Error('Series was missing tvdb id');
throw new Error('TVDB ID not found');
}
let seriesType: SonarrSeries['seriesType'] = 'standard';
@@ -628,12 +590,10 @@ export class MediaRequest {
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId;
let languageProfile =
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
? sonarrSettings.activeAnimeLanguageProfileId
: sonarrSettings.activeLanguageProfileId;
let tags =
seriesType === 'anime'
? sonarrSettings.animeTags
@@ -647,14 +607,21 @@ export class MediaRequest {
rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
}
if (this.profileId && this.profileId !== qualityProfile) {
qualityProfile = this.profileId;
logger.info(`Request has an override profile ID: ${qualityProfile}`, {
label: 'Media Request',
});
logger.info(
`Request has an override quality profile ID: ${qualityProfile}`,
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
}
if (
@@ -663,9 +630,11 @@ export class MediaRequest {
) {
languageProfile = this.languageProfileId;
logger.info(
`Request has an override Language Profile: ${languageProfile}`,
`Request has an override language profile ID: ${languageProfile}`,
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
}
@@ -674,25 +643,29 @@ export class MediaRequest {
tags = this.tags;
logger.info(`Request has override tags`, {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
tagIds: tags,
});
}
const sonarrSeriesOptions: AddSeriesOptions = {
profileId: qualityProfile,
languageProfileId: languageProfile,
rootFolderPath: rootFolder,
title: series.name,
tvdbid: tvdbId,
seasons: this.seasons.map((season) => season.seasonNumber),
seasonFolder: sonarrSettings.enableSeasonFolders,
seriesType,
tags,
monitored: true,
searchNow: !sonarrSettings.preventSearch,
};
// Run this asynchronously so we don't wait for it on the UI side
sonarr
.addSeries({
profileId: qualityProfile,
languageProfileId: languageProfile,
rootFolderPath: rootFolder,
title: series.name,
tvdbid: tvdbId,
seasons: this.seasons.map((season) => season.seasonNumber),
seasonFolder: sonarrSettings.enableSeasonFolders,
seriesType,
tags,
monitored: true,
searchNow: !sonarrSettings.preventSearch,
})
.addSeries(sonarrSeriesOptions)
.then(async (sonarrSeries) => {
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
@@ -701,7 +674,7 @@ export class MediaRequest {
});
if (!media) {
throw new Error('Media data is missing');
throw new Error('Media data not found');
}
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
@@ -715,45 +688,116 @@ export class MediaRequest {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added series request failed to add to Sonarr, marking as unknown',
'Something went wrong sending series request to Sonarr, marking status as UNKNOWN',
{
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
sonarrSeriesOptions,
}
);
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: `${series.name}${
series.first_air_date
? ` (${series.first_air_date.slice(0, 4)})`
: ''
}`,
message: truncate(series.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media,
extra: [
{
name: 'Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: this,
});
this.sendNotification(media, Notification.MEDIA_FAILED);
});
logger.info('Sent request to Sonarr', { label: 'Media Request' });
} catch (e) {
const errorMessage = `Request failed to send to Sonarr: ${e.message}`;
logger.error('Request failed to send to Sonarr', {
logger.info('Sent request to Sonarr', {
label: 'Media Request',
errorMessage,
requestId: this.id,
mediaId: this.media.id,
});
throw new Error(errorMessage);
} catch (e) {
logger.error('Something went wrong sending request to Sonarr', {
label: 'Media Request',
errorMessage: e.message,
requestId: this.id,
mediaId: this.media.id,
});
throw new Error(e.message);
}
}
}
private async sendNotification(media: Media, type: Notification) {
const tmdb = new TheMovieDb();
try {
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
let event: string | undefined;
let notifyAdmin = true;
switch (type) {
case Notification.MEDIA_APPROVED:
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Approved`;
notifyAdmin = false;
break;
case Notification.MEDIA_DECLINED:
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Declined`;
notifyAdmin = false;
break;
case Notification.MEDIA_PENDING:
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
break;
case Notification.MEDIA_AUTO_APPROVED:
event = `${
this.is4k ? '4K ' : ''
}${mediaType} Request Automatically Approved`;
break;
case Notification.MEDIA_FAILED:
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Failed`;
break;
}
if (this.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(type, {
media,
request: this,
notifyAdmin,
notifyUser: notifyAdmin ? undefined : this.requestedBy,
event,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
} else if (this.type === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(type, {
media,
request: this,
notifyAdmin,
notifyUser: notifyAdmin ? undefined : this.requestedBy,
event,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
extra: [
{
name: 'Requested Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
});
}
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
requestId: this.id,
mediaId: this.media.id,
});
}
}
}

View File

@@ -27,6 +27,7 @@ import {
} from '../lib/permissions';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import Issue from './Issue';
import { MediaRequest } from './MediaRequest';
import SeasonRequest from './SeasonRequest';
import { UserPushSubscription } from './UserPushSubscription';
@@ -61,7 +62,7 @@ export class User {
public plexUsername?: string;
@Column({ nullable: true })
public jellyfinUsername: string;
public jellyfinUsername?: string;
@Column({ nullable: true })
public username?: string;
@@ -127,6 +128,9 @@ export class User {
@OneToMany(() => UserPushSubscription, (pushSub) => pushSub.user)
public pushSubscriptions: UserPushSubscription[];
@OneToMany(() => Issue, (issue) => issue.createdBy, { cascade: true })
public createdIssues: Issue[];
@CreateDateColumn()
public createdAt: Date;
@@ -190,6 +194,7 @@ export class User {
password: password,
applicationUrl,
applicationTitle,
recipientName: this.username,
},
});
} catch (e) {
@@ -226,6 +231,8 @@ export class User {
resetPasswordLink,
applicationUrl,
applicationTitle,
recipientName: this.displayName,
recipientEmail: this.email,
},
});
} catch (e) {
@@ -239,8 +246,7 @@ export class User {
@AfterLoad()
public setDisplayName(): void {
this.displayName =
this.username || this.plexUsername || this.jellyfinUsername;
this.displayName = this.username || this.plexUsername || this.email;
this.username || this.plexUsername || this.jellyfinUsername || this.email;
}
public async getQuota(): Promise<QuotaResponse> {

View File

@@ -42,6 +42,15 @@ export class UserSettings {
@Column({ nullable: true })
public discordId?: string;
@Column({ nullable: true })
public pushbulletAccessToken?: string;
@Column({ nullable: true })
public pushoverApplicationToken?: string;
@Column({ nullable: true })
public pushoverUserKey?: string;
@Column({ nullable: true })
public telegramChatId?: string;

View File

@@ -17,6 +17,7 @@ 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';
@@ -31,7 +32,7 @@ import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
@@ -63,11 +64,12 @@ app
});
if (admin) {
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
await plexapi.syncLibraries();
logger.info('Migrating libraries to include media type', {
logger.info('Migrating Plex libraries to include media type', {
label: 'Settings',
});
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
await plexapi.syncLibraries();
}
}
@@ -75,6 +77,7 @@ app
notificationManager.registerAgents([
new DiscordAgent(),
new EmailAgent(),
new GotifyAgent(),
new LunaSeaAgent(),
new PushbulletAgent(),
new PushoverAgent(),
@@ -138,6 +141,9 @@ app
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: true,
secure: 'auto',
},
store: new TypeormStore({
cleanupLimit: 2,

View File

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

View File

@@ -1,6 +1,22 @@
import type Media from '../../entity/Media';
import { User } from '../../entity/User';
import { PaginatedResponse } from './common';
export interface MediaResultsResponse extends PaginatedResponse {
results: Media[];
}
export interface MediaWatchDataResponse {
data?: {
users: User[];
playCount: number;
playCount7Days: number;
playCount30Days: number;
};
data4k?: {
users: User[];
playCount: number;
playCount7Days: number;
playCount30Days: number;
};
}

View File

@@ -3,7 +3,7 @@ import type { PaginatedResponse } from './common';
export type LogMessage = {
timestamp: string;
level: string;
label: string;
label?: string;
message: string;
data?: Record<string, unknown>;
};
@@ -17,6 +17,7 @@ export interface SettingsAboutResponse {
totalRequests: number;
totalMediaItems: number;
tz?: string;
appDataPath: string;
}
export interface PublicSettingsResponse {
@@ -38,6 +39,7 @@ export interface PublicSettingsResponse {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
newPlexLogin: boolean;
}
export interface CacheItem {

View File

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

View File

@@ -2,6 +2,7 @@ import { NotificationAgentKey } from '../../lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
discordId?: string;
locale?: string;
region?: string;
originalLanguage?: string;
@@ -22,6 +23,9 @@ export interface UserSettingsNotificationsResponse {
discordEnabled?: boolean;
discordEnabledTypes?: number;
discordId?: string;
pushbulletAccessToken?: string;
pushoverApplicationToken?: string;
pushoverUserKey?: string;
telegramEnabled?: boolean;
telegramBotUsername?: string;
telegramChatId?: string;

View File

@@ -71,7 +71,7 @@ class JobJellyfinSync {
newMedia.tmdbId = Number(metadata.ProviderIds.Tmdb ?? null);
newMedia.imdbId = metadata.ProviderIds.Imdb;
if (newMedia.imdbId && !isNaN(newMedia.tmdbId)) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
const tmdbMovie = await this.tmdb.getMediaByImdbId({
imdbId: newMedia.imdbId,
});
newMedia.tmdbId = tmdbMovie.id;

View File

@@ -1,16 +1,19 @@
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 {
id: string;
id: JobId;
job: schedule.Job;
name: string;
type: 'process' | 'command';
interval: 'short' | 'long' | 'fixed';
running?: () => boolean;
cancelFn?: () => void;
}
@@ -18,72 +21,91 @@ interface ScheduledJob {
export const scheduledJobs: ScheduledJob[] = [];
export const startJobs = (): void => {
// Run recently added plex scan every 5 minutes
scheduledJobs.push({
id: 'plex-recently-added-scan',
name: 'Plex Recently Added Scan',
type: 'process',
job: schedule.scheduleJob('0 */5 * * * *', () => {
logger.info('Starting scheduled job: Plex Recently Added Scan', {
label: 'Jobs',
});
plexRecentScanner.run();
}),
running: () => plexRecentScanner.status().running,
cancelFn: () => plexRecentScanner.cancel(),
});
const jobs = getSettings().jobs;
const mediaServerType = getSettings().main.mediaServerType;
// Run full plex scan every 24 hours
scheduledJobs.push({
id: 'plex-full-scan',
name: 'Plex Full Library Scan',
type: 'process',
job: schedule.scheduleJob('0 0 3 * * *', () => {
logger.info('Starting scheduled job: Plex Full Library Scan', {
label: 'Jobs',
});
plexFullScanner.run();
}),
running: () => plexFullScanner.status().running,
cancelFn: () => plexFullScanner.cancel(),
});
if (mediaServerType === MediaServerType.PLEX) {
// Run recently added plex scan every 5 minutes
scheduledJobs.push({
id: 'plex-recently-added-scan',
name: 'Plex Recently Added Scan',
type: 'process',
interval: 'short',
job: schedule.scheduleJob(
jobs['plex-recently-added-scan'].schedule,
() => {
logger.info('Starting scheduled job: Plex Recently Added Scan', {
label: 'Jobs',
});
plexRecentScanner.run();
}
),
running: () => plexRecentScanner.status().running,
cancelFn: () => plexRecentScanner.cancel(),
});
// Run recently added jellyfin sync every 5 minutes
scheduledJobs.push({
id: 'jellyfin-recently-added-sync',
name: 'Jellyfin Recently Added Sync',
type: 'process',
job: schedule.scheduleJob('0 */5 * * * *', () => {
logger.info('Starting scheduled job: Jellyfin Recently Added Sync', {
label: 'Jobs',
});
jobJellyfinRecentSync.run();
}),
running: () => jobJellyfinRecentSync.status().running,
cancelFn: () => jobJellyfinRecentSync.cancel(),
});
// Run full plex scan every 24 hours
scheduledJobs.push({
id: 'plex-full-scan',
name: 'Plex Full Library Scan',
type: 'process',
interval: 'long',
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Full Library Scan', {
label: 'Jobs',
});
plexFullScanner.run();
}),
running: () => plexFullScanner.status().running,
cancelFn: () => plexFullScanner.cancel(),
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
// Run recently added jellyfin sync every 5 minutes
scheduledJobs.push({
id: 'jellyfin-recently-added-sync',
name: 'Jellyfin Recently Added Sync',
type: 'process',
interval: 'long',
job: schedule.scheduleJob(
jobs['jellyfin-recently-added-sync'].schedule,
() => {
logger.info('Starting scheduled job: Jellyfin Recently Added Sync', {
label: 'Jobs',
});
jobJellyfinRecentSync.run();
}
),
running: () => jobJellyfinRecentSync.status().running,
cancelFn: () => jobJellyfinRecentSync.cancel(),
});
// Run full jellyfin sync every 24 hours
scheduledJobs.push({
id: 'jellyfin-full-sync',
name: 'Jellyfin Full Library Sync',
type: 'process',
job: schedule.scheduleJob('0 0 3 * * *', () => {
logger.info('Starting scheduled job: Jellyfin Full Sync', {
label: 'Jobs',
});
jobJellyfinFullSync.run();
}),
running: () => jobJellyfinFullSync.status().running,
cancelFn: () => jobJellyfinFullSync.cancel(),
});
// Run full jellyfin sync every 24 hours
scheduledJobs.push({
id: 'jellyfin-full-sync',
name: 'Jellyfin Full Library Sync',
type: 'process',
interval: 'long',
job: schedule.scheduleJob(jobs['jellyfin-full-sync'].schedule, () => {
logger.info('Starting scheduled job: Jellyfin Full Sync', {
label: 'Jobs',
});
jobJellyfinFullSync.run();
}),
running: () => jobJellyfinFullSync.status().running,
cancelFn: () => jobJellyfinFullSync.cancel(),
});
}
// Run full radarr scan every 24 hours
scheduledJobs.push({
id: 'radarr-scan',
name: 'Radarr Scan',
type: 'process',
job: schedule.scheduleJob('0 0 4 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
radarrScanner.run();
}),
@@ -96,7 +118,8 @@ export const startJobs = (): void => {
id: 'sonarr-scan',
name: 'Sonarr Scan',
type: 'process',
job: schedule.scheduleJob('0 30 4 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
sonarrScanner.run();
}),
@@ -104,23 +127,27 @@ export const startJobs = (): void => {
cancelFn: () => sonarrScanner.cancel(),
});
// Run download sync
// Run download sync every minute
scheduledJobs.push({
id: 'download-sync',
name: 'Download Sync',
type: 'command',
job: schedule.scheduleJob('0 * * * * *', () => {
logger.debug('Starting scheduled job: Download Sync', { label: 'Jobs' });
interval: 'fixed',
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
logger.debug('Starting scheduled job: Download Sync', {
label: 'Jobs',
});
downloadTracker.updateDownloads();
}),
});
// Reset download sync
// Reset download sync everyday at 01:00 am
scheduledJobs.push({
id: 'download-sync-reset',
name: 'Download Sync Reset',
type: 'command',
job: schedule.scheduleJob('0 0 1 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
logger.info('Starting scheduled job: Download Sync Reset', {
label: 'Jobs',
});

View File

@@ -40,7 +40,7 @@ class Cache {
class CacheManager {
private availableCaches: Record<AvailableCacheIds, Cache> = {
tmdb: new Cache('tmdb', 'TMDb API', {
tmdb: new Cache('tmdb', 'The Movie Database API', {
stdTtl: 21600,
checkPeriod: 60 * 30,
}),
@@ -54,7 +54,7 @@ class CacheManager {
stdTtl: 21600,
checkPeriod: 60 * 30,
}),
plexguid: new Cache('plexguid', 'Plex GUID Cache', {
plexguid: new Cache('plexguid', 'Plex GUID', {
stdTtl: 86400 * 7, // 1 week cache
checkPeriod: 60 * 30,
}),

View File

@@ -76,23 +76,32 @@ class DownloadTracker {
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
const queueItems = await radarr.getQueue();
try {
const queueItems = await radarr.getQueue();
this.radarrServers[server.id] = queueItems.map((item) => ({
externalId: item.movieId,
estimatedCompletionTime: new Date(item.estimatedCompletionTime),
mediaType: MediaType.MOVIE,
size: item.size,
sizeLeft: item.sizeleft,
status: item.status,
timeLeft: item.timeleft,
title: item.title,
}));
this.radarrServers[server.id] = queueItems.map((item) => ({
externalId: item.movieId,
estimatedCompletionTime: new Date(item.estimatedCompletionTime),
mediaType: MediaType.MOVIE,
size: item.size,
sizeLeft: item.sizeleft,
status: item.status,
timeLeft: item.timeleft,
title: item.title,
}));
if (queueItems.length > 0) {
logger.debug(
`Found ${queueItems.length} item(s) in progress on Radarr server: ${server.name}`,
{ label: 'Download Tracker' }
if (queueItems.length > 0) {
logger.debug(
`Found ${queueItems.length} item(s) in progress on Radarr server: ${server.name}`,
{ label: 'Download Tracker' }
);
}
} catch {
logger.error(
`Unable to get queue from Radarr server: ${server.name}`,
{
label: 'Download Tracker',
}
);
}
@@ -134,42 +143,51 @@ class DownloadTracker {
);
});
// Load downloads from Radarr servers
// Load downloads from Sonarr servers
Promise.all(
filteredServers.map(async (server) => {
if (server.syncEnabled) {
const radarr = new SonarrAPI({
const sonarr = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
const queueItems = await radarr.getQueue();
try {
const queueItems = await sonarr.getQueue();
this.sonarrServers[server.id] = queueItems.map((item) => ({
externalId: item.seriesId,
estimatedCompletionTime: new Date(item.estimatedCompletionTime),
mediaType: MediaType.TV,
size: item.size,
sizeLeft: item.sizeleft,
status: item.status,
timeLeft: item.timeleft,
title: item.title,
}));
this.sonarrServers[server.id] = queueItems.map((item) => ({
externalId: item.seriesId,
estimatedCompletionTime: new Date(item.estimatedCompletionTime),
mediaType: MediaType.TV,
size: item.size,
sizeLeft: item.sizeleft,
status: item.status,
timeLeft: item.timeleft,
title: item.title,
}));
if (queueItems.length > 0) {
logger.debug(
`Found ${queueItems.length} item(s) in progress on Sonarr server: ${server.name}`,
{ label: 'Download Tracker' }
if (queueItems.length > 0) {
logger.debug(
`Found ${queueItems.length} item(s) in progress on Sonarr server: ${server.name}`,
{ label: 'Download Tracker' }
);
}
} catch {
logger.error(
`Unable to get queue from Sonarr server: ${server.name}`,
{
label: 'Download Tracker',
}
);
}
// Duplicate this data to matching servers
const matchingServers = settings.sonarr.filter(
(rs) =>
rs.hostname === server.hostname &&
rs.port === server.port &&
rs.baseUrl === server.baseUrl &&
rs.id !== server.id
(ss) =>
ss.hostname === server.hostname &&
ss.port === server.port &&
ss.baseUrl === server.baseUrl &&
ss.id !== server.id
);
if (matchingServers.length > 0) {

View File

@@ -1,7 +1,7 @@
import { randomBytes } from 'crypto';
import MailMessage from 'nodemailer/lib/mailer/mail-message';
import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream';
import logger from '../../logger';
interface EncryptorOptions {
signingKey?: string;
@@ -26,7 +26,7 @@ class PGPEncryptor extends Transform {
// just save the whole message
_transform = (
chunk: Uint8Array,
chunk: any,
_encoding: BufferEncoding,
callback: TransformCallback
): void => {
@@ -37,146 +37,164 @@ class PGPEncryptor extends Transform {
// Actually do stuff
_flush = async (callback: TransformCallback): Promise<void> => {
// Reconstruct message as buffer
const message = Buffer.concat(this._messageChunks, this._messageLength);
const validPublicKeys = await Promise.all(
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey }))
);
let privateKey: openpgp.PrivateKey | undefined;
// Just return the message if there is no one to encrypt for
if (!validPublicKeys.length) {
this.push(message);
return callback();
}
try {
// Reconstruct message as buffer
const validPublicKeys = await Promise.all(
this._encryptionKeys.map((armoredKey) =>
openpgp.readKey({ armoredKey })
)
);
let privateKey: openpgp.PrivateKey | undefined;
// Only sign the message if private key and password exist
if (this._signingKey && this._password) {
privateKey = await openpgp.readPrivateKey({
armoredKey: this._signingKey,
// Just return the message if there is no one to encrypt for
if (!validPublicKeys.length) {
this.push(message);
return callback();
}
// Only sign the message if private key and password exist
if (this._signingKey && this._password) {
privateKey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({
armoredKey: this._signingKey,
}),
passphrase: this._password,
});
}
const emailPartDelimiter = '\r\n\r\n';
const messageParts = message.toString().split(emailPartDelimiter);
/**
* In this loop original headers are split up into two parts,
* one for the email that is sent
* and one for the encrypted content
*/
const header = messageParts.shift() as string;
const emailHeaders: string[][] = [];
const contentHeaders: string[][] = [];
const linesInHeader = header.split('\r\n');
let previousHeader: string[] = [];
for (let i = 0; i < linesInHeader.length; i++) {
const line = linesInHeader[i];
/**
* If it is a multi-line header (current line starts with whitespace)
* or it's the first line in the iteration
* add the current line with previous header and move on
*/
if (/^\s/.test(line) || i === 0) {
previousHeader.push(line);
continue;
}
/**
* This is done to prevent the last header
* from being missed
*/
if (i === linesInHeader.length - 1) {
previousHeader.push(line);
}
/**
* We need to seperate the actual content headers
* so that we can add it as a header for the encrypted content
* So that the content will be displayed properly after decryption
*/
if (
/^(content-type|content-transfer-encoding):/i.test(previousHeader[0])
) {
contentHeaders.push(previousHeader);
} else {
emailHeaders.push(previousHeader);
}
previousHeader = [line];
}
// Generate a new boundary for the email content
const boundary = 'nm_' + randomBytes(14).toString('hex');
/**
* Concatenate everything into single strings
* and add pgp headers to the email headers
*/
const emailHeadersRaw =
emailHeaders.map((line) => line.join('\r\n')).join('\r\n') +
'\r\n' +
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' +
'\r\n' +
' boundary="' +
boundary +
'"' +
'\r\n' +
'Content-Description: OpenPGP encrypted message' +
'\r\n' +
'Content-Transfer-Encoding: 7bit';
const contentHeadersRaw = contentHeaders
.map((line) => line.join('\r\n'))
.join('\r\n');
const encryptedMessage = await openpgp.encrypt({
message: await openpgp.createMessage({
text:
contentHeadersRaw +
emailPartDelimiter +
messageParts.join(emailPartDelimiter),
}),
encryptionKeys: validPublicKeys,
signingKeys: privateKey,
});
await openpgp.decryptKey({ privateKey, passphrase: this._password });
const body =
'--' +
boundary +
'\r\n' +
'Content-Type: application/pgp-encrypted\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'Version: 1\r\n' +
'\r\n' +
'--' +
boundary +
'\r\n' +
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
encryptedMessage +
'\r\n--' +
boundary +
'--\r\n';
this.push(Buffer.from(emailHeadersRaw + emailPartDelimiter + body));
callback();
} catch (e) {
logger.error(
'Something went wrong while encrypting email message with OpenPGP. Sending email without encryption',
{
label: 'Notifications',
errorMessage: e.message,
}
);
this.push(message);
callback();
}
const emailPartDelimiter = '\r\n\r\n';
const messageParts = message.toString().split(emailPartDelimiter);
/**
* In this loop original headers are split up into two parts,
* one for the email that is sent
* and one for the encrypted content
*/
const header = messageParts.shift() as string;
const emailHeaders: string[][] = [];
const contentHeaders: string[][] = [];
const linesInHeader = header.split('\r\n');
let previousHeader: string[] = [];
for (let i = 0; i < linesInHeader.length; i++) {
const line = linesInHeader[i];
/**
* If it is a multi-line header (current line starts with whitespace)
* or it's the first line in the iteration
* add the current line with previous header and move on
*/
if (/^\s/.test(line) || i === 0) {
previousHeader.push(line);
continue;
}
/**
* This is done to prevent the last header
* from being missed
*/
if (i === linesInHeader.length - 1) {
previousHeader.push(line);
}
/**
* We need to seperate the actual content headers
* so that we can add it as a header for the encrypted content
* So that the content will be displayed properly after decryption
*/
if (
/^(content-type|content-transfer-encoding):/i.test(previousHeader[0])
) {
contentHeaders.push(previousHeader);
} else {
emailHeaders.push(previousHeader);
}
previousHeader = [line];
}
// Generate a new boundary for the email content
const boundary = 'nm_' + randomBytes(14).toString('hex');
/**
* Concatenate everything into single strings
* and add pgp headers to the email headers
*/
const emailHeadersRaw =
emailHeaders.map((line) => line.join('\r\n')).join('\r\n') +
'\r\n' +
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' +
'\r\n' +
' boundary="' +
boundary +
'"' +
'\r\n' +
'Content-Description: OpenPGP encrypted message' +
'\r\n' +
'Content-Transfer-Encoding: 7bit';
const contentHeadersRaw = contentHeaders
.map((line) => line.join('\r\n'))
.join('\r\n');
const encryptedMessage = await openpgp.encrypt({
message: await openpgp.createMessage({
text:
contentHeadersRaw +
emailPartDelimiter +
messageParts.join(emailPartDelimiter),
}),
encryptionKeys: validPublicKeys,
signingKeys: privateKey,
});
const body =
'--' +
boundary +
'\r\n' +
'Content-Type: application/pgp-encrypted\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'Version: 1\r\n' +
'\r\n' +
'--' +
boundary +
'\r\n' +
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
encryptedMessage +
'\r\n--' +
boundary +
'--\r\n';
this.push(Buffer.from(emailHeadersRaw + emailPartDelimiter + body));
callback();
};
}
export const openpgpEncrypt = (options: EncryptorOptions) => {
return function (mail: MailMessage, callback: () => unknown): void {
return function (mail: any, callback: () => unknown): void {
if (!options.encryptionKeys.length) {
setImmediate(callback);
}
mail.message.transform(
new PGPEncryptor({
signingKey: options.signingKey,
password: options.password,
encryptionKeys: options.encryptionKeys,
})
() =>
new PGPEncryptor({
signingKey: options.signingKey,
password: options.password,
encryptionKeys: options.encryptionKeys,
})
);
setImmediate(callback);
};

View File

@@ -1,17 +1,23 @@
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';
export interface NotificationPayload {
event?: string;
subject: string;
notifyAdmin: boolean;
notifyUser?: User;
media?: Media;
image?: string;
message?: string;
extra?: { name: string; value: string }[];
request?: MediaRequest;
issue?: Issue;
comment?: IssueComment;
}
export abstract class BaseAgent<T extends NotificationAgentConfig> {

View File

@@ -1,9 +1,13 @@
import axios from 'axios';
import { getRepository } from 'typeorm';
import { hasNotificationType, Notification } from '..';
import {
hasNotificationType,
Notification,
shouldSendAdminNotification,
} from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentDiscord,
@@ -91,7 +95,8 @@ interface DiscordWebhookPayload {
class DiscordAgent
extends BaseAgent<NotificationAgentDiscord>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentDiscord {
if (this.settings) {
return this.settings;
@@ -106,9 +111,9 @@ class DiscordAgent
type: Notification,
payload: NotificationPayload
): DiscordRichEmbed {
const settings = getSettings();
let color = EmbedColors.DARK_PURPLE;
const { applicationUrl } = getSettings().main;
let color = EmbedColors.DARK_PURPLE;
const fields: Field[] = [];
if (payload.request) {
@@ -117,56 +122,94 @@ class DiscordAgent
value: payload.request.requestedBy.displayName,
inline: true,
});
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
color = EmbedColors.ORANGE;
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
color = EmbedColors.PURPLE;
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
color = EmbedColors.GREEN;
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
color = EmbedColors.RED;
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
color = EmbedColors.RED;
status = 'Failed';
break;
}
if (status) {
fields.push({
name: 'Request Status',
value: status,
inline: true,
});
}
} else if (payload.comment) {
fields.push({
name: `Comment from ${payload.comment.user.displayName}`,
value: payload.comment.message,
inline: false,
});
} else if (payload.issue) {
fields.push(
{
name: 'Reported By',
value: payload.issue.createdBy.displayName,
inline: true,
},
{
name: 'Issue Type',
value: IssueTypeName[payload.issue.issueType],
inline: true,
},
{
name: 'Issue Status',
value:
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved',
inline: true,
}
);
switch (type) {
case Notification.ISSUE_CREATED:
case Notification.ISSUE_REOPENED:
color = EmbedColors.RED;
break;
case Notification.ISSUE_COMMENT:
color = EmbedColors.ORANGE;
break;
case Notification.ISSUE_RESOLVED:
color = EmbedColors.GREEN;
break;
}
}
switch (type) {
case Notification.MEDIA_PENDING:
color = EmbedColors.ORANGE;
fields.push({
name: 'Status',
value: 'Pending Approval',
inline: true,
});
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
color = EmbedColors.PURPLE;
fields.push({
name: 'Status',
value: 'Processing',
inline: true,
});
break;
case Notification.MEDIA_AVAILABLE:
color = EmbedColors.GREEN;
fields.push({
name: 'Status',
value: 'Available',
inline: true,
});
break;
case Notification.MEDIA_DECLINED:
color = EmbedColors.RED;
fields.push({
name: 'Status',
value: 'Declined',
inline: true,
});
break;
case Notification.MEDIA_FAILED:
color = EmbedColors.RED;
fields.push({
name: 'Status',
value: 'Failed',
inline: true,
});
break;
for (const extra of payload.extra ?? []) {
fields.push({
name: extra.name,
value: extra.value,
inline: true,
});
}
const url =
settings.main.applicationUrl && payload.media
? `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined;
const url = applicationUrl
? payload.issue
? `${applicationUrl}/issues/${payload.issue.id}`
: payload.media
? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined
: undefined;
return {
title: payload.subject,
@@ -174,18 +217,12 @@ class DiscordAgent
description: payload.message,
color,
timestamp: new Date().toISOString(),
author: {
name: settings.main.applicationTitle,
url: settings.main.applicationUrl,
},
fields: [
...fields,
// If we have extra data, map it to fields for discord notifications
...(payload.extra ?? []).map((extra) => ({
name: extra.name,
value: extra.value,
})),
],
author: payload.event
? {
name: payload.event,
}
: undefined,
fields,
thumbnail: {
url: payload.image,
},
@@ -218,54 +255,55 @@ class DiscordAgent
subject: payload.subject,
});
let content = undefined;
const userMentions: string[] = [];
try {
if (payload.notifyUser) {
// Mention user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
payload.notifyUser.settings?.discordId
) {
content = `<@${payload.notifyUser.settings.discordId}>`;
if (settings.options.enableMentions) {
if (payload.notifyUser) {
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
payload.notifyUser.settings.discordId
) {
userMentions.push(`<@${payload.notifyUser.settings.discordId}>`);
}
}
} else {
// Mention all users with the Manage Requests permission
const userRepository = getRepository(User);
const users = await userRepository.find();
content = users
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
user.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
user.settings?.discordId &&
// Check if it's the user's own auto-approved request
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !== payload.request?.requestedBy.id)
)
.map((user) => `<@${user.settings?.discordId}>`)
.join(' ');
if (payload.notifyAdmin) {
const userRepository = getRepository(User);
const users = await userRepository.find();
userMentions.push(
...users
.filter(
(user) =>
user.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
user.settings.discordId &&
shouldSendAdminNotification(type, user, payload)
)
.map((user) => `<@${user.settings?.discordId}>`)
);
}
}
await axios.post(settings.options.webhookUrl, {
username: settings.options.botUsername,
username: settings.options.botUsername
? settings.options.botUsername
: getSettings().main.applicationTitle,
avatar_url: settings.options.botAvatarUrl,
embeds: [this.buildEmbed(type, payload)],
content,
content: userMentions.join(' '),
} as DiscordWebhookPayload);
return true;
} catch (e) {
logger.error('Error sending Discord notification', {
label: 'Notifications',
mentions: content,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,

View File

@@ -1,12 +1,12 @@
import { EmailOptions } from 'email-templates';
import path from 'path';
import { getRepository } from 'typeorm';
import { Notification } from '..';
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 { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentEmail,
@@ -16,7 +16,8 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentEmail {
if (this.settings) {
return this.settings;
@@ -45,7 +46,8 @@ class EmailAgent
private buildMessage(
type: Notification,
payload: NotificationPayload,
toEmail: string
recipientEmail: string,
recipientName?: string
): EmailOptions | undefined {
const { applicationUrl, applicationTitle } = getSettings().main;
@@ -53,69 +55,59 @@ class EmailAgent
return {
template: path.join(__dirname, '../../../templates/email/test-email'),
message: {
to: toEmail,
to: recipientEmail,
},
locals: {
body: payload.message,
applicationUrl,
applicationTitle,
recipientName,
recipientEmail,
},
};
}
if (payload.media) {
let requestType = '';
const mediaType = payload.media
? payload.media.mediaType === MediaType.MOVIE
? 'movie'
: 'series'
: undefined;
const is4k = payload.request?.is4k;
if (payload.request) {
let body = '';
switch (type) {
case Notification.MEDIA_PENDING:
requestType = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
body = `A user has requested a new ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
}!`;
body = `A new request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}is pending approval:`;
break;
case Notification.MEDIA_APPROVED:
requestType = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
body = `Your request for the following ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
} has been approved:`;
body = `Your request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}has been approved:`;
break;
case Notification.MEDIA_AUTO_APPROVED:
requestType = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
body = `A new request for the following ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
} has been automatically approved:`;
body = `A new request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}has been automatically approved:`;
break;
case Notification.MEDIA_AVAILABLE:
requestType = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
body = `The following ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
} you requested is now available!`;
body = `Your request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}is now available:`;
break;
case Notification.MEDIA_DECLINED:
requestType = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
body = `Your request for the following ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
} was declined:`;
body = `Your request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}was declined:`;
break;
case Notification.MEDIA_FAILED:
requestType = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
body = `A new request for the following ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
} could not be added to ${
payload.media?.mediaType === MediaType.TV ? 'Sonarr' : 'Radarr'
body = `A request for the following ${mediaType} ${
is4k ? 'in 4K ' : ''
}failed to be added to ${
payload.media?.mediaType === MediaType.MOVIE ? 'Radarr' : 'Sonarr'
}:`;
break;
}
@@ -126,22 +118,69 @@ class EmailAgent
'../../../templates/email/media-request'
),
message: {
to: toEmail,
to: recipientEmail,
},
locals: {
requestType,
event: payload.event,
body,
mediaName: payload.subject,
mediaPlot: payload.message,
mediaExtra: payload.extra ?? [],
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.request?.requestedBy.displayName,
requestedBy: payload.request.requestedBy.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
applicationTitle,
recipientName,
recipientEmail,
},
};
} else if (payload.issue) {
const issueType =
payload.issue && payload.issue.issueType !== IssueType.OTHER
? `${IssueTypeName[payload.issue.issueType].toLowerCase()} issue`
: 'issue';
let body = '';
switch (type) {
case Notification.ISSUE_CREATED:
body = `A new ${issueType} has been reported by ${payload.issue.createdBy.displayName} for the ${mediaType} ${payload.subject}:`;
break;
case Notification.ISSUE_COMMENT:
body = `${payload.comment?.user.displayName} commented on the ${issueType} for the ${mediaType} ${payload.subject}:`;
break;
case Notification.ISSUE_RESOLVED:
body = `The ${issueType} for the ${mediaType} ${payload.subject} was marked as resolved by ${payload.issue.modifiedBy?.displayName}!`;
break;
case Notification.ISSUE_REOPENED:
body = `The ${issueType} for the ${mediaType} ${payload.subject} was reopened by ${payload.issue.modifiedBy?.displayName}.`;
break;
}
return {
template: path.join(__dirname, '../../../templates/email/media-issue'),
message: {
to: recipientEmail,
},
locals: {
event: payload.event,
body,
issueDescription: payload.message,
issueComment: payload.comment?.message,
mediaName: payload.subject,
extra: payload.extra ?? [],
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
actionUrl: applicationUrl
? `${applicationUrl}/issues/${payload.issue.id}`
: undefined,
applicationUrl,
applicationTitle,
recipientName,
recipientEmail,
},
};
}
@@ -154,7 +193,6 @@ class EmailAgent
payload: NotificationPayload
): Promise<boolean> {
if (payload.notifyUser) {
// Send notification to the user who submitted the request
if (
!payload.notifyUser.settings ||
// Check if user has email notifications enabled and fallback to true if undefined
@@ -178,7 +216,12 @@ class EmailAgent
payload.notifyUser.settings?.pgpKey
);
await email.send(
this.buildMessage(type, payload, payload.notifyUser.email)
this.buildMessage(
type,
payload,
payload.notifyUser.email,
payload.notifyUser.displayName
)
);
} catch (e) {
logger.error('Error sending email notification', {
@@ -192,8 +235,9 @@ class EmailAgent
return false;
}
}
} else {
// Send notifications to all users with the Manage Requests permission
}
if (payload.notifyAdmin) {
const userRepository = getRepository(User);
const users = await userRepository.find();
@@ -201,7 +245,6 @@ class EmailAgent
users
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
(!user.settings ||
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
@@ -210,9 +253,7 @@ class EmailAgent
type
) ??
true)) &&
// Check if it's the user's own auto-approved request
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !== payload.request?.requestedBy.id)
shouldSendAdminNotification(type, user, payload)
)
.map(async (user) => {
logger.debug('Sending email notification', {
@@ -227,7 +268,9 @@ class EmailAgent
this.getSettings(),
user.settings?.pgpKey
);
await email.send(this.buildMessage(type, payload, user.email));
await email.send(
this.buildMessage(type, payload, user.email, user.displayName)
);
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',

View File

@@ -0,0 +1,148 @@
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';
interface GotifyPayload {
title: string;
message: string;
priority: number;
extras: any;
}
class GotifyAgent
extends BaseAgent<NotificationAgentGotify>
implements NotificationAgent
{
protected getSettings(): NotificationAgentGotify {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.gotify;
}
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.url && settings.options.token) {
return true;
}
return false;
}
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): GotifyPayload {
const { applicationUrl, applicationTitle } = getSettings().main;
let priority = 0;
const title = payload.event
? `${payload.event} - ${payload.subject}`
: payload.subject;
let message = payload.message ?? '';
if (payload.request) {
message += `\n\nRequested By: ${payload.request.requestedBy.displayName}`;
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
break;
}
if (status) {
message += `\nRequest Status: ${status}`;
}
} else if (payload.comment) {
message += `\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message}`;
} else if (payload.issue) {
message += `\n\nReported By: ${payload.issue.createdBy.displayName}`;
message += `\nIssue Type: ${IssueTypeName[payload.issue.issueType]}`;
message += `\nIssue Status: ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`;
if (type == Notification.ISSUE_CREATED) {
priority = 1;
}
}
for (const extra of payload.extra ?? []) {
message += `\n\n**${extra.name}**\n${extra.value}`;
}
if (applicationUrl && payload.media) {
const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\n\nOpen in ${applicationTitle}(${actionUrl})`;
}
return {
extras: {
'client::display': {
contentType: 'text/markdown',
},
},
title,
message,
priority,
};
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Gotify notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
const notificationPayload = this.getNotificationPayload(type, payload);
await axios.post(endpoint, notificationPayload);
return true;
} catch (e) {
logger.error('Error sending Gotify notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
}
export default GotifyAgent;

View File

@@ -1,5 +1,6 @@
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';
@@ -7,7 +8,8 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class LunaSeaAgent
extends BaseAgent<NotificationAgentLunaSea>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentLunaSea {
if (this.settings) {
return this.settings;
@@ -21,17 +23,17 @@ class LunaSeaAgent
private buildPayload(type: Notification, payload: NotificationPayload) {
return {
notification_type: Notification[type],
event: payload.event,
subject: payload.subject,
message: payload.message,
image: payload.image ?? null,
email: payload.notifyUser?.email,
username: payload.notifyUser?.username,
username: payload.notifyUser?.displayName,
avatar: payload.notifyUser?.avatar,
media: payload.media
? {
media_type: payload.media.mediaType,
tmdbId: payload.media.tmdbId,
imdbId: payload.media.imdbId,
tvdbId: payload.media.tvdbId,
status: MediaStatus[payload.media.status],
status4k: MediaStatus[payload.media.status4k],
@@ -46,6 +48,24 @@ class LunaSeaAgent
requestedBy_avatar: payload.request.requestedBy.avatar,
}
: null,
issue: payload.issue
? {
issue_id: payload.issue.id,
issue_type: IssueType[payload.issue.issueType],
issue_status: IssueStatus[payload.issue.status],
createdBy_email: payload.issue.createdBy.email,
createdBy_username: payload.issue.createdBy.displayName,
createdBy_avatar: payload.issue.createdBy.avatar,
}
: null,
comment: payload.comment
? {
comment_message: payload.comment.message,
commentedBy_email: payload.comment.user.email,
commentedBy_username: payload.comment.user.displayName,
commentedBy_avatar: payload.comment.user.avatar,
}
: null,
};
}

View File

@@ -1,18 +1,31 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
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, NotificationAgentPushbullet } from '../../settings';
import {
getSettings,
NotificationAgentKey,
NotificationAgentPushbullet,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushbulletPayload {
type: string;
title: string;
body: string;
channel_tag?: string;
}
class PushbulletAgent
extends BaseAgent<NotificationAgentPushbullet>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentPushbullet {
if (this.settings) {
return this.settings;
@@ -24,109 +37,62 @@ class PushbulletAgent
}
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.accessToken) {
return true;
}
return false;
return true;
}
private constructMessageDetails(
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): {
title: string;
body: string;
} {
let messageTitle = '';
let message = '';
): PushbulletPayload {
const title = payload.event
? `${payload.event} - ${payload.subject}`
: payload.subject;
let body = payload.message ?? '';
const title = payload.subject;
const plot = payload.message;
const username = payload.request?.requestedBy.displayName;
if (payload.request) {
body += `\n\nRequested By: ${payload.request.requestedBy.displayName}`;
switch (type) {
case Notification.MEDIA_PENDING:
messageTitle = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Pending Approval`;
break;
case Notification.MEDIA_APPROVED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Processing`;
break;
case Notification.MEDIA_AUTO_APPROVED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Processing`;
break;
case Notification.MEDIA_AVAILABLE:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Available`;
break;
case Notification.MEDIA_DECLINED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Declined`;
break;
case Notification.MEDIA_FAILED:
messageTitle = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Failed`;
break;
case Notification.TEST_NOTIFICATION:
messageTitle = 'Test Notification';
message += `${plot}`;
break;
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
break;
}
if (status) {
body += `\nRequest Status: ${status}`;
}
} else if (payload.comment) {
body += `\n\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message}`;
} else if (payload.issue) {
body += `\n\nReported By: ${payload.issue.createdBy.displayName}`;
body += `\nIssue Type: ${IssueTypeName[payload.issue.issueType]}`;
body += `\nIssue Status: ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`;
}
for (const extra of payload.extra ?? []) {
message += `\n${extra.name}: ${extra.value}`;
body += `\n${extra.name}: ${extra.value}`;
}
return {
title: messageTitle,
body: message,
type: 'note',
title,
body,
};
}
@@ -135,46 +101,133 @@ class PushbulletAgent
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
const endpoint = 'https://api.pushbullet.com/v2/pushes';
const notificationPayload = this.getNotificationPayload(type, payload);
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Pushbullet notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const { title, body } = this.constructMessageDetails(type, payload);
await axios.post(
'https://api.pushbullet.com/v2/pushes',
{
type: 'note',
title: title,
body: body,
} as PushbulletPayload,
{
headers: {
'Access-Token': settings.options.accessToken,
},
}
);
return true;
} catch (e) {
logger.error('Error sending Pushbullet notification', {
// Send system notification
if (
hasNotificationType(type, settings.types ?? 0) &&
settings.enabled &&
settings.options.accessToken
) {
logger.debug('Sending Pushbullet notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
try {
await axios.post(
endpoint,
{ ...notificationPayload, channel_tag: settings.options.channelTag },
{
headers: {
'Access-Token': settings.options.accessToken,
},
}
);
} catch (e) {
logger.error('Error sending Pushbullet notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
if (payload.notifyUser) {
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.PUSHBULLET,
type
) &&
payload.notifyUser.settings?.pushbulletAccessToken &&
payload.notifyUser.settings.pushbulletAccessToken !==
settings.options.accessToken
) {
logger.debug('Sending Pushbullet notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await axios.post(endpoint, notificationPayload, {
headers: {
'Access-Token': payload.notifyUser.settings.pushbulletAccessToken,
},
});
} catch (e) {
logger.error('Error sending Pushbullet notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
}
if (payload.notifyAdmin) {
const userRepository = getRepository(User);
const users = await userRepository.find();
await Promise.all(
users
.filter(
(user) =>
user.settings?.hasNotificationType(
NotificationAgentKey.PUSHBULLET,
type
) && shouldSendAdminNotification(type, user, payload)
)
.map(async (user) => {
if (
user.settings?.pushbulletAccessToken &&
(settings.options.channelTag ||
user.settings.pushbulletAccessToken !==
settings.options.accessToken)
) {
logger.debug('Sending Pushbullet notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await axios.post(endpoint, notificationPayload, {
headers: {
'Access-Token': user.settings.pushbulletAccessToken,
},
});
} catch (e) {
logger.error('Error sending Pushbullet notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
})
);
}
return true;
}
}

View File

@@ -1,8 +1,18 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
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, NotificationAgentPushover } from '../../settings';
import {
getSettings,
NotificationAgentKey,
NotificationAgentPushover,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushoverPayload {
@@ -18,7 +28,8 @@ interface PushoverPayload {
class PushoverAgent
extends BaseAgent<NotificationAgentPushover>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentPushover {
if (this.settings) {
return this.settings;
@@ -30,130 +41,89 @@ class PushoverAgent
}
public shouldSend(): boolean {
const settings = this.getSettings();
if (
settings.enabled &&
settings.options.accessToken &&
settings.options.userToken
) {
return true;
}
return false;
return true;
}
private constructMessageDetails(
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): {
title: string;
message: string;
url: string | undefined;
url_title: string | undefined;
priority: number;
} {
const settings = getSettings();
let messageTitle = '';
let message = '';
let url: string | undefined;
let url_title: string | undefined;
): Partial<PushoverPayload> {
const { applicationUrl, applicationTitle } = getSettings().main;
const title = payload.event ?? payload.subject;
let message = payload.event ? `<b>${payload.subject}</b>` : '';
let priority = 0;
const title = payload.subject;
const plot = payload.message;
const username = payload.request?.requestedBy.displayName;
if (payload.message) {
message += `<small>${message ? '\n' : ''}${payload.message}</small>`;
}
switch (type) {
case Notification.MEDIA_PENDING:
messageTitle = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nPending Approval</small>`;
break;
case Notification.MEDIA_APPROVED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nProcessing</small>`;
break;
case Notification.MEDIA_AUTO_APPROVED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nProcessing</small>`;
break;
case Notification.MEDIA_AVAILABLE:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nAvailable</small>`;
break;
case Notification.MEDIA_DECLINED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nDeclined</small>`;
if (payload.request) {
message += `<small>\n\n<b>Requested By:</b> ${payload.request.requestedBy.displayName}</small>`;
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
priority = 1;
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
priority = 1;
break;
}
if (status) {
message += `<small>\n<b>Request Status:</b> ${status}</small>`;
}
} else if (payload.comment) {
message += `<small>\n\n<b>Comment from ${payload.comment.user.displayName}:</b> ${payload.comment.message}</small>`;
} else if (payload.issue) {
message += `<small>\n\n<b>Reported By:</b> ${payload.issue.createdBy.displayName}</small>`;
message += `<small>\n<b>Issue Type:</b> ${
IssueTypeName[payload.issue.issueType]
}</small>`;
message += `<small>\n<b>Issue Status:</b> ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}</small>`;
if (type === Notification.ISSUE_CREATED) {
priority = 1;
break;
case Notification.MEDIA_FAILED:
messageTitle = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nFailed</small>`;
priority = 1;
break;
case Notification.TEST_NOTIFICATION:
messageTitle = 'Test Notification';
message += `<small>${plot}</small>`;
break;
}
}
for (const extra of payload.extra ?? []) {
message += `<small>\n\n<b>${extra.name}</b>\n${extra.value}</small>`;
message += `<small>\n<b>${extra.name}:</b> ${extra.value}</small>`;
}
if (settings.main.applicationUrl && payload.media) {
url = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
url_title = `Open in ${settings.main.applicationTitle}`;
}
const url = applicationUrl
? payload.issue
? `${applicationUrl}/issues/${payload.issue.id}`
: payload.media
? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined
: undefined;
const url_title = url
? `View ${payload.issue ? 'Issue' : 'Media'} in ${applicationTitle}`
: undefined;
return {
title: messageTitle,
title,
message,
url,
url_title,
priority,
html: 1,
};
}
@@ -162,50 +132,134 @@ class PushoverAgent
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
const endpoint = 'https://api.pushover.net/1/messages.json';
const notificationPayload = this.getNotificationPayload(type, payload);
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Pushover notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const endpoint = 'https://api.pushover.net/1/messages.json';
const {
title,
message,
url,
url_title,
priority,
} = this.constructMessageDetails(type, payload);
await axios.post(endpoint, {
token: settings.options.accessToken,
user: settings.options.userToken,
title: title,
message: message,
url: url,
url_title: url_title,
priority: priority,
html: 1,
} as PushoverPayload);
return true;
} catch (e) {
logger.error('Error sending Pushover notification', {
// Send system notification
if (
hasNotificationType(type, settings.types ?? 0) &&
settings.enabled &&
settings.options.accessToken &&
settings.options.userToken
) {
logger.debug('Sending Pushover notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
try {
await axios.post(endpoint, {
...notificationPayload,
token: settings.options.accessToken,
user: settings.options.userToken,
} as PushoverPayload);
} catch (e) {
logger.error('Error sending Pushover notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
if (payload.notifyUser) {
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.PUSHOVER,
type
) &&
payload.notifyUser.settings.pushoverApplicationToken &&
payload.notifyUser.settings.pushoverUserKey &&
(payload.notifyUser.settings.pushoverApplicationToken !==
settings.options.accessToken ||
payload.notifyUser.settings.pushoverUserKey !==
settings.options.userToken)
) {
logger.debug('Sending Pushover notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await axios.post(endpoint, {
...notificationPayload,
token: payload.notifyUser.settings.pushoverApplicationToken,
user: payload.notifyUser.settings.pushoverUserKey,
} as PushoverPayload);
} catch (e) {
logger.error('Error sending Pushover notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
}
if (payload.notifyAdmin) {
const userRepository = getRepository(User);
const users = await userRepository.find();
await Promise.all(
users
.filter(
(user) =>
user.settings?.hasNotificationType(
NotificationAgentKey.PUSHOVER,
type
) && shouldSendAdminNotification(type, user, payload)
)
.map(async (user) => {
if (
user.settings?.pushoverApplicationToken &&
user.settings?.pushoverUserKey &&
user.settings.pushoverApplicationToken !==
settings.options.accessToken &&
user.settings.pushoverUserKey !== settings.options.userToken
) {
logger.debug('Sending Pushover notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await axios.post(endpoint, {
...notificationPayload,
token: user.settings.pushoverApplicationToken,
user: user.settings.pushoverUserKey,
} as PushoverPayload);
} catch (e) {
logger.error('Error sending Pushover notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
})
);
}
return true;
}
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import logger from '../../../logger';
import { getSettings, NotificationAgentSlack } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
@@ -19,9 +19,10 @@ interface TextItem {
interface Element {
type: 'button';
text?: TextItem;
value: string;
url: string;
action_id: 'button-action';
action_id: string;
url?: string;
value?: string;
style?: 'primary' | 'danger';
}
interface EmbedBlock {
@@ -34,10 +35,11 @@ interface EmbedBlock {
image_url: string;
alt_text: string;
};
elements?: Element[];
elements?: (Element | TextItem)[];
}
interface SlackBlockEmbed {
text: string;
blocks: EmbedBlock[];
}
@@ -59,9 +61,7 @@ class SlackAgent
type: Notification,
payload: NotificationPayload
): SlackBlockEmbed {
const settings = getSettings();
let header = '';
let actionUrl: string | undefined;
const { applicationUrl, applicationTitle } = getSettings().main;
const fields: EmbedField[] = [];
@@ -70,66 +70,55 @@ class SlackAgent
type: 'mrkdwn',
text: `*Requested By*\n${payload.request.requestedBy.displayName}`,
});
}
switch (type) {
case Notification.MEDIA_PENDING:
header = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
break;
}
if (status) {
fields.push({
type: 'mrkdwn',
text: '*Status*\nPending Approval',
text: `*Request Status*\n${status}`,
});
break;
case Notification.MEDIA_APPROVED:
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
fields.push({
}
} else if (payload.comment) {
fields.push({
type: 'mrkdwn',
text: `*Comment from ${payload.comment.user.displayName}*\n${payload.comment.message}`,
});
} else if (payload.issue) {
fields.push(
{
type: 'mrkdwn',
text: '*Status*\nProcessing',
});
break;
case Notification.MEDIA_AUTO_APPROVED:
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
fields.push({
text: `*Reported By*\n${payload.issue.createdBy.displayName}`,
},
{
type: 'mrkdwn',
text: '*Status*\nProcessing',
});
break;
case Notification.MEDIA_AVAILABLE:
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
fields.push({
text: `*Issue Type*\n${IssueTypeName[payload.issue.issueType]}`,
},
{
type: 'mrkdwn',
text: '*Status*\nAvailable',
});
break;
case Notification.MEDIA_DECLINED:
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nDeclined',
});
break;
case Notification.MEDIA_FAILED:
header = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nFailed',
});
break;
case Notification.TEST_NOTIFICATION:
header = 'Test Notification';
break;
text: `*Issue Status*\n${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`,
}
);
}
for (const extra of payload.extra ?? []) {
@@ -139,30 +128,28 @@ class SlackAgent
});
}
if (settings.main.applicationUrl && payload.media) {
actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`;
}
const blocks: EmbedBlock[] = [];
const blocks: EmbedBlock[] = [
{
type: 'header',
text: {
type: 'plain_text',
text: header,
},
},
];
if (type !== Notification.TEST_NOTIFICATION) {
if (payload.event) {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: `*${payload.subject}*`,
},
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `*${payload.event}*`,
},
],
});
}
blocks.push({
type: 'header',
text: {
type: 'plain_text',
text: payload.subject,
},
});
if (payload.message) {
blocks.push({
type: 'section',
@@ -183,30 +170,31 @@ class SlackAgent
if (fields.length > 0) {
blocks.push({
type: 'section',
fields: [
...fields,
...(payload.extra ?? []).map(
(extra): EmbedField => ({
type: 'mrkdwn',
text: `*${extra.name}*\n${extra.value}`,
})
),
],
fields,
});
}
if (actionUrl) {
const url = applicationUrl
? payload.issue
? `${applicationUrl}/issues/${payload.issue.id}`
: payload.media
? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined
: undefined;
if (url) {
blocks.push({
type: 'actions',
elements: [
{
action_id: 'button-action',
action_id: 'open-in-overseerr',
type: 'button',
url: actionUrl,
value: 'open_jellyseerr',
url,
text: {
type: 'plain_text',
text: `Open in ${settings.main.applicationTitle}`,
text: `View ${
payload.issue ? 'Issue' : 'Media'
} in ${applicationTitle}`,
},
},
],
@@ -214,6 +202,7 @@ class SlackAgent
}
return {
text: payload.event ?? payload.subject,
blocks,
};
}

View File

@@ -1,10 +1,13 @@
import axios from 'axios';
import { getRepository } from 'typeorm';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import {
hasNotificationType,
Notification,
shouldSendAdminNotification,
} from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentKey,
@@ -29,7 +32,8 @@ interface TelegramPhotoPayload {
class TelegramAgent
extends BaseAgent<NotificationAgentTelegram>
implements NotificationAgent {
implements NotificationAgent
{
private baseUrl = 'https://api.telegram.org/';
protected getSettings(): NotificationAgentTelegram {
@@ -45,11 +49,7 @@ class TelegramAgent
public shouldSend(): boolean {
const settings = this.getSettings();
if (
settings.enabled &&
settings.options.botAPI &&
settings.options.chatId
) {
if (settings.enabled && settings.options.botAPI) {
return true;
}
@@ -60,118 +60,91 @@ class TelegramAgent
return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : '';
}
private buildMessage(
private getNotificationPayload(
type: Notification,
payload: NotificationPayload,
chatId: string,
sendSilently: boolean
): TelegramMessagePayload | TelegramPhotoPayload {
const settings = getSettings();
let message = '';
const title = this.escapeText(payload.subject);
const plot = this.escapeText(payload.message);
const user = this.escapeText(payload.request?.requestedBy.displayName);
const applicationTitle = this.escapeText(settings.main.applicationTitle);
payload: NotificationPayload
): Partial<TelegramMessagePayload | TelegramPhotoPayload> {
const { applicationUrl, applicationTitle } = getSettings().main;
/* eslint-disable no-useless-escape */
switch (type) {
case Notification.MEDIA_PENDING:
message += `\*New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nPending Approval`;
break;
case Notification.MEDIA_APPROVED:
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nProcessing`;
break;
case Notification.MEDIA_AUTO_APPROVED:
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nProcessing`;
break;
case Notification.MEDIA_AVAILABLE:
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nAvailable`;
break;
case Notification.MEDIA_DECLINED:
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nDeclined`;
break;
case Notification.MEDIA_FAILED:
message += `\*Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nFailed`;
break;
case Notification.TEST_NOTIFICATION:
message += `\*Test Notification\*`;
message += `\n\n${plot}`;
break;
let message = `\*${this.escapeText(
payload.event ? `${payload.event} - ${payload.subject}` : payload.subject
)}\*`;
if (payload.message) {
message += `\n${this.escapeText(payload.message)}`;
}
if (payload.request) {
message += `\n\n\*Requested By:\* ${this.escapeText(
payload.request?.requestedBy.displayName
)}`;
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
break;
}
if (status) {
message += `\n\*Request Status:\* ${status}`;
}
} else if (payload.comment) {
message += `\n\n\*Comment from ${this.escapeText(
payload.comment.user.displayName
)}:\* ${this.escapeText(payload.comment.message)}`;
} else if (payload.issue) {
message += `\n\n\*Reported By:\* ${this.escapeText(
payload.issue.createdBy.displayName
)}`;
message += `\n\*Issue Type:\* ${IssueTypeName[payload.issue.issueType]}`;
message += `\n\*Issue Status:\* ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`;
}
for (const extra of payload.extra ?? []) {
message += `\n\n\*${extra.name}\*\n${extra.value}`;
message += `\n\*${extra.name}:\* ${extra.value}`;
}
if (settings.main.applicationUrl && payload.media) {
const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\n\n\[Open in ${applicationTitle}\]\(${actionUrl}\)`;
const url = applicationUrl
? payload.issue
? `${applicationUrl}/issues/${payload.issue.id}`
: payload.media
? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined
: undefined;
if (url) {
message += `\n\n\[View ${
payload.issue ? 'Issue' : 'Media'
} in ${this.escapeText(applicationTitle)}\]\(${url}\)`;
}
/* eslint-enable */
return payload.image
? ({
? {
photo: payload.image,
caption: message,
parse_mode: 'MarkdownV2',
chat_id: chatId,
disable_notification: !!sendSilently,
} as TelegramPhotoPayload)
: ({
}
: {
text: message,
parse_mode: 'MarkdownV2',
chat_id: chatId,
disable_notification: !!sendSilently,
} as TelegramMessagePayload);
};
}
public async send(
@@ -179,13 +152,16 @@ class TelegramAgent
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${
payload.image ? 'sendPhoto' : 'sendMessage'
}`;
const notificationPayload = this.getNotificationPayload(type, payload);
// Send system notification
if (hasNotificationType(type, settings.types ?? 0)) {
if (
hasNotificationType(type, settings.types ?? 0) &&
settings.options.chatId
) {
logger.debug('Sending Telegram notification', {
label: 'Notifications',
type: Notification[type],
@@ -193,15 +169,11 @@ class TelegramAgent
});
try {
await axios.post(
endpoint,
this.buildMessage(
type,
payload,
settings.options.chatId,
settings.options.sendSilently
)
);
await axios.post(endpoint, {
...notificationPayload,
chat_id: settings.options.chatId,
disable_notification: !!settings.options.sendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
@@ -216,14 +188,13 @@ class TelegramAgent
}
if (payload.notifyUser) {
// Send notification to the user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
payload.notifyUser.settings?.telegramChatId &&
payload.notifyUser.settings?.telegramChatId !== settings.options.chatId
payload.notifyUser.settings.telegramChatId !== settings.options.chatId
) {
logger.debug('Sending Telegram notification', {
label: 'Notifications',
@@ -233,15 +204,12 @@ class TelegramAgent
});
try {
await axios.post(
endpoint,
this.buildMessage(
type,
payload,
payload.notifyUser.settings.telegramChatId,
!!payload.notifyUser.settings.telegramSendSilently
)
);
await axios.post(endpoint, {
...notificationPayload,
chat_id: payload.notifyUser.settings.telegramChatId,
disable_notification:
!!payload.notifyUser.settings.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
@@ -255,8 +223,9 @@ class TelegramAgent
return false;
}
}
} else {
// Send notifications to all users with the Manage Requests permission
}
if (payload.notifyAdmin) {
const userRepository = getRepository(User);
const users = await userRepository.find();
@@ -264,14 +233,10 @@ class TelegramAgent
users
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
user.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
// Check if it's the user's own auto-approved request
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !== payload.request?.requestedBy.id)
) && shouldSendAdminNotification(type, user, payload)
)
.map(async (user) => {
if (
@@ -286,15 +251,11 @@ class TelegramAgent
});
try {
await axios.post(
endpoint,
this.buildMessage(
type,
payload,
user.settings.telegramChatId,
!!user.settings?.telegramSendSilently
)
);
await axios.post(endpoint, {
...notificationPayload,
chat_id: user.settings.telegramChatId,
disable_notification: !!user.settings?.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',

View File

@@ -1,6 +1,7 @@
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';
@@ -13,6 +14,7 @@ type KeyMapFunction = (
const KeyMap: Record<string, string | KeyMapFunction> = {
notification_type: (_payload, type) => Notification[type],
event: 'event',
subject: 'subject',
message: 'message',
image: 'image',
@@ -22,13 +24,12 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
notifyuser_settings_discordId: 'notifyUser.settings.discordId',
notifyuser_settings_telegramChatId: 'notifyUser.settings.telegramChatId',
media_tmdbid: 'media.tmdbId',
media_imdbid: 'media.imdbId',
media_tvdbid: 'media.tvdbId',
media_type: 'media.mediaType',
media_status: (payload) =>
payload.media?.status ? MediaStatus[payload.media?.status] : '',
payload.media ? MediaStatus[payload.media.status] : '',
media_status4k: (payload) =>
payload.media?.status ? MediaStatus[payload.media?.status4k] : '',
payload.media ? MediaStatus[payload.media.status4k] : '',
request_id: 'request.id',
requestedBy_username: 'request.requestedBy.displayName',
requestedBy_email: 'request.requestedBy.email',
@@ -36,11 +37,28 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
requestedBy_settings_discordId: 'request.requestedBy.settings.discordId',
requestedBy_settings_telegramChatId:
'request.requestedBy.settings.telegramChatId',
issue_id: 'issue.id',
issue_type: (payload) =>
payload.issue ? IssueType[payload.issue.issueType] : '',
issue_status: (payload) =>
payload.issue ? IssueStatus[payload.issue.status] : '',
reportedBy_username: 'issue.createdBy.displayName',
reportedBy_email: 'issue.createdBy.email',
reportedBy_avatar: 'issue.createdBy.avatar',
reportedBy_settings_discordId: 'issue.createdBy.settings.discordId',
reportedBy_settings_telegramChatId: 'issue.createdBy.settings.telegramChatId',
comment_message: 'comment.message',
commentedBy_username: 'comment.user.displayName',
commentedBy_email: 'comment.user.email',
commentedBy_avatar: 'comment.user.avatar',
commentedBy_settings_discordId: 'comment.user.settings.discordId',
commentedBy_settings_telegramChatId: 'comment.user.settings.telegramChatId',
};
class WebhookAgent
extends BaseAgent<NotificationAgentWebhook>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentWebhook {
if (this.settings) {
return this.settings;
@@ -77,6 +95,22 @@ class WebhookAgent
}
delete finalPayload[key];
key = 'request';
} else if (key === '{{issue}}') {
if (payload.issue) {
finalPayload.issue = finalPayload[key];
} else {
finalPayload.issue = null;
}
delete finalPayload[key];
key = 'issue';
} else if (key === '{{comment}}') {
if (payload.comment) {
finalPayload.comment = finalPayload[key];
} else {
finalPayload.comment = null;
}
delete finalPayload[key];
key = 'comment';
}
if (typeof finalPayload[key] === 'string') {

View File

@@ -1,11 +1,11 @@
import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { Notification } from '..';
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 { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentConfig,
@@ -15,18 +15,18 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushNotificationPayload {
notificationType: string;
mediaType?: 'movie' | 'tv';
tmdbId?: number;
subject: string;
message?: string;
image?: string;
actionUrl?: string;
actionUrlTitle?: string;
requestId?: number;
}
class WebPushAgent
extends BaseAgent<NotificationAgentConfig>
implements NotificationAgent {
implements NotificationAgent
{
protected getSettings(): NotificationAgentConfig {
if (this.settings) {
return this.settings;
@@ -41,97 +41,92 @@ class WebPushAgent
type: Notification,
payload: NotificationPayload
): PushNotificationPayload {
const mediaType = payload.media
? payload.media.mediaType === MediaType.MOVIE
? 'movie'
: 'series'
: undefined;
const is4k = payload.request?.is4k;
const issueType = payload.issue
? payload.issue.issueType !== IssueType.OTHER
? `${IssueTypeName[payload.issue.issueType].toLowerCase()} issue`
: 'issue'
: undefined;
let message: string | undefined;
switch (type) {
case Notification.NONE:
case Notification.TEST_NOTIFICATION:
message = payload.message;
break;
case Notification.MEDIA_APPROVED:
message = `Your ${
is4k ? '4K ' : ''
}${mediaType} request has been approved.`;
break;
case Notification.MEDIA_AUTO_APPROVED:
message = `Automatically approved a new ${
is4k ? '4K ' : ''
}${mediaType} request from ${
payload.request?.requestedBy.displayName
}.`;
break;
case Notification.MEDIA_AVAILABLE:
message = `Your ${
is4k ? '4K ' : ''
}${mediaType} request is now available!`;
break;
case Notification.MEDIA_DECLINED:
message = `Your ${is4k ? '4K ' : ''}${mediaType} request was declined.`;
break;
case Notification.MEDIA_FAILED:
message = `Failed to process ${is4k ? '4K ' : ''}${mediaType} request.`;
break;
case Notification.MEDIA_PENDING:
message = `Approval required for a new ${
is4k ? '4K ' : ''
}${mediaType} request from ${
payload.request?.requestedBy.displayName
}.`;
break;
case Notification.ISSUE_CREATED:
message = `A new ${issueType} was reported by ${payload.issue?.createdBy.displayName}.`;
break;
case Notification.ISSUE_COMMENT:
message = `${payload.comment?.user.displayName} commented on the ${issueType}.`;
break;
case Notification.ISSUE_RESOLVED:
message = `The ${issueType} was marked as resolved by ${payload.issue?.modifiedBy?.displayName}!`;
break;
case Notification.ISSUE_REOPENED:
message = `The ${issueType} was reopened by ${payload.issue?.modifiedBy?.displayName}.`;
break;
default:
return {
notificationType: Notification[type],
subject: 'Unknown',
};
case Notification.TEST_NOTIFICATION:
return {
notificationType: Notification[type],
subject: payload.subject,
message: payload.message,
};
case Notification.MEDIA_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request has been approved.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AUTO_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Automatically approved a new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AVAILABLE:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request is now available!`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_DECLINED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request was declined.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_FAILED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Failed to process ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_PENDING:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Approval required for new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
}
const actionUrl = payload.issue
? `/issues/${payload.issue.id}`
: payload.media
? `/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined;
const actionUrlTitle = actionUrl
? `View ${payload.issue ? 'Issue' : 'Media'}`
: undefined;
return {
notificationType: Notification[type],
subject: payload.subject,
message,
image: payload.image,
requestId: payload.request?.id,
actionUrl,
actionUrlTitle,
};
}
public shouldSend(): boolean {
@@ -150,7 +145,7 @@ class WebPushAgent
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
let pushSubs: UserPushSubscription[] = [];
const pushSubs: UserPushSubscription[] = [];
const mainUser = await userRepository.findOne({ where: { id: 1 } });
@@ -168,13 +163,14 @@ class WebPushAgent
where: { user: payload.notifyUser.id },
});
pushSubs = notifySubs;
} else if (!payload.notifyUser) {
pushSubs.push(...notifySubs);
}
if (payload.notifyAdmin) {
const users = await userRepository.find();
const manageUsers = users.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(user.settings?.hasNotificationType(
@@ -182,9 +178,7 @@ class WebPushAgent
type
) ??
true) &&
// Check if it's the user's own auto-approved request
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !== payload.request?.requestedBy.id)
shouldSendAdminNotification(type, user, payload)
);
const allSubs = await userPushSubRepository
@@ -195,7 +189,7 @@ class WebPushAgent
})
.getMany();
pushSubs = allSubs;
pushSubs.push(...allSubs);
}
if (mainUser && pushSubs.length > 0) {
@@ -205,6 +199,11 @@ class WebPushAgent
settings.vapidPrivate
);
const notificationPayload = Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
);
await Promise.all(
pushSubs.map(async (sub) => {
logger.debug('Sending web push notification', {
@@ -223,10 +222,7 @@ class WebPushAgent
p256dh: sub.p256dh,
},
},
Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
)
notificationPayload
);
} catch (e) {
logger.error(

View File

@@ -1,4 +1,6 @@
import { User } from '../../entity/User';
import logger from '../../logger';
import { Permission } from '../permissions';
import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification {
@@ -10,6 +12,10 @@ export enum Notification {
TEST_NOTIFICATION = 32,
MEDIA_DECLINED = 64,
MEDIA_AUTO_APPROVED = 128,
ISSUE_CREATED = 256,
ISSUE_COMMENT = 512,
ISSUE_RESOLVED = 1024,
ISSUE_REOPENED = 2048,
}
export const hasNotificationType = (
@@ -38,6 +44,50 @@ export const hasNotificationType = (
return !!(value & total);
};
export const getAdminPermission = (type: Notification): Permission => {
switch (type) {
case Notification.MEDIA_PENDING:
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AVAILABLE:
case Notification.MEDIA_FAILED:
case Notification.MEDIA_DECLINED:
case Notification.MEDIA_AUTO_APPROVED:
return Permission.MANAGE_REQUESTS;
case Notification.ISSUE_CREATED:
case Notification.ISSUE_COMMENT:
case Notification.ISSUE_RESOLVED:
case Notification.ISSUE_REOPENED:
return Permission.MANAGE_ISSUES;
default:
return Permission.ADMIN;
}
};
export const shouldSendAdminNotification = (
type: Notification,
user: User,
payload: NotificationPayload
): boolean => {
return (
user.id !== payload.notifyUser?.id &&
user.hasPermission(getAdminPermission(type)) &&
// Check if the user submitted this request (on behalf of themself OR another user)
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !==
(payload.request?.modifiedBy ?? payload.request?.requestedBy)?.id) &&
// Check if the user created this issue
(type !== Notification.ISSUE_CREATED ||
user.id !== payload.issue?.createdBy.id) &&
// Check if the user submitted this issue comment
(type !== Notification.ISSUE_COMMENT ||
user.id !== payload.comment?.user.id) &&
// Check if the user resolved/reopened this issue
((type !== Notification.ISSUE_RESOLVED &&
type !== Notification.ISSUE_REOPENED) ||
user.id !== payload.issue?.modifiedBy?.id)
);
};
class NotificationManager {
private activeAgents: NotificationAgent[] = [];

View File

@@ -19,6 +19,9 @@ export enum Permission {
AUTO_APPROVE_4K_TV = 131072,
REQUEST_MOVIE = 262144,
REQUEST_TV = 524288,
MANAGE_ISSUES = 1048576,
VIEW_ISSUES = 2097152,
CREATE_ISSUES = 4194304,
}
export interface PermissionCheckOptions {

View File

@@ -146,9 +146,8 @@ class BaseScanner<T> {
existing[is4k ? 'externalServiceId4k' : 'externalServiceId'] !==
externalServiceId
) {
existing[
is4k ? 'externalServiceId4k' : 'externalServiceId'
] = externalServiceId;
existing[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
externalServiceId;
changedExisting = true;
}
@@ -157,9 +156,8 @@ class BaseScanner<T> {
existing[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !==
externalServiceSlug
) {
existing[
is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
] = externalServiceSlug;
existing[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
externalServiceSlug;
changedExisting = true;
}
@@ -390,15 +388,13 @@ class BaseScanner<T> {
}
if (externalServiceId !== undefined) {
media[
is4k ? 'externalServiceId4k' : 'externalServiceId'
] = externalServiceId;
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
externalServiceId;
}
if (externalServiceSlug !== undefined) {
media[
is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
] = externalServiceSlug;
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
externalServiceSlug;
}
// If the show is already available, and there are no new seasons, dont adjust

View File

@@ -31,7 +31,8 @@ type SyncStatus = StatusBase & {
class PlexScanner
extends BaseScanner<PlexLibraryItem>
implements RunnableScanner<SyncStatus> {
implements RunnableScanner<SyncStatus>
{
private plexClient: PlexAPI;
private libraries: Library[];
private currentLibrary: Library;
@@ -370,10 +371,10 @@ class PlexScanner
// If we got an IMDb ID, but no TMDb ID, lookup the TMDb ID with the IMDb ID
if (mediaIds.imdbId && !mediaIds.tmdbId) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
const tmdbMedia = await this.tmdb.getMediaByImdbId({
imdbId: mediaIds.imdbId,
});
mediaIds.tmdbId = tmdbMovie.id;
mediaIds.tmdbId = tmdbMedia.id;
}
// Cache GUIDs
@@ -384,10 +385,10 @@ class PlexScanner
const imdbMatch = plexitem.guid.match(imdbRegex);
if (imdbMatch) {
mediaIds.imdbId = imdbMatch[1];
const tmdbMovie = await this.tmdb.getMovieByImdbId({
const tmdbMedia = await this.tmdb.getMediaByImdbId({
imdbId: mediaIds.imdbId,
});
mediaIds.tmdbId = tmdbMovie.id;
mediaIds.tmdbId = tmdbMedia.id;
}
// Check if the agent is TMDb
} else if (plexitem.guid.match(tmdbRegex)) {
@@ -472,7 +473,7 @@ class PlexScanner
mediaIds.tmdbId = result.tmdbId;
mediaIds.imdbId = result?.imdbId;
} else if (result?.imdbId) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
const tmdbMovie = await this.tmdb.getMediaByImdbId({
imdbId: result.imdbId,
});
mediaIds.tmdbId = tmdbMovie.id;
@@ -521,7 +522,7 @@ class PlexScanner
if (special.tmdbId) {
await this.processPlexMovieByTmdbId(episode, special.tmdbId);
} else if (special.imdbId) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
const tmdbMovie = await this.tmdb.getMediaByImdbId({
imdbId: special.imdbId,
});
await this.processPlexMovieByTmdbId(episode, tmdbMovie.id);

View File

@@ -10,7 +10,8 @@ type SyncStatus = StatusBase & {
class RadarrScanner
extends BaseScanner<RadarrMovie>
implements RunnableScanner<SyncStatus> {
implements RunnableScanner<SyncStatus>
{
private servers: RadarrSettings[];
private currentServer: RadarrSettings;
private radarrApi: RadarrAPI;
@@ -72,7 +73,7 @@ class RadarrScanner
}
private async processRadarrMovie(radarrMovie: RadarrMovie): Promise<void> {
if (!radarrMovie.monitored && !radarrMovie.downloaded) {
if (!radarrMovie.monitored && !radarrMovie.hasFile) {
this.log(
'Title is unmonitored and has not been downloaded. Skipping item.',
'debug',
@@ -91,7 +92,7 @@ class RadarrScanner
externalServiceId: radarrMovie.id,
externalServiceSlug: radarrMovie.titleSlug,
title: radarrMovie.title,
processing: !radarrMovie.downloaded,
processing: !radarrMovie.hasFile,
});
} catch (e) {
this.log('Failed to process Radarr media', 'error', {

View File

@@ -1,6 +1,7 @@
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, {
@@ -16,7 +17,8 @@ type SyncStatus = StatusBase & {
class SonarrScanner
extends BaseScanner<SonarrSeries>
implements RunnableScanner<SyncStatus> {
implements RunnableScanner<SyncStatus>
{
private servers: SonarrSettings[];
private currentServer: SonarrSettings;
private sonarrApi: SonarrAPI;
@@ -82,24 +84,26 @@ class SonarrScanner
const mediaRepository = getRepository(Media);
const server4k = this.enable4kShow && this.currentServer.is4k;
const processableSeasons: ProcessableSeason[] = [];
let tmdbId: number;
let tvShow: TmdbTvDetails;
const media = await mediaRepository.findOne({
where: { tvdbId: sonarrSeries.tvdbId },
});
if (!media || !media.tmdbId) {
const tvShow = await this.tmdb.getShowByTvdbId({
tvShow = await this.tmdb.getShowByTvdbId({
tvdbId: sonarrSeries.tvdbId,
});
tmdbId = tvShow.id;
} else {
tmdbId = media.tmdbId;
tvShow = await this.tmdb.getTvShow({ tvId: media.tmdbId });
}
const tmdbId = tvShow.id;
const filteredSeasons = sonarrSeries.seasons.filter(
(sn) => sn.seasonNumber !== 0
(sn) =>
sn.seasonNumber !== 0 &&
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
);
for (const season of filteredSeasons) {

212
server/lib/search.ts Normal file
View File

@@ -0,0 +1,212 @@
import TheMovieDb from '../api/themoviedb';
import {
TmdbMovieDetails,
TmdbMovieResult,
TmdbPersonDetails,
TmdbPersonResult,
TmdbSearchMovieResponse,
TmdbSearchMultiResponse,
TmdbSearchTvResponse,
TmdbTvDetails,
TmdbTvResult,
} from '../api/themoviedb/interfaces';
import {
mapMovieDetailsToResult,
mapPersonDetailsToResult,
mapTvDetailsToResult,
} from '../models/Search';
import { isMovie, isMovieDetails, isTvDetails } from '../utils/typeHelpers';
interface SearchProvider {
pattern: RegExp;
search: ({
id,
language,
query,
}: {
id: string;
language?: string;
query?: string;
}) => Promise<TmdbSearchMultiResponse>;
}
const searchProviders: SearchProvider[] = [];
export const findSearchProvider = (
query: string
): SearchProvider | undefined => {
return searchProviders.find((provider) => provider.pattern.test(query));
};
searchProviders.push({
pattern: new RegExp(/(?<=tmdb:)\d+/),
search: async ({ id, language }) => {
const tmdb = new TheMovieDb();
const moviePromise = tmdb.getMovie({ movieId: parseInt(id), language });
const tvShowPromise = tmdb.getTvShow({ tvId: parseInt(id), language });
const personPromise = tmdb.getPerson({ personId: parseInt(id), language });
const responses = await Promise.allSettled([
moviePromise,
tvShowPromise,
personPromise,
]);
const successfulResponses = responses.filter(
(r) => r.status === 'fulfilled'
) as
| (
| PromiseFulfilledResult<TmdbMovieDetails>
| PromiseFulfilledResult<TmdbTvDetails>
| PromiseFulfilledResult<TmdbPersonDetails>
)[];
const results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] = [];
if (successfulResponses.length) {
results.push(
...successfulResponses.map((r) => {
if (isMovieDetails(r.value)) {
return mapMovieDetailsToResult(r.value);
} else if (isTvDetails(r.value)) {
return mapTvDetailsToResult(r.value);
} else {
return mapPersonDetailsToResult(r.value);
}
})
);
}
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});
searchProviders.push({
pattern: new RegExp(/(?<=imdb:)(tt|nm)\d+/),
search: async ({ id, language }) => {
const tmdb = new TheMovieDb();
const responses = await tmdb.getByExternalId({
externalId: id,
type: 'imdb',
language,
});
const results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] = [];
// set the media_type here since searching by external id doesn't return it
results.push(
...(responses.movie_results.map((movie) => ({
...movie,
media_type: 'movie',
})) as TmdbMovieResult[]),
...(responses.tv_results.map((tv) => ({
...tv,
media_type: 'tv',
})) as TmdbTvResult[]),
...(responses.person_results.map((person) => ({
...person,
media_type: 'person',
})) as TmdbPersonResult[])
);
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});
searchProviders.push({
pattern: new RegExp(/(?<=tvdb:)\d+/),
search: async ({ id, language }) => {
const tmdb = new TheMovieDb();
const responses = await tmdb.getByExternalId({
externalId: parseInt(id),
type: 'tvdb',
language,
});
const results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] = [];
// set the media_type here since searching by external id doesn't return it
results.push(
...(responses.movie_results.map((movie) => ({
...movie,
media_type: 'movie',
})) as TmdbMovieResult[]),
...(responses.tv_results.map((tv) => ({
...tv,
media_type: 'tv',
})) as TmdbTvResult[]),
...(responses.person_results.map((person) => ({
...person,
media_type: 'person',
})) as TmdbPersonResult[])
);
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});
searchProviders.push({
pattern: new RegExp(/(?<=year:)\d{4}/),
search: async ({ id: year, query }) => {
const tmdb = new TheMovieDb();
const moviesPromise = tmdb.searchMovies({
query: query?.replace(new RegExp(/year:\d{4}/), '') ?? '',
year: parseInt(year),
});
const tvShowsPromise = tmdb.searchTvShows({
query: query?.replace(new RegExp(/year:\d{4}/), '') ?? '',
year: parseInt(year),
});
const responses = await Promise.allSettled([moviesPromise, tvShowsPromise]);
const successfulResponses = responses.filter(
(r) => r.status === 'fulfilled'
) as
| (
| PromiseFulfilledResult<TmdbSearchMovieResponse>
| PromiseFulfilledResult<TmdbSearchTvResponse>
)[];
const results: (TmdbMovieResult | TmdbTvResult)[] = [];
if (successfulResponses.length) {
successfulResponses.forEach((response) => {
response.value.results.forEach((result) =>
// set the media_type here since the search endpoints don't return it
results.push(
isMovie(result)
? { ...result, media_type: 'movie' }
: { ...result, media_type: 'tv' }
)
);
});
}
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});

View File

@@ -39,9 +39,18 @@ export interface PlexSettings {
export interface JellyfinSettings {
name: string;
hostname?: string;
externalHostname?: string;
libraries: Library[];
serverId: string;
}
export interface TautulliSettings {
hostname?: string;
port?: number;
useSsl?: boolean;
urlBase?: string;
apiKey?: string;
externalUrl?: string;
}
export interface DVRSettings {
id: number;
@@ -125,6 +134,7 @@ interface FullPublicSettings extends PublicSettings {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
newPlexLogin: boolean;
}
export interface NotificationAgentConfig {
@@ -137,6 +147,7 @@ export interface NotificationAgentDiscord extends NotificationAgentConfig {
botUsername?: string;
botAvatarUrl?: string;
webhookUrl: string;
enableMentions: boolean;
};
}
@@ -182,6 +193,7 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig {
export interface NotificationAgentPushbullet extends NotificationAgentConfig {
options: {
accessToken: string;
channelTag?: string;
};
}
@@ -200,9 +212,17 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
};
}
export interface NotificationAgentGotify extends NotificationAgentConfig {
options: {
url: string;
token: string;
};
}
export enum NotificationAgentKey {
DISCORD = 'discord',
EMAIL = 'email',
GOTIFY = 'gotify',
PUSHBULLET = 'pushbullet',
PUSHOVER = 'pushover',
SLACK = 'slack',
@@ -214,6 +234,7 @@ export enum NotificationAgentKey {
interface NotificationAgents {
discord: NotificationAgentDiscord;
email: NotificationAgentEmail;
gotify: NotificationAgentGotify;
lunasea: NotificationAgentLunaSea;
pushbullet: NotificationAgentPushbullet;
pushover: NotificationAgentPushover;
@@ -227,6 +248,20 @@ interface NotificationSettings {
agents: NotificationAgents;
}
interface JobSettings {
schedule: string;
}
export type JobId =
| 'plex-recently-added-scan'
| 'plex-full-scan'
| 'radarr-scan'
| 'sonarr-scan'
| 'download-sync'
| 'download-sync-reset'
| 'jellyfin-recently-added-sync'
| 'jellyfin-full-sync';
interface AllSettings {
clientId: string;
vapidPublic: string;
@@ -234,10 +269,12 @@ interface AllSettings {
main: MainSettings;
plex: PlexSettings;
jellyfin: JellyfinSettings;
tautulli: TautulliSettings;
radarr: RadarrSettings[];
sonarr: SonarrSettings[];
public: PublicSettings;
notifications: NotificationSettings;
jobs: Record<JobId, JobSettings>;
}
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
@@ -283,9 +320,11 @@ class Settings {
jellyfin: {
name: '',
hostname: '',
externalHostname: '',
libraries: [],
serverId: '',
},
tautulli: {},
radarr: [],
sonarr: [],
public: {
@@ -303,7 +342,7 @@ class Settings {
ignoreTls: false,
requireTls: false,
allowSelfSigned: false,
senderName: 'Jellyseerr',
senderName: 'Overseerr',
},
},
discord: {
@@ -311,6 +350,7 @@ class Settings {
types: 0,
options: {
webhookUrl: '',
enableMentions: true,
},
},
lunasea: {
@@ -357,13 +397,47 @@ class Settings {
options: {
webhookUrl: '',
jsonPayload:
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
'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 * * *',
},
'jellyfin-recently-added-sync': {
schedule: '0 */5 * * * *',
},
'jellyfin-full-sync': {
schedule: '0 0 3 * * *',
},
},
};
@@ -400,6 +474,14 @@ class Settings {
this.data.jellyfin = data;
}
get tautulli(): TautulliSettings {
return this.data.tautulli;
}
set tautulli(data: TautulliSettings) {
this.data.tautulli = data;
}
get radarr(): RadarrSettings[] {
return this.data.radarr;
}
@@ -447,6 +529,7 @@ class Settings {
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
locale: this.data.main.locale,
emailEnabled: this.data.notifications.agents.email.enabled,
newPlexLogin: this.data.main.newPlexLogin,
};
}
@@ -458,6 +541,14 @@ class Settings {
this.data.notifications = data;
}
get jobs(): Record<JobId, JobSettings> {
return this.data.jobs;
}
set jobs(data: Record<JobId, JobSettings>) {
this.data.jobs = data;
}
get clientId(): string {
if (!this.data.clientId) {
this.data.clientId = randomUUID();

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