Compare commits

..

131 Commits

Author SHA1 Message Date
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
sct
5e5ba40505 fix: change default internal port to 5055 (#389) 2020-12-19 00:12:41 +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
ecelebi29
36b2b4fba4 docs: fixed multiple typos, updated latest en.json strings (#371) [skip ci] 2020-12-18 20:45:31 +09:00
sct
6beac736ef feat: api key regeneration 2020-12-18 19:40:21 +09:00
sct
6933b661ca refactor(frontend): remove notification bell from user bar for now 2020-12-18 19:11:39 +09:00
sct
4b6ad8a387 feat(frontend): add studio/networks to movie/tv details
closes #370
2020-12-18 19:01:56 +09:00
sct
992a23222a ci: add @semantic-release/github to release plugins 2020-12-18 18:37:52 +09:00
sct
e75b592bb2 ci: fix image name for ghcr.io tags 2020-12-18 07:29:23 +00:00
sct
baa62b286d ci: push develop images to ghcr.io 2020-12-18 07:04:17 +00:00
sct
908f63557c feat(holiday): special seasonal slider added to discover :) 2020-12-18 06:14:41 +00:00
sct
f88c4a6d4a feat(api): add movie keyword search 2020-12-18 06:13:44 +00:00
sct
0972f40a4e feat: anime profile support (#384)
closes #266
2020-12-18 14:32:40 +09:00
allcontributors[bot]
1f0486eba2 docs: add mmozeiko as a contributor (#382) [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>
2020-12-18 11:12:30 +09:00
Mārtiņš Možeiko
086183b563 feat: allow to listen server on specific host interface (#381)
fix #273
2020-12-18 11:11:20 +09:00
samwiseg0
df4ef1fbe5 docs: Remove the issue lock as it will not allow users to edit [skip ci] (#376) 2020-12-18 08:06:14 +09:00
sct
727fa06c18 feat(frontend): added user deletion to the user list
also includes small updates to the api to prevent administrators from being deleted, as well as
migrations to cascade deletions to requests the users made

fixes #348
2020-12-17 16:03:27 +00: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
Weblate (bot)
ee84f74f8a feat(lang): translations update from Weblate (#336)
* feat(lang): translated using Weblate (Spanish)

Currently translated at 96.4% (323 of 335 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (335 of 335 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 95.2% (319 of 335 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 95.2% (319 of 335 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 98.7% (319 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
2020-12-17 22:54:41 +09:00
sct
18925decaf fix(frontend): correctly show an unauthorized error when a user fails to login
fixes #322
2020-12-17 12:05:45 +00:00
sct
67146c33ef fix(plex-sync): bundle duplicate ratingKeys to speed up recently added sync
This includes a rewrite to move movie/series availability notifications into a subscriber to prevent
duplicate notifications for series

fix #360
2020-12-17 06:28:03 +00:00
sct
d5eb4d8d43 fix(email): do not pass auth object to transport if no auth data present
re #312
2020-12-17 04:06:45 +00:00
sct
fc12ab84d9 fix(frontend): clarify that radarr/sonnarr servers must be tested before profiles/folders appear
Also blocks "Add Server" or "Save" button until all required fields are entered

fixes #326 and #328
2020-12-17 03:28:49 +00:00
sct
0d088e085e feat(frontend): show alert when there are no default radarr/sonarr servers
fixes #344
2020-12-17 02:08:09 +00:00
sct
2fe53ec5a8 fix(frontend): make minimum availability required for Radarr servers
fixes #345
2020-12-17 01:42:21 +00:00
sct
ce0266f74e fix(frontend): add http/https prefix to hostname fields for plex/radarr/sonarr
fixes #357
2020-12-17 01:36:23 +00:00
sct
c21fa5b535 fix(frontend): fix tv shows failing to open when firstAirDate is undefined
fix #347
2020-12-17 01:14:12 +00:00
allcontributors[bot]
647d5efb09 docs: add ecelebi29 as a contributor (#362) [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>
2020-12-17 10:07:40 +09:00
ecelebi29
bb89f679b6 docs(contributing.md, readme.md): fixed 2 small typos (#361) [skip ci] 2020-12-17 10:07:07 +09:00
allcontributors[bot]
3210ac29d0 docs: add ecelebi29 as a contributor (#359) [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>
2020-12-17 09:20:28 +09:00
ecelebi29
e9c899ce41 fix(sonarr.ts, mediarequest.ts): add missing seasonFolder option (#358) 2020-12-17 09:18:18 +09: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
allcontributors[bot]
ad18a40969 docs: add samwiseg0 as a contributor (#341) [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>
2020-12-16 14:06:25 +09:00
samwiseg0
ae2ede8952 docs: add invalid template workflow (#340) 2020-12-16 13:56:52 +09:00
sct
675060bcdf fix(api): set plex libraries to disabled if the name changes
re #324
2020-12-16 04:07:59 +00:00
sct
f8f388db6d refactor(api): add better error context when plex items fail to sync 2020-12-16 03:57:03 +00:00
sct
2948f9360e feat: add version to startup logs 2020-12-16 03:54:32 +00:00
sct
df4ac8361f fix(frontend): adds a tip to plex setup to clarify that syncing runs in the background
fixes #325
2020-12-16 02:35:15 +00:00
sct
92a874e36d docs: update readme.md with emoji key for contributors 2020-12-16 02:07:49 +00:00
Brandon Cohen
6d7907e844 fix(frontend): close sidebar when clicking outside (#333) 2020-12-16 10:58:41 +09:00
sct
d9788c4aa9 feat(frontend): temporary logs page to clear up confusion about it 404ing
This commit also disables the "Run Now" buttons under Jobs until they actually work

re #272
2020-12-16 01:35:26 +00:00
Brandon Cohen
db0a5c44f6 fix(frontend): aligned movie and tv details (#331) 2020-12-16 09:44:58 +09:00
allcontributors[bot]
c545ec16e7 docs: add kieron as a contributor (#329) [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>
2020-12-16 09:29:17 +09:00
Kieron Boswell
961d110720 fix: spelling mistake on the word 'requested' fixed (#319) 2020-12-16 09:28:09 +09:00
sct
e8776fd336 refactor(frontend): titlecard behavior changed to allow clicking anywhere to go through to title
mobile behavior remains mostly the same, except after the first click, a second click anywhere else
will go through to the title.
2020-12-15 15:49:59 +00:00
allcontributors[bot]
7c08809501 docs: add krystiancharubin as a contributor (#311) [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>
2020-12-16 00:43:31 +09:00
Krystian Charubin
7fd43c9529 style: dark splash screen (#310) 2020-12-16 00:42:27 +09:00
sct
33f8831e88 fix(api): accept the api key to perform actions on the api with X-API-Key header 2020-12-15 14:13:41 +00:00
sct
20b119c1ac docs: update bug report issue template to include app version [skip ci] 2020-12-15 11:55:24 +00:00
sct
6cd20491d2 feat(lang): add support for Spanish language 2020-12-15 11:46:43 +00:00
allcontributors[bot]
cb8a50bd66 docs: add Shutruk as a contributor (#304) [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>
2020-12-15 20:40:15 +09:00
Weblate (bot)
fddbb3cdfe feat(lang): Translations update from Weblate (#291)
* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (321 of 321 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 56.0% (180 of 321 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 52.6% (169 of 321 strings)

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

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

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

Currently translated at 100.0% (310 of 310 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (314 of 314 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (310 of 310 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (310 of 310 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheRaZex <therazex@protonmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
2020-12-15 20:39:14 +09:00
sct
01c179f762 fix(api): filter out libraries that do not have any metadata agent or are not movie/show 2020-12-15 10:24:39 +00:00
sct
e08fa35548 fix(api): only run recently added sync on enabled libraries
fixes #259
2020-12-15 10:23:19 +00:00
sct
d96d65b8af refactor(api): increased plex sync speed and changed full sync to run every 24 hours
With this change, plex sync will now process 20 items at a time every 4 seconds. (About double its
previous speed). We will see how much faster we can push this in the future.
2020-12-15 09:16:35 +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
sct
900827be97 feat(lang): add i18n strings for new about page 2020-12-15 08:08:01 +00:00
sct
b9faa6486b fix(api): require package.json directly so typescript doesnt compile it into dist folder 2020-12-15 07:56:53 +00:00
sct
3f2a04c881 feat: about page initial version
Adding something to the about page for now, including the app version to better know what versions
of the app people are running
2020-12-15 07:27:30 +00:00
sct
ff618956b5 fix(frontend): only show alpha notice to admins 2020-12-15 05:15:45 +00:00
sct
b5988f9a5f fix(frontend): add validation for Radarr/Sonarr server name 2020-12-15 03:30:13 +00:00
sct
3ba09d07eb fix: add support for ssl when connecting to plex
fixes #275
2020-12-15 03:22:54 +00:00
Alex Zoitos
f998873fc5 fix(services): improve logging for when Radarr movie already exists (#285)
re #260
2020-12-15 11:11:48 +09:00
allcontributors[bot]
33b54c08b5 docs: add saltydk as a contributor (#286) [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>
2020-12-15 11:05:19 +09:00
salty
34136ef617 build(dockerfile): Expose port 3000. (#278)
Allows for automated nginx reverse proxy setups to identify the port used in the container without having to bind the port to the host/localhost.
2020-12-15 10:57:01 +09: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
allcontributors[bot]
2d3359a7e3 docs: add darknessgp as a contributor (#279) [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>
2020-12-15 09:15:18 +09:00
darknessgp
6d08b10820 fix: changing parameter name to use correct 'port' #276 (#277)
Co-authored-by: Ryan Boyles <Ryan.Boyles@ahstech.com>
2020-12-15 09:14:11 +09:00
sct
78af1a3e6d fix(services): include radarr/sonarr baseUrl when adding media 2020-12-14 16:39:01 +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
allcontributors[bot]
67a8f7fbfe docs: add jvennik as a contributor (#253) [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>
2020-12-14 19:08:45 +09:00
Weblate (bot)
e17c637483 feat(lang): Translations update from Weblate (#240)
* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (306 of 306 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (310 of 310 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 88.8% (272 of 306 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 72.5% (222 of 306 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 69.9% (214 of 306 strings)

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

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

Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (306 of 306 strings)

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

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
2020-12-14 19:07:46 +09:00
sct
15013d6c5d fix(frontend): encode special characters in search input to prevent crashing router
re #252
2020-12-14 09:38:38 +00:00
sct
8cb05c413a fix(frontend): convert plex port to a number before posting to the api 2020-12-14 09:13:59 +00:00
sct
2098a2d3d2 fix(frontend): converts email smtp port to a number before posting to the api
- also adds toast notifications to both email/discord notifications pages to know when sucesses or
failures occur

re #251
2020-12-14 09:00:10 +00:00
sct
6c1ee830a1 fix(services): improve logging for adding movies to Radarr 2020-12-14 08:17:38 +00:00
sct
0658b7943e fix(services): radarr/sonarr will use the correct default server 2020-12-14 08:14:08 +00:00
semantic-release-bot
f1df9eeb2f chore(release): 1.3.1 2020-12-14 08:03:51 +00:00
sct
edbbccf3ae fix(plex sync): catch errors that occur during processMovie
this also removes the unique constraint on imdbId

re #244 #246 #250
2020-12-14 08:01:33 +00:00
sct
a3973791fb Merge branch 'develop' 2020-12-14 07:59:18 +00:00
sct
cb5c0403c1 docs: update bug template with docker version question [skip ci] 2020-12-14 07:57:33 +00:00
sct
7bf924f7e9 fix(frontend): also convert activeProfileId to a number for radarr/sonarr submissions 2020-12-14 07:17:32 +00:00
sct
c53dc3b15d fix(frontend): also convert ports to numbers when saving radarr/sonarr servers 2020-12-14 07:02:29 +00:00
sct
b96bd33982 refactor(frontend): remove search filter buttons until they actually do something 2020-12-14 06:40:31 +00:00
sct
92c9001c9d fix(frontend): new radarr/sonarr ports will be converted to a number before posting 2020-12-14 06:37:34 +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
sct
5f09e83ed8 fix(api): correctly generate clientId on first startup 2020-12-14 05:18:22 +00:00
sct
27349ec953 docs: update readme.md 2020-12-14 04:49:00 +00:00
allcontributors[bot]
2ce6e0a94a docs: add angrycuban13 as a contributor (#242) [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>
2020-12-14 13:44:24 +09:00
Angry Cuban
da65988926 docs: Update README.md (#241) [skip ci] 2020-12-14 13:38:47 +09:00
sct
b3b8e7b0cc docs: update readme [skip ci] 2020-12-14 01:05:11 +00:00
sct
c8f1756a2b Merge pull request #239 from weblate/weblate-overseerr-overseerr-frontend
Translations update from Weblate
2020-12-14 00:28:18 +09:00
Hosted Weblate
f943701e13 feat(lang): translated using Weblate (French)
Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (306 of 306 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend
2020-12-13 16:24:27 +01:00
Hosted Weblate
7bf9addd13 feat(lang): translated using Weblate (German)
Currently translated at 99.3% (304 of 306 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend
2020-12-13 16:24:27 +01:00
dependabot[bot]
93983c976b build(deps): bump yup from 0.32.6 to 0.32.8 (#223)
Bumps [yup](https://github.com/jquense/yup) from 0.32.6 to 0.32.8.
- [Release notes](https://github.com/jquense/yup/releases)
- [Changelog](https://github.com/jquense/yup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jquense/yup/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-13 21:24:25 +09:00
dependabot[bot]
05a2fddf4a build(deps): bump ini from 1.3.5 to 1.3.8 (#237)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-13 15:28:43 +09:00
sct
7f2be51a7e refactor(frontend): change person details page to show appears in titles as list view 2020-12-12 12:39:05 +00:00
sct
de01e13dda docs: update all-contributors list 2020-12-12 12:37:15 +00:00
allcontributors[bot]
beca52715a docs: add GiyomuWeb as a contributor (#235)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-12 21:09:03 +09:00
allcontributors[bot]
61ed508a56 docs: add KovalevArtem as a contributor (#234)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-12 21:08:21 +09:00
allcontributors[bot]
ce9d9fdf13 docs: add Ahreluth as a contributor (#233)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-12 21:06:55 +09:00
allcontributors[bot]
e32b585e97 docs: add comradekingu as a contributor (#232)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-12 21:05:29 +09:00
allcontributors[bot]
6d2abfa863 docs: add OwsleyJr as a contributor (#231)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-12 21:04:00 +09:00
allcontributors[bot]
d02724770d docs: add azoitos as a contributor (#230)
* docs: update README.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-12 21:02:57 +09:00
sct
5fa37b5913 ci: add the ability to skip ci with commit message [skip ci] 2020-12-12 11:59:15 +00:00
sct
5155a4caf2 docs: move all-contributors table into the correct section 2020-12-12 11:57:37 +00:00
sct
42381ed5cb docs: fix all-contributors template/position 2020-12-12 11:55:47 +00:00
sct
b99f9e6888 Merge pull request #229 from sct/all-contributors/add-sct
docs: add sct as a contributor
2020-12-12 20:50:51 +09:00
allcontributors[bot]
e6c5d6ff7c docs: create .all-contributorsrc [skip ci] 2020-12-12 11:19:05 +00:00
allcontributors[bot]
4f9514885b docs: update README.md [skip ci] 2020-12-12 11:19:04 +00:00
sct
52e695bd08 docs: remove contributors image so we can replace it with all-contributors 2020-12-12 10:59:12 +00:00
sct
051f1b3e89 feat(frontend): add full cast page for movies and series 2020-12-12 10:54:53 +00:00
sct
4392a881aa Merge pull request #219 from weblate/weblate-overseerr-overseerr-frontend
Translations update from Weblate
2020-12-12 17:32:24 +09:00
Hosted Weblate
0949c9b334 feat(lang): translated using Weblate (English)
Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (English)

Currently translated at 100.0% (304 of 304 strings)

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/en/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
30d04ce35a feat(lang): translated using Weblate (French)
Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
83efb0e3d4 feat(lang): translated using Weblate (Norwegian Bokmål)
Currently translated at 87.1% (265 of 304 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
08e968fd00 feat(lang): translated using Weblate (Japanese)
Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 99.0% (301 of 304 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 99.0% (301 of 304 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shingo Takada <geneofthetiger@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
0d8e0d0352 feat(lang): translated using Weblate (Russian)
Currently translated at 26.9% (82 of 304 strings)

Co-authored-by: Artem <KovalevArtem.ru@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
1ab3a4b80a feat(lang): translated using Weblate (Dutch)
Currently translated at 18.7% (57 of 304 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
b6e60a412b feat(lang): translated using Weblate (German)
Currently translated at 99.3% (302 of 304 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.3% (302 of 304 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.3% (302 of 304 strings)

feat(lang): translated using Weblate (German)

Currently translated at 71.3% (217 of 304 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
sct
d4cc727d52 ci: fix typo in github workflow 2020-12-12 07:13:55 +00:00
97 changed files with 3908 additions and 1419 deletions

165
.all-contributorsrc Normal file
View File

@@ -0,0 +1,165 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "sct",
"name": "sct",
"avatar_url": "https://avatars1.githubusercontent.com/u/234213?v=4",
"profile": "https://sct.dev",
"contributions": [
"code",
"design",
"ideas"
]
},
{
"login": "azoitos",
"name": "Alex Zoitos",
"avatar_url": "https://avatars2.githubusercontent.com/u/26529049?v=4",
"profile": "https://github.com/azoitos",
"contributions": [
"code"
]
},
{
"login": "OwsleyJr",
"name": "Brandon Cohen",
"avatar_url": "https://avatars3.githubusercontent.com/u/8635678?v=4",
"profile": "https://github.com/OwsleyJr",
"contributions": [
"code",
"doc"
]
},
{
"login": "Ahreluth",
"name": "Ahreluth",
"avatar_url": "https://avatars2.githubusercontent.com/u/75682440?v=4",
"profile": "https://github.com/Ahreluth",
"contributions": [
"translation"
]
},
{
"login": "KovalevArtem",
"name": "KovalevArtem",
"avatar_url": "https://avatars0.githubusercontent.com/u/36500228?v=4",
"profile": "https://github.com/KovalevArtem",
"contributions": [
"translation"
]
},
{
"login": "GiyomuWeb",
"name": "GiyomuWeb",
"avatar_url": "https://avatars0.githubusercontent.com/u/62489209?v=4",
"profile": "https://github.com/GiyomuWeb",
"contributions": [
"translation"
]
},
{
"login": "angrycuban13",
"name": "Angry Cuban",
"avatar_url": "https://avatars3.githubusercontent.com/u/39564898?v=4",
"profile": "https://github.com/angrycuban13",
"contributions": [
"doc"
]
},
{
"login": "jvennik",
"name": "jvennik",
"avatar_url": "https://avatars3.githubusercontent.com/u/6672637?v=4",
"profile": "https://github.com/jvennik",
"contributions": [
"translation"
]
},
{
"login": "darknessgp",
"name": "darknessgp",
"avatar_url": "https://avatars0.githubusercontent.com/u/1521243?v=4",
"profile": "https://github.com/darknessgp",
"contributions": [
"code"
]
},
{
"login": "saltydk",
"name": "salty",
"avatar_url": "https://avatars1.githubusercontent.com/u/6587950?v=4",
"profile": "https://github.com/saltydk",
"contributions": [
"infra"
]
},
{
"login": "Shutruk",
"name": "Shutruk",
"avatar_url": "https://avatars2.githubusercontent.com/u/9198633?v=4",
"profile": "https://github.com/Shutruk",
"contributions": [
"translation"
]
},
{
"login": "krystiancharubin",
"name": "Krystian Charubin",
"avatar_url": "https://avatars2.githubusercontent.com/u/17775600?v=4",
"profile": "https://github.com/krystiancharubin",
"contributions": [
"design"
]
},
{
"login": "kieron",
"name": "Kieron Boswell",
"avatar_url": "https://avatars2.githubusercontent.com/u/8655212?v=4",
"profile": "https://github.com/kieron",
"contributions": [
"code"
]
},
{
"login": "samwiseg0",
"name": "samwiseg0",
"avatar_url": "https://avatars1.githubusercontent.com/u/2241731?v=4",
"profile": "https://github.com/samwiseg0",
"contributions": [
"question",
"infra"
]
},
{
"login": "ecelebi29",
"name": "ecelebi29",
"avatar_url": "https://avatars2.githubusercontent.com/u/8337120?v=4",
"profile": "https://github.com/ecelebi29",
"contributions": [
"code",
"doc"
]
},
{
"login": "mmozeiko",
"name": "Mārtiņš Možeiko",
"avatar_url": "https://avatars3.githubusercontent.com/u/1665010?v=4",
"profile": "https://github.com/mmozeiko",
"contributions": [
"code"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
"contributorsPerLine": 7,
"projectName": "overseerr",
"projectOwner": "sct",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

View File

@@ -9,6 +9,11 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is.
**What version of Overseerr are you running?**
Please fill in the version you are currently running.
You can find it under: Settings -> About -> Version
**To Reproduce**
Steps to reproduce the behavior:

View File

@@ -26,15 +26,34 @@ jobs:
build_and_push:
name: Build and push Docker image to Docker Hub
needs: test
if: github.ref == 'refs/heads/develop'
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, 'skip ci')
runs-on: ubuntu-latest
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: Build and push to Docker Hub
uses: docker/build-push-action@v1
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: sctx/overseerr
tags: develop
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: |
sctx/overseerr:develop
sctx/overseerr:${{ github.sha }}
ghcr.io/sct/overseerr:develop
ghcr.io/sct/overseerr:${{ github.sha }}

19
.github/workflows/invalid_template.yml vendored Normal file
View File

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

View File

@@ -20,7 +20,7 @@ jobs:
run: yarn lint
- name: build
run: yarn build
semnatic-release:
semantic-release:
name: Tag and release latest version
needs: test
runs-on: ubuntu-latest

View File

@@ -1,3 +1,143 @@
## [1.9.1](https://github.com/sct/overseerr/compare/v1.9.0...v1.9.1) (2020-12-18)
### Bug Fixes
* change default internal port to 5055 ([#389](https://github.com/sct/overseerr/issues/389)) ([5e5ba40](https://github.com/sct/overseerr/commit/5e5ba4050563f07bff367d2fb31ed7e7fca4291e))
# [1.9.0](https://github.com/sct/overseerr/compare/v1.8.0...v1.9.0) (2020-12-18)
### Features
* api key regeneration ([6beac73](https://github.com/sct/overseerr/commit/6beac736efcf7b9102e02e43b75d91a9a158cd22))
* **api:** add movie keyword search ([f88c4a6](https://github.com/sct/overseerr/commit/f88c4a6d4a49f8f3451ba6c85153677f33b7f5f6))
* **frontend:** add studio/networks to movie/tv details ([4b6ad8a](https://github.com/sct/overseerr/commit/4b6ad8a3871957db4192b603abf38404250cea5d)), closes [#370](https://github.com/sct/overseerr/issues/370)
* **frontend:** added user deletion to the user list ([727fa06](https://github.com/sct/overseerr/commit/727fa06c18febb2a97ca219cc6bf0277ff462acd)), closes [#348](https://github.com/sct/overseerr/issues/348)
* **holiday:** special seasonal slider added to discover :) ([908f635](https://github.com/sct/overseerr/commit/908f63557ca03a1da8b16809ffa2c3acd782d94e))
* allow to listen server on specific host interface ([#381](https://github.com/sct/overseerr/issues/381)) ([086183b](https://github.com/sct/overseerr/commit/086183b5636aa8d075d01fe59492c3eab0d1345b)), closes [#273](https://github.com/sct/overseerr/issues/273)
* anime profile support ([#384](https://github.com/sct/overseerr/issues/384)) ([0972f40](https://github.com/sct/overseerr/commit/0972f40a4e1fb3b5f02b07ae46b997d71aab9bfb)), closes [#266](https://github.com/sct/overseerr/issues/266)
# [1.8.0](https://github.com/sct/overseerr/compare/v1.7.0...v1.8.0) (2020-12-17)
### Features
* **lang:** translations update from Weblate ([#336](https://github.com/sct/overseerr/issues/336)) ([ee84f74](https://github.com/sct/overseerr/commit/ee84f74f8a3558875b41daa539f42d00b949898a))
# [1.7.0](https://github.com/sct/overseerr/compare/v1.6.0...v1.7.0) (2020-12-17)
### Bug Fixes
* **email:** do not pass auth object to transport if no auth data present ([d5eb4d8](https://github.com/sct/overseerr/commit/d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef)), closes [#312](https://github.com/sct/overseerr/issues/312)
* **frontend:** add http/https prefix to hostname fields for plex/radarr/sonarr ([ce0266f](https://github.com/sct/overseerr/commit/ce0266f74ea3979b291ff962271a928682892788)), closes [#357](https://github.com/sct/overseerr/issues/357)
* **frontend:** clarify that radarr/sonnarr servers must be tested before profiles/folders appear ([fc12ab8](https://github.com/sct/overseerr/commit/fc12ab84d9482eb3a11f117f8cab6fd48a9401cd)), closes [#326](https://github.com/sct/overseerr/issues/326) [#328](https://github.com/sct/overseerr/issues/328)
* **frontend:** correctly show an unauthorized error when a user fails to login ([18925de](https://github.com/sct/overseerr/commit/18925decafdac518f52a354c594cc378d2529022)), closes [#322](https://github.com/sct/overseerr/issues/322)
* **frontend:** fix tv shows failing to open when firstAirDate is undefined ([c21fa5b](https://github.com/sct/overseerr/commit/c21fa5b5350abdd8e03c077fde7246fa398e176e)), closes [#347](https://github.com/sct/overseerr/issues/347)
* **frontend:** make minimum availability required for Radarr servers ([2fe53ec](https://github.com/sct/overseerr/commit/2fe53ec5a8534e75c7d0cef31a8b46065111e0a7)), closes [#345](https://github.com/sct/overseerr/issues/345)
* **plex-sync:** bundle duplicate ratingKeys to speed up recently added sync ([67146c3](https://github.com/sct/overseerr/commit/67146c33ef7f28d520ba2c50b32673d43f4525c8)), closes [#360](https://github.com/sct/overseerr/issues/360)
* **sonarr.ts, mediarequest.ts:** add missing seasonFolder option ([#358](https://github.com/sct/overseerr/issues/358)) ([e9c899c](https://github.com/sct/overseerr/commit/e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf))
### Features
* **frontend:** show alert when there are no default radarr/sonarr servers ([0d088e0](https://github.com/sct/overseerr/commit/0d088e085e68d39455fda21d1fd08ebcaef2c06b)), closes [#344](https://github.com/sct/overseerr/issues/344)
# [1.6.0](https://github.com/sct/overseerr/compare/v1.5.0...v1.6.0) (2020-12-16)
### Bug Fixes
* **api:** accept the api key to perform actions on the api with X-API-Key header ([33f8831](https://github.com/sct/overseerr/commit/33f8831e880dc7fd3f69d951246cada5c6c0ffe7))
* **api:** filter out libraries that do not have any metadata agent or are not movie/show ([01c179f](https://github.com/sct/overseerr/commit/01c179f762e686a1e5a3d4dab3a5bea53425b575))
* **api:** only run recently added sync on enabled libraries ([e08fa35](https://github.com/sct/overseerr/commit/e08fa35548bb8644afa8df3124e6f9cc3a2c8f4a)), closes [#259](https://github.com/sct/overseerr/issues/259)
* **api:** set plex libraries to disabled if the name changes ([675060b](https://github.com/sct/overseerr/commit/675060bcdf23acbfd4de2900a65f95e74f4966a5)), closes [#324](https://github.com/sct/overseerr/issues/324)
* **frontend:** adds a tip to plex setup to clarify that syncing runs in the background ([df4ac83](https://github.com/sct/overseerr/commit/df4ac8361f82971ee845f3be217408a9123a0bf3)), closes [#325](https://github.com/sct/overseerr/issues/325)
* **frontend:** aligned movie and tv details ([#331](https://github.com/sct/overseerr/issues/331)) ([db0a5c4](https://github.com/sct/overseerr/commit/db0a5c44f678e76eee7f5582381016306d1f46a2))
* **frontend:** close sidebar when clicking outside ([#333](https://github.com/sct/overseerr/issues/333)) ([6d7907e](https://github.com/sct/overseerr/commit/6d7907e844a909993d185759d660632f55aeaa35))
* spelling mistake on the word 'requested' fixed ([#319](https://github.com/sct/overseerr/issues/319)) ([961d110](https://github.com/sct/overseerr/commit/961d1107208069a6fc820a1ba97ffda7336677cb))
### Features
* add version to startup logs ([2948f93](https://github.com/sct/overseerr/commit/2948f9360eb484d1d6c0740a840135ca97e7240a))
* **frontend:** temporary logs page to clear up confusion about it 404ing ([d9788c4](https://github.com/sct/overseerr/commit/d9788c4aa9f87e2eda3f7e3f1adc985f16039552)), closes [#272](https://github.com/sct/overseerr/issues/272)
* **lang:** add support for Spanish language ([6cd2049](https://github.com/sct/overseerr/commit/6cd20491d2a0ceb995c4744eeb92a6e2f57a4893))
* **lang:** Translations update from Weblate ([#291](https://github.com/sct/overseerr/issues/291)) ([fddbb3c](https://github.com/sct/overseerr/commit/fddbb3cdfe3d50b2835c248556139c769dc2b805))
# [1.5.0](https://github.com/sct/overseerr/compare/v1.4.0...v1.5.0) (2020-12-15)
### Bug Fixes
* **api:** require package.json directly so typescript doesnt compile it into dist folder ([b9faa64](https://github.com/sct/overseerr/commit/b9faa6486b35aa865019aa8af9d307531054bc1d))
* **frontend:** add validation for Radarr/Sonarr server name ([b5988f9](https://github.com/sct/overseerr/commit/b5988f9a5ff274e97f208c2726abe76c22c858ee))
* **frontend:** only show alpha notice to admins ([ff61895](https://github.com/sct/overseerr/commit/ff618956b5d9cf933d867ea979b612c3d8a6f30b))
* add support for ssl when connecting to plex ([3ba09d0](https://github.com/sct/overseerr/commit/3ba09d07eb0367c41603cd55e7ff41c66fb641c4)), closes [#275](https://github.com/sct/overseerr/issues/275)
* **services:** improve logging for when Radarr movie already exists ([#285](https://github.com/sct/overseerr/issues/285)) ([f998873](https://github.com/sct/overseerr/commit/f998873fc5669a547901f2733c9c785d744d27ca)), closes [#260](https://github.com/sct/overseerr/issues/260)
### Features
* **lang:** add i18n strings for new about page ([900827b](https://github.com/sct/overseerr/commit/900827be97845688e4bea72a8c5d9611a3e9d069))
* about page initial version ([3f2a04c](https://github.com/sct/overseerr/commit/3f2a04c881bf06b73a952181fa463af84454b0dd))
# [1.4.0](https://github.com/sct/overseerr/compare/v1.3.2...v1.4.0) (2020-12-15)
### Bug Fixes
* changing parameter name to use correct 'port' [#276](https://github.com/sct/overseerr/issues/276) ([#277](https://github.com/sct/overseerr/issues/277)) ([6d08b10](https://github.com/sct/overseerr/commit/6d08b108200177ca3068c852e60a0df75ce2232a))
* **services:** include radarr/sonarr baseUrl when adding media ([78af1a3](https://github.com/sct/overseerr/commit/78af1a3e6d00a5645a05e7bf3cf56a59439b6cc9))
### Features
* **lang:** Translations update from Weblate ([#240](https://github.com/sct/overseerr/issues/240)) ([e17c637](https://github.com/sct/overseerr/commit/e17c63748362b6a480693e003ef5eec614dcec43))
## [1.3.2](https://github.com/sct/overseerr/compare/v1.3.1...v1.3.2) (2020-12-14)
### Bug Fixes
* **frontend:** convert plex port to a number before posting to the api ([8cb05c4](https://github.com/sct/overseerr/commit/8cb05c413a15a4b74e37ece5e24367d115995b32))
* **frontend:** converts email smtp port to a number before posting to the api ([2098a2d](https://github.com/sct/overseerr/commit/2098a2d3d2981fd2ae54392aec3ef81327f2858e)), closes [#251](https://github.com/sct/overseerr/issues/251)
* **frontend:** encode special characters in search input to prevent crashing router ([15013d6](https://github.com/sct/overseerr/commit/15013d6c5dbff15704c7c30d261d68a265e7f2d7)), closes [#252](https://github.com/sct/overseerr/issues/252)
* **plex sync:** catch errors that occur during processMovie ([edbbccf](https://github.com/sct/overseerr/commit/edbbccf3ae623430294f1a5c3fd2728dbd42e555)), closes [#244](https://github.com/sct/overseerr/issues/244) [#246](https://github.com/sct/overseerr/issues/246) [#250](https://github.com/sct/overseerr/issues/250)
* **services:** improve logging for adding movies to Radarr ([6c1ee83](https://github.com/sct/overseerr/commit/6c1ee830a183f89bb1fe96a181a7d61684e23b22))
* **services:** radarr/sonarr will use the correct default server ([0658b79](https://github.com/sct/overseerr/commit/0658b7943e1ab25816db9da34d4c9ea808d9203d))
## [1.3.1](https://github.com/sct/overseerr/compare/v1.3.0...v1.3.1) (2020-12-14)
### Bug Fixes
* **frontend:** also convert activeProfileId to a number for radarr/sonarr submissions ([7bf924f](https://github.com/sct/overseerr/commit/7bf924f7e94a0e0834f41b4ec067ed277c652766))
* **frontend:** also convert ports to numbers when saving radarr/sonarr servers ([c53dc3b](https://github.com/sct/overseerr/commit/c53dc3b15da522c6e6ab76bbc9d15008a8a9fb9d))
* **frontend:** new radarr/sonarr ports will be converted to a number before posting ([92c9001](https://github.com/sct/overseerr/commit/92c9001c9d1f2cbd272a5897ea1157d2cadbce2d))
# [1.3.0](https://github.com/sct/overseerr/compare/v1.2.0...v1.3.0) (2020-12-14)
### Bug Fixes
* **api:** correctly generate clientId on first startup ([5f09e83](https://github.com/sct/overseerr/commit/5f09e83ed870336638d3e9d94fcf55ead928e737))
### Features
* **frontend:** add full cast page for movies and series ([051f1b3](https://github.com/sct/overseerr/commit/051f1b3e899bf749e632743e5c8d45a02b621998))
* **lang:** translated using Weblate (Dutch) ([1ab3a4b](https://github.com/sct/overseerr/commit/1ab3a4b80a081d7e4a201f1290cd270ed5b38ac7))
* **lang:** translated using Weblate (English) ([0949c9b](https://github.com/sct/overseerr/commit/0949c9b334b3a4b6c342517a157a9e2b7596f2f0))
* **lang:** translated using Weblate (French) ([f943701](https://github.com/sct/overseerr/commit/f943701e13c7f0de5a711302597858cc898b16e2))
* **lang:** translated using Weblate (French) ([30d04ce](https://github.com/sct/overseerr/commit/30d04ce35adc21070cce37ab10384154afda191b))
* **lang:** translated using Weblate (German) ([7bf9add](https://github.com/sct/overseerr/commit/7bf9addd13a707aac23b64ef3f1733e491d40a4e))
* **lang:** translated using Weblate (German) ([b6e60a4](https://github.com/sct/overseerr/commit/b6e60a412b30907aea751a4cf1ce0cc8230f9814))
* **lang:** translated using Weblate (Japanese) ([08e968f](https://github.com/sct/overseerr/commit/08e968fd0097ec7b2a65de064ed5b07e7c49ef39))
* **lang:** translated using Weblate (Norwegian Bokmål) ([83efb0e](https://github.com/sct/overseerr/commit/83efb0e3d4d96b6a2d2ebdd85d36c9d78c1717b2))
* **lang:** translated using Weblate (Russian) ([0d8e0d0](https://github.com/sct/overseerr/commit/0d8e0d0352f72fdb65ee8f054371eae08c39fe33))
# [1.2.0](https://github.com/sct/overseerr/compare/v1.1.0...v1.2.0) (2020-12-11)

View File

@@ -33,7 +33,7 @@ You can also run the development environment in [Docker](https://www.docker.com/
- PRs with commits not following this standard will not be merged.
- Please make meaningful commits, or squash them
- Always rebase your commit to the latest `develop` branch. Do not merge develop into your branch.
- It is your responsbility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch.
- It is your responsibility to keep your branch up to date. It will not be merged unless its 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 codebase. It should auto run with a git hook, but its recommended to have a Prettier extension installed in your editor and have it format on save.

View File

@@ -12,6 +12,9 @@ RUN yarn cache clean
FROM node:12.18-alpine
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app
WORKDIR /app
@@ -21,3 +24,5 @@ COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
CMD yarn start
EXPOSE 5055

View File

@@ -15,9 +15,12 @@
</a>
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-16-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
**Overseerr** is a libre software tool for managing requests for your media library. It integrates with existing services such as **Sonarr** and **Radarr**!
**Overseerr** is a free and open source software application for managing requests for your media library. It integrates with your existing services such as **Sonarr**, **Radarr** and **Plex**!
## Current Features
@@ -41,7 +44,7 @@
- More notification types (Slack/Telegram/etc.).
- Issues system. This will allow users to report issues with content on your media server.
- Local user system (for those who don't use Plex).
- Compatiblity APIs (to work with existing tools in your system).
- Compatibility APIs (to work with existing tools in your system).
## Running Overseerr
@@ -51,10 +54,10 @@ Currently, Overseerr is only distributed through Docker images. If you have Dock
docker run -d \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
-p 5055:3000 \
-v /path/to/appdata/config:/config \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseer
sctx/overseerr
```
After running Overseerr for the first time, configure it by visiting the web UI at http://[address]:5055 and completing the setup steps.
@@ -73,7 +76,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
## API Documentation
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:3000/api-docs
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:5055/api-docs
## Community
@@ -83,12 +86,43 @@ If you would like to chat with community members you can join the [Overseerr Dis
Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Overseerr community channels.
## Contributors
<a href="https://github.com/sct/overseerr/graphs/contributors">
<img src="https://contributors-img.web.app/image?repo=sct/overseerr" />
</a>
## Contributing
You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://sct.dev"><img src="https://avatars1.githubusercontent.com/u/234213?v=4" width="100px;" alt=""/><br /><sub><b>sct</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sct" title="Code">💻</a> <a href="#design-sct" title="Design">🎨</a> <a href="#ideas-sct" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/azoitos"><img src="https://avatars2.githubusercontent.com/u/26529049?v=4" width="100px;" alt=""/><br /><sub><b>Alex Zoitos</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=azoitos" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/OwsleyJr"><img src="https://avatars3.githubusercontent.com/u/8635678?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Cohen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Ahreluth"><img src="https://avatars2.githubusercontent.com/u/75682440?v=4" width="100px;" alt=""/><br /><sub><b>Ahreluth</b></sub></a><br /><a href="#translation-Ahreluth" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/KovalevArtem"><img src="https://avatars0.githubusercontent.com/u/36500228?v=4" width="100px;" alt=""/><br /><sub><b>KovalevArtem</b></sub></a><br /><a href="#translation-KovalevArtem" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/GiyomuWeb"><img src="https://avatars0.githubusercontent.com/u/62489209?v=4" width="100px;" alt=""/><br /><sub><b>GiyomuWeb</b></sub></a><br /><a href="#translation-GiyomuWeb" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/angrycuban13"><img src="https://avatars3.githubusercontent.com/u/39564898?v=4" width="100px;" alt=""/><br /><sub><b>Angry Cuban</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=angrycuban13" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4" width="100px;" alt=""/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4" width="100px;" alt=""/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/saltydk"><img src="https://avatars1.githubusercontent.com/u/6587950?v=4" width="100px;" alt=""/><br /><sub><b>salty</b></sub></a><br /><a href="#infra-saltydk" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/Shutruk"><img src="https://avatars2.githubusercontent.com/u/9198633?v=4" width="100px;" alt=""/><br /><sub><b>Shutruk</b></sub></a><br /><a href="#translation-Shutruk" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/krystiancharubin"><img src="https://avatars2.githubusercontent.com/u/17775600?v=4" width="100px;" alt=""/><br /><sub><b>Krystian Charubin</b></sub></a><br /><a href="#design-krystiancharubin" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/kieron"><img src="https://avatars2.githubusercontent.com/u/8655212?v=4" width="100px;" alt=""/><br /><sub><b>Kieron Boswell</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=kieron" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/samwiseg0"><img src="https://avatars1.githubusercontent.com/u/2241731?v=4" width="100px;" alt=""/><br /><sub><b>samwiseg0</b></sub></a><br /><a href="#question-samwiseg0" title="Answering Questions">💬</a> <a href="#infra-samwiseg0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ecelebi29"><img src="https://avatars2.githubusercontent.com/u/8337120?v=4" width="100px;" alt=""/><br /><sub><b>ecelebi29</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mmozeiko"><img src="https://avatars3.githubusercontent.com/u/1665010?v=4" width="100px;" alt=""/><br /><sub><b>Mārtiņš Možeiko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=mmozeiko" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

View File

@@ -5,7 +5,7 @@ services:
context: .
dockerfile: Dockerfile.local
ports:
- 3000:3000
- 5055:5055
volumes:
- .:/app:rw,cached
- /app/node_modules

View File

@@ -5,6 +5,7 @@ const devConfig = {
logging: false,
entities: ['server/entity/**/*.ts'],
migrations: ['server/migration/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
cli: {
entitiesDir: 'server/entity',
migrationsDir: 'server/migration',
@@ -19,6 +20,7 @@ const prodConfig = {
entities: ['dist/entity/**/*.js'],
migrations: ['dist/migration/**/*.js'],
migrationsRun: true,
subscribers: ['dist/subscriber/**/*.js'],
cli: {
entitiesDir: 'dist/entity',
migrationsDir: 'dist/migration',

View File

@@ -436,12 +436,7 @@ components:
spokenLanguages:
type: array
items:
type: object
properties:
iso_639_1:
type: string
name:
type: string
$ref: '#/components/schemas/SpokenLanguage'
status:
type: string
tagline:
@@ -592,6 +587,10 @@ components:
type: array
items:
$ref: '#/components/schemas/ProductionCompany'
spokenLanguages:
type: array
items:
$ref: '#/components/schemas/SpokenLanguage'
seasons:
type: array
items:
@@ -617,6 +616,10 @@ components:
$ref: '#/components/schemas/Crew'
externalIds:
$ref: '#/components/schemas/ExternalIds'
keywords:
type: array
items:
$ref: '#/components/schemas/Keyword'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
MediaRequest:
@@ -643,7 +646,6 @@ components:
readOnly: true
requestedBy:
$ref: '#/components/schemas/User'
readOnly: true
modifiedBy:
anyOf:
- $ref: '#/components/schemas/User'
@@ -962,17 +964,43 @@ components:
type: string
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
Keyword:
type: object
properties:
id:
type: number
example: 1
name:
type: string
example: 'anime'
SpokenLanguage:
type: object
properties:
englishName:
type: string
example: 'English'
nullable: true
iso_639_1:
type: string
example: 'en'
name:
type: string
example: 'English'
securitySchemes:
cookieAuth:
type: apiKey
name: connect.sid
in: cookie
apiKey:
type: apiKey
in: header
name: X-Api-Key
paths:
/settings/main:
get:
summary: Returns main settings
description: Retreives all main settings in JSON format
description: Retrieves all main settings in JSON format
tags:
- settings
responses:
@@ -1000,6 +1028,19 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MainSettings'
/settings/main/regenerate:
get:
summary: Returns main settings with newly generated API Key
description: Retreives all main settings in JSON format with new API Key
tags:
- settings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MainSettings'
/settings/plex:
get:
summary: Returns plex settings
@@ -1137,7 +1178,7 @@ paths:
/settings/radarr/test:
post:
summary: Test radarr configuration
description: Test if the provided Radarr congifuration values are valid. Returns profiles and root folders on success
description: Test if the provided Radarr configuration values are valid. Returns profiles and root folders on success
tags:
- settings
requestBody:
@@ -1281,7 +1322,7 @@ paths:
/settings/sonarr/test:
post:
summary: Test Sonarr configuration
description: Test if the provided Sonarr congifuration values are valid. Returns profiles and root folders on success
description: Test if the provided Sonarr configuration values are valid. Returns profiles and root folders on success
tags:
- settings
requestBody:
@@ -1384,7 +1425,7 @@ paths:
/settings/initialize:
get:
summary: Set the application as initialized
description: Sets the app as initalized and allows the user to navigate to pages other than the setup page
description: Sets the app as initialized and allows the user to navigate to pages other than the setup page
tags:
- settings
responses:
@@ -1478,6 +1519,29 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DiscordSettings'
/settings/about:
get:
summary: Return current about stats
description: Returns current server stats in JSON format
tags:
- settings
responses:
'200':
description: Returned about settings
content:
application/json:
schema:
type: object
properties:
version:
type: string
example: '1.0.0'
totalRequests:
type: number
example: 100
totalMediaItems:
type: number
example: 100
/auth/me:
get:
summary: Returns the currently logged in user
@@ -1844,6 +1908,51 @@ paths:
- $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult'
- $ref: '#/components/schemas/PersonResult'
/discover/keyword/{keywordId}/movies:
get:
summary: Request list of movies from keyword
description: Returns list of movies based on provided keyword ID in JSON format
tags:
- search
parameters:
- in: path
name: keywordId
required: true
schema:
type: number
example: 207317
- in: query
name: page
schema:
type: number
example: 1
default: 1
- in: query
name: language
schema:
type: string
example: en
responses:
'200':
description: List of movies
content:
application/json:
schema:
type: object
properties:
page:
type: number
example: 1
totalPages:
type: number
example: 20
totalResults:
type: number
example: 200
results:
type: array
items:
$ref: '#/components/schemas/MovieResult'
/request:
get:
summary: Get all requests
@@ -2463,3 +2572,4 @@ paths:
security:
- cookieAuth: []
- apiKey: []

View File

@@ -1,6 +1,6 @@
{
"name": "overseerr",
"version": "1.2.0",
"version": "1.9.1",
"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",
@@ -9,8 +9,9 @@
"build": "yarn build:next && yarn build:server",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
"start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en,ja,fr,nb_NO,de,ru,nl -o src/i18n/locale -d en --flat true --overwriteDefault false './src/**/!(*.test).{ts,tsx}'",
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault false './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 ."
},
@@ -52,7 +53,7 @@
"winston": "^3.3.3",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.32.6"
"yup": "^0.32.8"
},
"devDependencies": {
"@babel/cli": "^7.12.8",
@@ -170,7 +171,8 @@
"prepareCmd": "docker build -t sctx/overseerr ."
}
],
"semantic-release-docker"
"semantic-release-docker",
"@semantic-release/github"
],
"branches": [
"master"

View File

@@ -1 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#1e2937","display":"standalone"}

View File

@@ -4,10 +4,12 @@ import { getSettings } from '../lib/settings';
export interface PlexLibraryItem {
ratingKey: string;
parentRatingKey?: string;
grandparentRatingKey?: string;
title: string;
guid: string;
parentGuid?: string;
type: 'movie' | 'show' | 'season';
grandparentGuid?: string;
type: 'movie' | 'show' | 'season' | 'episode';
}
interface PlexLibraryResponse {
@@ -20,6 +22,7 @@ export interface PlexLibrary {
type: 'show' | 'movie';
key: string;
title: string;
agent: string;
}
interface PlexLibrariesResponse {
@@ -61,7 +64,8 @@ class PlexAPI {
this.plexClient = new NodePlexAPI({
hostname: settings.plex.ip,
post: settings.plex.port,
port: settings.plex.port,
https: settings.plex.useSsl,
token: plexToken,
authenticator: {
authenticate: (
@@ -119,9 +123,9 @@ class PlexAPI {
return response.MediaContainer.Metadata[0];
}
public async getRecentlyAdded(): Promise<PlexLibraryItem[]> {
public async getRecentlyAdded(id: string): Promise<PlexLibraryItem[]> {
const response = await this.plexClient.query<PlexLibraryResponse>(
'/library/recentlyAdded'
`/library/sections/${id}/recentlyAdded`
);
return response.MediaContainer.Metadata;

View File

@@ -1,4 +1,5 @@
import Axios, { AxiosInstance } from 'axios';
import logger from '../logger';
interface RadarrMovieOptions {
title: string;
@@ -75,11 +76,9 @@ class RadarrAPI {
}
};
public addMovie = async (
options: RadarrMovieOptions
): Promise<RadarrMovie> => {
public addMovie = async (options: RadarrMovieOptions): Promise<void> => {
try {
const response = await this.axios.post<RadarrMovie>(`/movie`, {
await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title,
qualityProfileId: options.qualityProfileId,
profileId: options.profileId,
@@ -93,10 +92,15 @@ class RadarrAPI {
searchForMovie: options.searchNow,
},
});
return response.data;
} catch (e) {
throw new Error(`[Radarr] Failed to add movie: ${e.message}`);
logger.error(
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
{
label: 'Radarr',
errorMessage: e.message,
options,
}
);
}
};

View File

@@ -6,7 +6,7 @@ interface SonarrSeason {
monitored: boolean;
}
interface SonarrSeries {
export interface SonarrSeries {
title: string;
sortTitle: string;
seasonCount: number;
@@ -33,7 +33,7 @@ interface SonarrSeries {
tvMazeId: number;
firstAired: string;
lastInfoSync?: string;
seriesType: string;
seriesType: 'standard' | 'daily' | 'anime';
cleanTitle: string;
imdbId: string;
titleSlug: string;
@@ -76,7 +76,9 @@ interface AddSeriesOptions {
title: string;
profileId: number;
seasons: number[];
seasonFolder: boolean;
rootFolderPath: string;
seriesType: SonarrSeries['seriesType'];
monitored?: boolean;
searchNow?: boolean;
}
@@ -149,8 +151,10 @@ class SonarrAPI {
monitored: false,
}))
),
seasonFolder: options.seasonFolder,
monitored: options.monitored,
rootFolderPath: options.rootFolderPath,
seriesType: options.seriesType,
addOptions: {
ignoreEpisodesWithFiles: true,
searchForMissingEpisodes: options.searchNow,
@@ -162,7 +166,7 @@ class SonarrAPI {
} catch (e) {
logger.error('Something went wrong adding a series to Sonarr', {
label: 'Sonarr API',
message: e.message,
errorMessage: e.message,
error: e,
});
throw new Error('Failed to add series');

View File

@@ -1,5 +1,7 @@
import axios, { AxiosInstance } from 'axios';
export const ANIME_KEYWORD_ID = 210024;
interface SearchOptions {
query: string;
page?: number;
@@ -258,6 +260,11 @@ export interface TmdbTvDetails {
name: string;
origin_country: string;
}[];
spoken_languages: {
english_name: string;
iso_639_1: string;
name: string;
}[];
seasons: TmdbTvSeasonResult[];
status: string;
type: string;
@@ -268,6 +275,14 @@ export interface TmdbTvDetails {
crew: TmdbCreditCrew[];
};
external_ids: TmdbExternalIds;
keywords: {
results: TmdbKeyword[];
};
}
export interface TmdbKeyword {
id: number;
name: string;
}
export interface TmdbPersonDetail {
@@ -437,7 +452,10 @@ class TheMovieDb {
}): Promise<TmdbTvDetails> => {
try {
const response = await this.axios.get<TmdbTvDetails>(`/tv/${tvId}`, {
params: { language, append_to_response: 'credits,external_ids' },
params: {
language,
append_to_response: 'credits,external_ids,keywords',
},
});
return response.data;
@@ -524,6 +542,32 @@ class TheMovieDb {
}
}
public async getMoviesByKeyword({
keywordId,
page = 1,
language = 'en-US',
}: {
keywordId: number;
page?: number;
language?: string;
}): Promise<TmdbSearchMovieResponse> {
try {
const response = await this.axios.get<TmdbSearchMovieResponse>(
`/keyword/${keywordId}/movies`,
{
params: {
page,
language,
},
}
);
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
}
}
public async getTvRecommendations({
tvId,
page = 1,

View File

@@ -8,14 +8,11 @@ import {
UpdateDateColumn,
getRepository,
In,
AfterUpdate,
} from 'typeorm';
import { MediaRequest } from './MediaRequest';
import { MediaStatus, MediaType } from '../constants/media';
import logger from '../logger';
import Season from './Season';
import notificationManager, { Notification } from '../lib/notifications';
import TheMovieDb from '../api/themoviedb';
@Entity()
class Media {
@@ -98,32 +95,6 @@ class Media {
constructor(init?: Partial<Media>) {
Object.assign(this, init);
}
@AfterUpdate()
private async _notifyAvailable() {
if (this.status === MediaStatus.AVAILABLE) {
if (this.mediaType === MediaType.MOVIE) {
const requestRepository = getRepository(MediaRequest);
const relatedRequests = await requestRepository.find({
where: { media: this },
});
if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ movieId: this.tmdbId });
relatedRequests.forEach((request) => {
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
notifyUser: request.requestedBy,
subject: movie.title,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
});
}
}
}
}
}
export default Media;

View File

@@ -15,11 +15,11 @@ import { User } from './User';
import Media from './Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import TheMovieDb from '../api/themoviedb';
import TheMovieDb, { ANIME_KEYWORD_ID } from '../api/themoviedb';
import RadarrAPI from '../api/radarr';
import logger from '../logger';
import SeasonRequest from './SeasonRequest';
import SonarrAPI from '../api/sonarr';
import SonarrAPI, { SonarrSeries } from '../api/sonarr';
import notificationManager, { Notification } from '../lib/notifications';
@Entity()
@@ -36,10 +36,18 @@ export class MediaRequest {
})
public media: Media;
@ManyToOne(() => User, (user) => user.requests, { eager: true })
@ManyToOne(() => User, (user) => user.requests, {
eager: true,
onDelete: 'CASCADE',
})
public requestedBy: User;
@ManyToOne(() => User, { nullable: true, cascade: true, eager: true })
@ManyToOne(() => User, {
nullable: true,
cascade: true,
eager: true,
onDelete: 'SET NULL',
})
public modifiedBy?: User;
@CreateDateColumn()
@@ -234,13 +242,24 @@ export class MediaRequest {
return;
}
const radarrSettings = settings.radarr.find(
(radarr) => radarr.isDefault && !radarr.is4k
);
if (!radarrSettings) {
logger.info(
'There is no default radarr configured. Did you set any of your Radarr servers as default?',
{ label: 'Media Request' }
);
return;
}
const tmdb = new TheMovieDb();
const radarrSettings = settings.radarr[0];
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}/api`,
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}/api`,
});
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
@@ -283,6 +302,18 @@ export class MediaRequest {
return;
}
const sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && !sonarr.is4k
);
if (!sonarrSettings) {
logger.info(
'There is no default sonarr configured. Did you set any of your Sonarr servers as default?',
{ label: 'Media Request' }
);
return;
}
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
@@ -293,12 +324,11 @@ export class MediaRequest {
}
const tmdb = new TheMovieDb();
const sonarrSettings = settings.sonarr[0];
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}/api`,
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
});
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
@@ -306,13 +336,32 @@ export class MediaRequest {
throw new Error('Series was missing tvdb id');
}
let seriesType: SonarrSeries['seriesType'] = 'standard';
// Change series type to anime if the anime keyword is present on tmdb
if (
series.keywords.results.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID
)
) {
seriesType = 'anime';
}
// Run this asynchronously so we don't wait for it on the UI side
sonarr.addSeries({
profileId: sonarrSettings.activeProfileId,
rootFolderPath: sonarrSettings.activeDirectory,
profileId:
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId,
rootFolderPath:
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
? sonarrSettings.activeAnimeDirectory
: sonarrSettings.activeDirectory,
title: series.name,
tvdbid: series.external_ids.tvdb_id,
seasons: this.seasons.map((season) => season.seasonNumber),
seasonFolder: sonarrSettings.enableSeasonFolders,
seriesType,
monitored: true,
searchNow: true,
});

View File

@@ -5,15 +5,9 @@ import {
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
AfterInsert,
AfterUpdate,
getRepository,
} from 'typeorm';
import { MediaStatus } from '../constants/media';
import Media from './Media';
import logger from '../logger';
import TheMovieDb from '../api/themoviedb';
import notificationManager, { Notification } from '../lib/notifications';
@Entity()
class Season {
@@ -38,60 +32,6 @@ class Season {
constructor(init?: Partial<Season>) {
Object.assign(this, init);
}
@AfterInsert()
@AfterUpdate()
private async _sendSeasonAvailableNotification() {
if (this.status === MediaStatus.AVAILABLE) {
try {
const lazyMedia = await this.media;
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOneOrFail({
where: { id: lazyMedia.id },
relations: ['requests'],
});
const availableSeasons = media.seasons.map(
(season) => season.seasonNumber
);
const request = media.requests.find(
(request) =>
// Check if the season is complete AND it contains the current season that was just marked available
request.seasons.every((season) =>
availableSeasons.includes(season.seasonNumber)
) &&
request.seasons.some(
(season) => season.seasonNumber === this.seasonNumber
)
);
if (request) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
subject: tv.name,
message: tv.overview,
notifyUser: request.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
extra: [
{
name: 'Seasons',
value: request.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
});
}
} catch (e) {
logger.error('Something went wrong sending season available notice', {
label: 'Notifications',
message: e.message,
});
}
}
}
}
export default Season;

View File

@@ -17,9 +17,11 @@ 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 { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
@@ -98,12 +100,21 @@ app
}
);
const port = Number(process.env.PORT) || 3000;
server.listen(port, () => {
logger.info(`Server ready on port ${port}`, {
label: 'SERVER',
const port = Number(process.env.PORT) || 5055;
const host = process.env.HOST;
if (host) {
server.listen(port, host, () => {
logger.info(`Server ready on ${host} port ${port}`, {
label: 'Server',
});
});
});
} else {
server.listen(port, () => {
logger.info(`Server ready on port ${port}`, {
label: 'Server',
});
});
}
})
.catch((err) => {
logger.error(err.stack);

View File

@@ -0,0 +1,5 @@
export interface SettingsAboutResponse {
version: string;
totalRequests: number;
totalMediaItems: number;
}

View File

@@ -7,8 +7,10 @@ import { MediaStatus, MediaType } from '../../constants/media';
import logger from '../../logger';
import { getSettings, Library } from '../../lib/settings';
import Season from '../../entity/Season';
import { uniqWith } from 'lodash';
const BUNDLE_SIZE = 10;
const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000;
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
@@ -51,64 +53,81 @@ class JobPlexSync {
private async processMovie(plexitem: PlexLibraryItem) {
const mediaRepository = getRepository(Media);
if (plexitem.guid.match(plexRegex)) {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
const newMedia = new Media();
try {
if (plexitem.guid.match(plexRegex)) {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
const newMedia = new Media();
metadata.Guid.forEach((ref) => {
if (ref.id.match(imdbRegex)) {
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
} else if (ref.id.match(tmdbRegex)) {
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
newMedia.tmdbId = Number(tmdbMatch);
if (!metadata.Guid) {
logger.debug('No Guid metadata for this title. Skipping', {
label: 'Plex Sync',
ratingKey: plexitem.ratingKey,
});
return;
}
});
const existing = await this.getExisting(newMedia.tmdbId);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${metadata.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
mediaRepository.save(existing);
this.log(
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
'info'
);
} else {
newMedia.status = MediaStatus.AVAILABLE;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${plexitem.title}`);
}
} else {
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
if (matchedid?.[1]) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
imdbId: matchedid[1],
metadata.Guid.forEach((ref) => {
if (ref.id.match(imdbRegex)) {
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
} else if (ref.id.match(tmdbRegex)) {
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
newMedia.tmdbId = Number(tmdbMatch);
}
});
const existing = await this.getExisting(tmdbMovie.id);
const existing = await this.getExisting(newMedia.tmdbId);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${plexitem.title}`);
this.log(`Title exists and is already available ${metadata.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
await mediaRepository.save(existing);
mediaRepository.save(existing);
this.log(
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
'info'
);
} else if (tmdbMovie) {
const newMedia = new Media();
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
newMedia.tmdbId = tmdbMovie.id;
} else {
newMedia.status = MediaStatus.AVAILABLE;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${tmdbMovie.title}`);
this.log(`Saved ${plexitem.title}`);
}
} else {
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
if (matchedid?.[1]) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
imdbId: matchedid[1],
});
const existing = await this.getExisting(tmdbMovie.id);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${plexitem.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
await mediaRepository.save(existing);
this.log(
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
'info'
);
} else if (tmdbMovie) {
const newMedia = new Media();
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
newMedia.tmdbId = tmdbMovie.id;
newMedia.status = MediaStatus.AVAILABLE;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${tmdbMovie.title}`);
}
}
}
} catch (e) {
this.log(
`Failed to process plex item. ratingKey: ${
plexitem.parentRatingKey ?? plexitem.ratingKey
}`,
'error'
);
}
}
@@ -119,7 +138,9 @@ class JobPlexSync {
try {
const metadata = await this.plexClient.getMetadata(
plexitem.parentRatingKey ?? plexitem.ratingKey,
plexitem.grandparentRatingKey ??
plexitem.parentRatingKey ??
plexitem.ratingKey,
{ includeChildren: true }
);
if (metadata.guid.match(tvdbRegex)) {
@@ -222,9 +243,15 @@ class JobPlexSync {
} catch (e) {
this.log(
`Failed to process plex item. ratingKey: ${
plexitem.parentRatingKey ?? plexitem.ratingKey
plexitem.grandparentRatingKey ??
plexitem.parentRatingKey ??
plexitem.ratingKey
}`,
'error'
'error',
{
errorMessage: e.message,
plexitem,
}
);
}
}
@@ -234,7 +261,11 @@ class JobPlexSync {
slicedItems.map(async (plexitem) => {
if (plexitem.type === 'movie') {
await this.processMovie(plexitem);
} else if (plexitem.type === 'show') {
} else if (
plexitem.type === 'show' ||
plexitem.type === 'episode' ||
plexitem.type === 'season'
) {
await this.processShow(plexitem);
}
})
@@ -260,16 +291,17 @@ class JobPlexSync {
end: end + BUNDLE_SIZE,
});
resolve();
}, 5000)
}, UPDATE_RATE)
);
}
}
private log(
message: string,
level: 'info' | 'error' | 'debug' = 'debug'
level: 'info' | 'error' | 'debug' = 'debug',
optional?: Record<string, unknown>
): void {
logger[level](message, { label: 'Plex Sync' });
logger[level](message, { label: 'Plex Sync', ...optional });
}
public async run(): Promise<void> {
@@ -283,20 +315,40 @@ class JobPlexSync {
});
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
if (this.isRecentOnly) {
this.currentLibrary = {
id: '0',
name: 'Recently Added',
enabled: true,
};
this.log(`Beginning to process recently added`, 'info');
this.items = await this.plexClient.getRecentlyAdded();
await this.loop();
} else {
this.libraries = settings.plex.libraries.filter(
(library) => library.enabled
);
this.libraries = settings.plex.libraries.filter(
(library) => library.enabled
);
if (this.isRecentOnly) {
for (const library of this.libraries) {
this.currentLibrary = library;
this.log(
`Beginning to process recently added for library: ${library.name}`,
'info'
);
const libraryItems = await this.plexClient.getRecentlyAdded(
library.id
);
// Bundle items up by rating keys
this.items = uniqWith(libraryItems, (mediaA, mediaB) => {
if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) {
return (
mediaA.grandparentRatingKey === mediaB.grandparentRatingKey
);
}
if (mediaA.parentRatingKey && mediaB.parentRatingKey) {
return mediaA.parentRatingKey === mediaB.parentRatingKey;
}
return mediaA.ratingKey === mediaB.ratingKey;
});
await this.loop();
}
} else {
for (const library of this.libraries) {
this.currentLibrary = library;
this.log(`Beginning to process library: ${library.name}`, 'info');

View File

@@ -13,7 +13,7 @@ export const startJobs = (): void => {
// Run recently added plex sync every 5 minutes
scheduledJobs.push({
name: 'Plex Recently Added Sync',
job: schedule.scheduleJob('0 */10 * * * *', () => {
job: schedule.scheduleJob('0 */5 * * * *', () => {
logger.info('Starting scheduled job: Plex Recently Added Sync', {
label: 'Jobs',
});
@@ -23,7 +23,7 @@ export const startJobs = (): void => {
// Run full plex sync every 6 hours
scheduledJobs.push({
name: 'Plex Full Library Sync',
job: schedule.scheduleJob('0 0 */6 * * *', () => {
job: schedule.scheduleJob('0 0 3 * * *', () => {
logger.info('Starting scheduled job: Plex Full Sync', { label: 'Jobs' });
jobPlexFullSync.run();
}),

View File

@@ -29,10 +29,13 @@ class EmailAgent implements NotificationAgent {
host: emailSettings.smtpHost,
port: emailSettings.smtpPort,
secure: emailSettings.secure,
auth: {
user: emailSettings.authUser,
pass: emailSettings.authPass,
},
auth:
emailSettings.authUser && emailSettings.authPass
? {
user: emailSettings.authUser,
pass: emailSettings.authPass,
}
: undefined,
});
}
@@ -135,7 +138,7 @@ class EmailAgent implements NotificationAgent {
to: payload.notifyUser.email,
},
locals: {
body: 'Your requsested media is now available!',
body: 'Your requested media is now available!',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),

View File

@@ -14,6 +14,7 @@ export interface PlexSettings {
machineId?: string;
ip: string;
port: number;
useSsl?: boolean;
libraries: Library[];
}
@@ -100,7 +101,7 @@ class Settings {
constructor(initialSettings?: AllSettings) {
this.data = {
clientId: '',
clientId: uuidv4(),
main: {
apiKey: '',
applicationUrl: '',
@@ -109,6 +110,7 @@ class Settings {
name: '',
ip: '127.0.0.1',
port: 32400,
useSsl: false,
libraries: [],
},
radarr: [],
@@ -211,7 +213,7 @@ class Settings {
}
private generateApiKey(): string {
return Buffer.from(`${Date.now()}${this.clientId}`).toString('base64');
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
}
/**
@@ -222,10 +224,10 @@ class Settings {
* @param overrideSettings If passed in, will override all existing settings with these
* values
*/
public load(overrideSettings?: AllSettings): AllSettings {
public load(overrideSettings?: AllSettings): Settings {
if (overrideSettings) {
this.data = overrideSettings;
return this.data;
return this;
}
if (!fs.existsSync(SETTINGS_PATH)) {
@@ -237,7 +239,7 @@ class Settings {
this.data = merge(this.data, JSON.parse(data));
this.save();
}
return this.data;
return this;
}
public save(): void {

View File

@@ -1,9 +1,25 @@
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings';
export const checkUser: Middleware = async (req, _res, next) => {
if (req.session?.userId) {
const settings = getSettings();
if (req.header('X-API-Key') === settings.main.apiKey) {
const userRepository = getRepository(User);
let userId = 1; // Work on original administrator account
// If a User ID is provided, we will act on that users behalf
if (req.header('X-API-User')) {
userId = Number(req.header('X-API-User'));
}
const user = await userRepository.findOne({ where: { id: userId } });
if (user) {
req.user = user;
}
} else if (req.session?.userId) {
const userRepository = getRepository(User);
const user = await userRepository.findOne({
@@ -24,7 +40,7 @@ export const isAuthenticated = (
if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
res.status(403).json({
status: 403,
error: 'You do not have permisson to access this endpoint',
error: 'You do not have permission to access this endpoint',
});
} else {
next();

View File

@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm';
export class DropImdbIdConstraint1607928251245 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropUniqueConstraint(
'media',
'UQ_7ff2d11f6a83cb52386eaebe74b'
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createUniqueConstraint(
'media',
new TableUnique({
name: 'UQ_7ff2d11f6a83cb52386eaebe74b',
columnNames: ['imdbId'],
})
);
}
}

View File

@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserRequestDeleteCascades1608219049304
implements MigrationInterface {
name = 'AddUserRequestDeleteCascades1608219049304';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
}
}

View File

@@ -7,6 +7,7 @@ import {
mapCrew,
ExternalIds,
mapExternalIds,
Keyword,
} from './common';
import {
TmdbTvEpisodeResult,
@@ -45,6 +46,12 @@ export interface SeasonWithEpisodes extends Season {
externalIds: ExternalIds;
}
interface SpokenLanguage {
englishName: string;
iso_639_1: string;
name: string;
}
export interface TvDetails {
id: number;
backdropPath?: string;
@@ -56,7 +63,7 @@ export interface TvDetails {
profilePath?: string;
}[];
episodeRunTime: number[];
firstAirDate: string;
firstAirDate?: string;
genres: Genre[];
homepage: string;
inProduction: boolean;
@@ -74,6 +81,7 @@ export interface TvDetails {
overview: string;
popularity: number;
productionCompanies: ProductionCompany[];
spokenLanguages: SpokenLanguage[];
seasons: Season[];
status: string;
type: string;
@@ -84,6 +92,7 @@ export interface TvDetails {
crew: Crew[];
};
externalIds: ExternalIds;
keywords: Keyword[];
mediaInfo?: Media;
}
@@ -161,6 +170,11 @@ export const mapTvDetails = (
originCountry: company.origin_country,
logoPath: company.logo_path,
})),
spokenLanguages: show.spoken_languages.map((language) => ({
englishName: language.english_name,
iso_639_1: language.iso_639_1,
name: language.name,
})),
seasons: show.seasons.map(mapSeasonResult),
status: show.status,
type: show.type,
@@ -179,5 +193,9 @@ export const mapTvDetails = (
crew: show.credits.crew.map(mapCrew),
},
externalIds: mapExternalIds(show.external_ids),
keywords: show.keywords.results.map((keyword) => ({
id: keyword.id,
name: keyword.name,
})),
mediaInfo: media,
});

View File

@@ -11,6 +11,11 @@ export interface ProductionCompany {
name: string;
}
export interface Keyword {
id: number;
name: string;
}
export interface Genre {
id: number;
name: string;

View File

@@ -14,7 +14,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
return res.status(500).json({
status: 500,
error:
'Requsted user endpoint withuot valid authenticated user in session',
'Requested user endpoint without valid authenticated user in session',
});
}
const user = await userRepository.findOneOrFail({
@@ -24,7 +24,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
return res.status(200).json(user.filter());
});
authRoutes.post('/login', async (req, res) => {
authRoutes.post('/login', async (req, res, next) => {
const userRepository = getRepository(User);
const body = req.body as { authToken?: string };
@@ -86,6 +86,22 @@ authRoutes.post('/login', async (req, res) => {
avatar: account.thumb,
});
await userRepository.save(user);
} else {
logger.info(
'Failed login attempt from user without access to plex server',
{
label: 'Auth',
account: {
...account,
authentication_token: '__REDACTED__',
authToken: '__REDACTED__',
},
}
);
return next({
status: 403,
message: 'You do not have access to this Plex server',
});
}
}
@@ -97,9 +113,10 @@ authRoutes.post('/login', async (req, res) => {
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
logger.error(e.message, { label: 'Auth' });
res
.status(500)
.json({ error: 'Something went wrong. Is your auth token valid?' });
return next({
status: 500,
message: 'Something went wrong. Is your auth token valid?',
});
}
});

View File

@@ -121,4 +121,36 @@ discoverRoutes.get('/trending', async (req, res) => {
});
});
discoverRoutes.get<{ keywordId: string }>(
'/keyword/:keywordId/movies',
async (req, res) => {
const tmdb = new TheMovieDb();
const data = await tmdb.getMoviesByKeyword({
keywordId: Number(req.params.keywordId),
page: Number(req.query.page),
language: req.query.language as string,
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
}
);
export default discoverRoutes;

View File

@@ -17,19 +17,33 @@ import { scheduledJobs } from '../job/schedule';
import { Permission } from '../lib/permissions';
import { isAuthenticated } from '../middleware/auth';
import { merge } from 'lodash';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
import { getAppVersion } from '../utils/appVersion';
const settingsRoutes = Router();
settingsRoutes.get('/main', (req, res) => {
const settings = getSettings();
if (!req.user?.hasPermission(Permission.ADMIN)) {
return res.status(200).json({
applicationUrl: settings.main.applicationUrl,
} as Partial<MainSettings>);
const filteredMainSettings = (
user: User,
main: MainSettings
): Partial<MainSettings> => {
if (!user?.hasPermission(Permission.ADMIN)) {
return {
applicationUrl: main.applicationUrl,
};
}
res.status(200).json(settings.main);
return main;
};
settingsRoutes.get('/main', (req, res, next) => {
const settings = getSettings();
if (!req.user) {
return next({ status: 500, message: 'User missing from request' });
}
res.status(200).json(filteredMainSettings(req.user, settings.main));
});
settingsRoutes.post('/main', (req, res) => {
@@ -41,6 +55,18 @@ settingsRoutes.post('/main', (req, res) => {
return res.status(200).json(settings.main);
});
settingsRoutes.get('/main/regenerate', (req, res, next) => {
const settings = getSettings();
const main = settings.regenerateApiKey();
if (!req.user) {
return next({ status: 500, message: 'User missing from request' });
}
return res.status(200).json(filteredMainSettings(req.user, main));
});
settingsRoutes.get('/plex', (_req, res) => {
const settings = getSettings();
@@ -91,17 +117,22 @@ settingsRoutes.get('/plex/library', async (req, res) => {
const libraries = await plexapi.getLibraries();
const newLibraries: Library[] = libraries.map((library) => {
const existing = settings.plex.libraries.find(
(l) => l.id === library.key
);
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,
};
});
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
};
});
settings.plex.libraries = newLibraries;
}
@@ -431,4 +462,18 @@ settingsRoutes.post('/notifications/email', (req, res) => {
res.status(200).json(settings.notifications.agents.email);
});
settingsRoutes.get('/about', async (req, res) => {
const mediaRepository = getRepository(Media);
const mediaRequestRepository = getRepository(MediaRequest);
const totalMediaItems = await mediaRepository.count();
const totalRequests = await mediaRequestRepository.count();
return res.status(200).json({
version: getAppVersion(),
totalMediaItems,
totalRequests,
});
});
export default settingsRoutes;

View File

@@ -4,6 +4,7 @@ import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
import { mapTvResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
const tvRoutes = Router();
@@ -19,6 +20,10 @@ tvRoutes.get('/:id', async (req, res, next) => {
return res.status(200).json(mapTvDetails(tv, media));
} catch (e) {
logger.error('Failed to get tv show', {
label: 'API',
errorMessage: e.message,
});
return next({ status: 404, message: 'TV Show does not exist' });
}
});

View File

@@ -1,7 +1,9 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import { MediaRequest } from '../entity/MediaRequest';
import { User } from '../entity/User';
import { hasPermission, Permission } from '../lib/permissions';
import logger from '../logger';
const router = Router();
@@ -94,13 +96,49 @@ router.delete<{ id: string }>('/:id', async (req, res, next) => {
try {
const userRepository = getRepository(User);
const user = await userRepository.findOneOrFail({
const user = await userRepository.findOne({
where: { id: Number(req.params.id) },
relations: ['requests'],
});
if (!user) {
return next({ status: 404, message: 'User not found' });
}
if (user.id === 1) {
return next({ status: 405, message: 'This account cannot be deleted.' });
}
if (user.hasPermission(Permission.ADMIN)) {
return next({
status: 405,
message: 'You cannot delete users with administrative privileges.',
});
}
const requestRepository = getRepository(MediaRequest);
/**
* Requests are usually deleted through a cascade constraint. Those however, do
* not trigger the removal event so listeners to not run and the parent Media
* will not be updated back to unknown for titles that were still pending. So
* we manually remove all requests from the user here so the parent media's
* properly reflect the change.
*/
await requestRepository.remove(user.requests);
await userRepository.delete(user.id);
return res.status(200).json(user.filter());
} catch (e) {
next({ status: 404, message: 'User not found' });
logger.error('Something went wrong deleting a user', {
label: 'API',
userId: req.params.id,
errorMessage: e.message,
});
return next({
status: 500,
message: 'Something went wrong deleting the user',
});
}
});

View File

@@ -0,0 +1,112 @@
import {
EntitySubscriberInterface,
EventSubscriber,
getRepository,
UpdateEvent,
} from 'typeorm';
import TheMovieDb from '../api/themoviedb';
import { MediaStatus, MediaType } from '../constants/media';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
import notificationManager, { Notification } from '../lib/notifications';
@EventSubscriber()
export class MediaSubscriber implements EntitySubscriberInterface {
private async notifyAvailableMovie(entity: Media) {
if (entity.status === MediaStatus.AVAILABLE) {
if (entity.mediaType === MediaType.MOVIE) {
const requestRepository = getRepository(MediaRequest);
const relatedRequests = await requestRepository.find({
where: { media: entity },
});
if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
relatedRequests.forEach((request) => {
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
notifyUser: request.requestedBy,
subject: movie.title,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
});
}
}
}
}
private async notifyAvailableSeries(entity: Media, dbEntity: Media) {
const newAvailableSeasons = entity.seasons
.filter((season) => season.status === MediaStatus.AVAILABLE)
.map((season) => season.seasonNumber);
const oldAvailableSeasons = dbEntity.seasons
.filter((season) => season.status === MediaStatus.AVAILABLE)
.map((season) => season.seasonNumber);
const changedSeasons = newAvailableSeasons.filter(
(seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
);
if (changedSeasons.length > 0) {
const tmdb = new TheMovieDb();
const requestRepository = getRepository(MediaRequest);
const processedSeasons: number[] = [];
for (const changedSeasonNumber of changedSeasons) {
const requests = await requestRepository.find({
where: { media: entity },
});
const request = requests.find(
(request) =>
// Check if the season is complete AND it contains the current season that was just marked available
request.seasons.every((season) =>
newAvailableSeasons.includes(season.seasonNumber)
) &&
request.seasons.some(
(season) => season.seasonNumber === changedSeasonNumber
)
);
if (request && !processedSeasons.includes(changedSeasonNumber)) {
processedSeasons.push(
...request.seasons.map((season) => season.seasonNumber)
);
const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
subject: tv.name,
message: tv.overview,
notifyUser: request.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
extra: [
{
name: 'Seasons',
value: request.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
});
}
}
}
}
public beforeUpdate(event: UpdateEvent<Media>): void {
if (
event.entity.mediaType === MediaType.MOVIE &&
event.entity.status === MediaStatus.AVAILABLE
) {
this.notifyAvailableMovie(event.entity);
}
if (
event.entity.mediaType === MediaType.TV &&
(event.entity.status === MediaStatus.AVAILABLE ||
event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
) {
this.notifyAvailableSeries(event.entity, event.databaseEntity);
}
}
}

View File

@@ -2,8 +2,9 @@ declare module 'plex-api' {
export default class PlexAPI {
constructor(intiialOptions: {
hostname: string;
post: number;
port: number;
token?: string;
https?: boolean;
authenticator: {
authenticate: (
_plexApi: PlexAPI,

View File

@@ -0,0 +1,12 @@
export const getAppVersion = (): string => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require('../../package.json');
let finalVersion = version;
if (version === '0.1.0') {
finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`;
}
return finalVersion;
};

View File

@@ -2,7 +2,7 @@ import React from 'react';
interface HeaderProps {
extraMargin?: number;
subtext?: string;
subtext?: React.ReactNode;
}
const Header: React.FC<HeaderProps> = ({

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { withProperties } from '../../../utils/typeHelpers';
interface ListItemProps {
title: string;
}
const ListItem: React.FC<ListItemProps> = ({ title, children }) => {
return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt className="text-sm font-medium text-gray-200">{title}</dt>
<dd className="mt-1 flex text-sm text-gray-400 sm:mt-0 sm:col-span-2">
<span className="flex-grow">{children}</span>
</dd>
</div>
);
};
interface ListProps {
title: string;
subTitle?: string;
}
const List: React.FC<ListProps> = ({ title, subTitle, children }) => {
return (
<>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-100">{title}</h3>
{subTitle && (
<p className="mt-1 max-w-2xl text-sm text-gray-300">{subTitle}</p>
)}
</div>
<div className="mt-5 border-t border-gray-800">
<dl className="divide-y divide-gray-800">{children}</dl>
</div>
</>
);
};
export default withProperties(List, { Item: ListItem });

View File

@@ -8,7 +8,7 @@ import { useIntl } from 'react-intl';
import globalMessages from '../../../i18n/globalMessages';
import Transition from '../../Transition';
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
interface ModalProps {
title?: string;
onCancel?: (e?: MouseEvent<HTMLElement>) => void;
onOk?: (e?: MouseEvent<HTMLButtonElement>) => void;
@@ -63,126 +63,122 @@ const Modal: React.FC<ModalProps> = ({
});
useLockBodyScroll(true, disableScrollLock);
return (
<>
{ReactDOM.createPortal(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
return ReactDOM.createPortal(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="fixed top-0 left-0 right-0 bottom-0 bg-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined;
}
}}
>
<Transition
enter="transition opacity-0 duration-300 transform scale-75"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={loading}
>
<div style={{ position: 'absolute' }}>
<LoadingSpinner />
</div>
</Transition>
<Transition
enter="transition opacity-0 duration-300 transform scale-75"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={!loading}
>
<div
className="fixed top-0 left-0 right-0 bottom-0 bg-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined;
}
}}
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
ref={modalRef}
>
<Transition
enter="transition opacity-0 duration-300 transform scale-75"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={loading}
>
<div style={{ position: 'absolute' }}>
<LoadingSpinner />
</div>
</Transition>
<Transition
enter="transition opacity-0 duration-300 transform scale-75"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={!loading}
>
<div
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
ref={modalRef}
>
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
)}
<div
className={`mt-3 text-center sm:mt-0 sm:text-left ${
iconSvg ? 'sm:ml-4' : 'mb-6'
}`}
>
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
id="modal-headline"
>
{title}
</h3>
)}
</div>
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
{children && (
<div className="mt-4 text-sm leading-5 text-gray-300">
{children}
</div>
)}
{(onCancel || onOk || onSecondary || onTertiary) && (
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
{typeof onOk === 'function' && (
<Button
buttonType={okButtonType}
onClick={onOk}
className="ml-3"
disabled={okDisabled}
>
{okText ? okText : 'Ok'}
</Button>
)}
{typeof onSecondary === 'function' && secondaryText && (
<Button
buttonType={secondaryButtonType}
onClick={onSecondary}
className="ml-3"
disabled={secondaryDisabled}
>
{secondaryText}
</Button>
)}
{typeof onTertiary === 'function' && tertiaryText && (
<Button
buttonType={tertiaryButtonType}
onClick={onTertiary}
className="ml-3"
disabled={tertiaryDisabled}
>
{tertiaryText}
</Button>
)}
{typeof onCancel === 'function' && (
<Button
buttonType={cancelButtonType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText
? cancelText
: intl.formatMessage(globalMessages.cancel)}
</Button>
)}
</div>
)}
<div
className={`mt-3 text-center sm:mt-0 sm:text-left ${
iconSvg ? 'sm:ml-4' : 'mb-6'
}`}
>
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
id="modal-headline"
>
{title}
</h3>
)}
</div>
</Transition>
</div>,
document.body
)}
</>
</div>
{children && (
<div className="mt-4 text-sm leading-5 text-gray-300">
{children}
</div>
)}
{(onCancel || onOk || onSecondary || onTertiary) && (
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
{typeof onOk === 'function' && (
<Button
buttonType={okButtonType}
onClick={onOk}
className="ml-3"
disabled={okDisabled}
>
{okText ? okText : 'Ok'}
</Button>
)}
{typeof onSecondary === 'function' && secondaryText && (
<Button
buttonType={secondaryButtonType}
onClick={onSecondary}
className="ml-3"
disabled={secondaryDisabled}
>
{secondaryText}
</Button>
)}
{typeof onTertiary === 'function' && tertiaryText && (
<Button
buttonType={tertiaryButtonType}
onClick={onTertiary}
className="ml-3"
disabled={tertiaryDisabled}
>
{tertiaryText}
</Button>
)}
{typeof onCancel === 'function' && (
<Button
buttonType={cancelButtonType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText
? cancelText
: intl.formatMessage(globalMessages.cancel)}
</Button>
)}
</div>
)}
</div>
</Transition>
</div>,
document.body
);
};

View File

@@ -0,0 +1,70 @@
import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import type { MovieResult } from '../../../../server/models/Search';
import ListView from '../../Common/ListView';
import { LanguageContext } from '../../../context/LanguageContext';
import Header from '../../Common/Header';
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
const Holiday: React.FC = () => {
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/keyword/207317/movies?page=${
pageIndex + 1
}&language=${locale}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
if (error) {
return <div>{error}</div>;
}
const titles = data?.reduce(
(a, v) => [...a, ...v.results],
[] as MovieResult[]
);
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
return (
<>
<Header>Happy Holidays!</Header>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default Holiday;

View File

@@ -63,6 +63,12 @@ const Discover: React.FC = () => {
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/movies/upcoming?language=${locale}`
);
const {
data: holUpcomingData,
error: holUpcomingError,
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/keyword/207317/movies?language=${locale}`
);
const { data: trendingData, error: trendingError } = useSWR<MixedResult>(
`/api/v1/discover/trending?language=${locale}`
@@ -140,6 +146,57 @@ const Discover: React.FC = () => {
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.nopending)}
/>
{/* Special Temporary Slider */}
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<Link href="/discover/holiday">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<span>
<span role="img" aria-label="Christmas Tree" className="mr-2">
🎄
</span>
Happy Holidays!
<span role="img" aria-label="Christmas Tree" className="ml-2">
🎄
</span>
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="holiday"
isLoading={!holUpcomingData && !holUpcomingError}
isEmpty={false}
items={holUpcomingData?.results.map((title) => (
<TitleCard
key={`holiday-movie-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/>
{/* End Special Temporary Slider */}
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<Link href="/discover/movies/upcoming">

View File

@@ -45,6 +45,10 @@ const availableLanguages: AvailableLanguageObject = {
code: 'nl',
display: 'Nederlands',
},
es: {
code: 'es',
display: 'Spanish',
},
};
const LanguagePicker: React.FC = () => {

View File

@@ -1,9 +1,10 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useRef } from 'react';
import Transition from '../../Transition';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl';
import { useUser, Permission } from '../../../hooks/useUser';
import useClickOutside from '../../../hooks/useClickOutside';
const messages = defineMessages({
dashboard: 'Discover',
@@ -116,8 +117,10 @@ const SidebarLinks: SidebarLinkProps[] = [
];
const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
const navRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { hasPermission } = useUser();
useClickOutside(navRef, () => setClosed());
return (
<>
<div className="md:hidden">
@@ -166,7 +169,10 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
</svg>
</button>
</div>
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<div
ref={navRef}
className="flex-1 h-0 pt-5 pb-4 overflow-y-auto"
>
<div className="flex-shrink-0 flex items-center px-4">
<span className="text-xl text-gray-50">
<Link href="/">

View File

@@ -2,10 +2,10 @@ import React, { useState } from 'react';
import SearchInput from './SearchInput';
import UserDropdown from './UserDropdown';
import Sidebar from './Sidebar';
import Notifications from './Notifications';
import LanguagePicker from './LanguagePicker';
import { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Permission, useUser } from '../../hooks/useUser';
const messages = defineMessages({
alphawarning:
@@ -14,6 +14,7 @@ const messages = defineMessages({
const Layout: React.FC = ({ children }) => {
const [isSidebarOpen, setSidebarOpen] = useState(false);
const { hasPermission } = useUser();
const router = useRouter();
return (
@@ -45,7 +46,6 @@ const Layout: React.FC = ({ children }) => {
<SearchInput />
<div className="ml-4 flex items-center md:ml-6">
<LanguagePicker />
<Notifications />
<UserDropdown />
</div>
</div>
@@ -57,7 +57,7 @@ const Layout: React.FC = ({ children }) => {
>
<div className="pt-2 pb-6 md:py-6">
<div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8">
{router.pathname === '/' && (
{router.pathname === '/' && hasPermission(Permission.ADMIN) && (
<div className="rounded-md bg-indigo-700 p-4 mt-2">
<div className="flex">
<div className="flex-shrink-0">
@@ -85,7 +85,7 @@ const Layout: React.FC = ({ children }) => {
target="_blank"
rel="noreferrer"
>
Github &rarr;
GitHub &rarr;
</a>
</p>
</div>

View File

@@ -5,25 +5,35 @@ import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import ImageFader from '../Common/ImageFader';
import { defineMessages, FormattedMessage } from 'react-intl';
import Transition from '../Transition';
const messages = defineMessages({
signinplex: 'Sign in to continue',
});
const Login: React.FC = () => {
const [error, setError] = useState('');
const [isProcessing, setProcessing] = useState(false);
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
const { user, revalidate } = useUser();
const router = useRouter();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to login. If we get a success message, we will
// ask swr to revalidate the user which _shouid_ come back with a valid user.
// ask swr to revalidate the user which _should_ come back with a valid user.
useEffect(() => {
const login = async () => {
const response = await axios.post('/api/v1/auth/login', { authToken });
setProcessing(true);
try {
const response = await axios.post('/api/v1/auth/login', { authToken });
if (response.data?.email) {
revalidate();
if (response.data?.email) {
revalidate();
}
} catch (e) {
setError(e.response.data.message);
setAuthToken(undefined);
setProcessing(false);
}
};
if (authToken) {
@@ -64,7 +74,40 @@ const Login: React.FC = () => {
className="bg-gray-800 bg-opacity-50 py-8 px-4 shadow sm:rounded-lg sm:px-10"
style={{ backdropFilter: 'blur(5px)' }}
>
<Transition
show={!!error}
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="rounded-md bg-red-600 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-red-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-300">{error}</h3>
</div>
</div>
</div>
</Transition>
<PlexLoginButton
isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)}
/>
</div>

View File

@@ -0,0 +1,66 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullcast: 'Full Cast',
});
const MovieCast: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const { data, error } = useSWR<MovieDetails>(
`/api/v1/movie/${router.query.movieId}?language=${locale}`
);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <Error statusCode={404} />;
}
return (
<>
<Header
subtext={
<Link href={`/movie/${data.id}`}>
<a className="hover:underline">{data.title}</a>
</Link>
}
>
{intl.formatMessage(messages.fullcast)}
</Header>
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
{data?.credits.cast.map((person) => {
return (
<li
key={person.id}
className="col-span-1 flex flex-col text-center items-center"
>
<PersonCard
name={person.name}
personId={person.id}
subName={person.character}
profilePath={person.profilePath}
canExpand
/>
</li>
);
})}
</ul>
</>
);
};
export default MovieCast;

View File

@@ -65,6 +65,7 @@ const messages = defineMessages({
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
approve: 'Approve',
decline: 'Decline',
studio: 'Studio',
});
interface MovieDetailsProps {
@@ -193,14 +194,14 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
)}
</SlideOver>
<div className="flex flex-col items-center md:flex-row md:items-end pt-4">
<div className="mr-4 flex-shrink-0">
<div className="md:mr-4 flex-shrink-0">
<img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
alt=""
className="rounded md:rounded-lg shadow md:shadow-2xl w-32 md:w-52"
/>
</div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="text-white flex flex-col md:mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="mb-2">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">
@@ -352,7 +353,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="default"
className="ml-2"
className="ml-2 first:ml-0"
onClick={() => setShowManager(true)}
>
<svg
@@ -484,14 +485,32 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</span>
</div>
)}
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.originallanguage} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{data.originalLanguage}
</span>
</div>
{data.spokenLanguages.some(
(lng) => lng.iso_639_1 === data.originalLanguage
) && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.originallanguage} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{
data.spokenLanguages.find(
(lng) => lng.iso_639_1 === data.originalLanguage
)?.name
}
</span>
</div>
)}
{data.productionCompanies[0] && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.studio} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{data.productionCompanies[0]?.name}
</span>
</div>
)}
</div>
</div>
</div>

View File

@@ -5,7 +5,6 @@ import type { PersonDetail } from '../../../server/models/Person';
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
import Error from '../../pages/_error';
import LoadingSpinner from '../Common/LoadingSpinner';
import Slider from '../Slider';
import TitleCard from '../TitleCard';
import { defineMessages, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
@@ -54,9 +53,11 @@ const PersonDetails: React.FC = () => {
return 1;
});
const isLoading = !combinedCredits && !errorCombinedCredits;
return (
<>
<div className="flex mt-8 mb-8 flex-col md:flex-row items-center">
<div className="flex mt-8 mb-8 flex-col md:flex-row items-center md:items-start">
{data.profilePath && (
<div
style={{
@@ -81,13 +82,13 @@ const PersonDetails: React.FC = () => {
</div>
</div>
</div>
<Slider
isEmpty={!sortedCast}
isLoading={!combinedCredits && !errorCombinedCredits}
sliderKey={`person-${data.id}-slider-cast`}
items={sortedCast?.map((media) => {
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
{sortedCast?.map((media) => {
return (
<div key={`slider-cast-item-${media.id}`} className="flex flex-col">
<li
key={`list-cast-item-${media.id}`}
className="col-span-1 flex flex-col text-center items-center"
>
<TitleCard
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
@@ -101,6 +102,7 @@ const PersonDetails: React.FC = () => {
summary={media.overview}
mediaType={media.mediaType as 'movie' | 'tv'}
status={media.mediaInfo?.status}
canExpand
/>
{media.character && (
<div className="mt-2 text-gray-300 text-xs truncate w-36 sm:w-36 md:w-44 text-center">
@@ -109,10 +111,19 @@ const PersonDetails: React.FC = () => {
})}
</div>
)}
</div>
</li>
);
})}
/>
{isLoading &&
[...Array(20)].map((_item, i) => (
<li
key={`placeholder-${i}`}
className="col-span-1 flex flex-col text-center items-center"
>
<TitleCard.Placeholder canExpand />
</li>
))}
</ul>
</>
);
};

View File

@@ -12,23 +12,23 @@ const plexOAuth = new PlexOAuth();
interface PlexLoginButtonProps {
onAuthToken: (authToken: string) => void;
isProcessing?: boolean;
onError?: (message: string) => void;
}
const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
onAuthToken,
onError,
isProcessing,
}) => {
const intl = useIntl();
const [loading, setLoading] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const getPlexLogin = async () => {
setLoading(true);
try {
const authToken = await plexOAuth.login();
setLoading(false);
setIsProcessing(true);
onAuthToken(authToken);
} catch (e) {
if (onError) {

View File

@@ -10,7 +10,6 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
searchresults: 'Search Results',
@@ -66,25 +65,7 @@ const Search: React.FC = () => {
return (
<>
<div className="md:flex md:items-center md:justify-between mb-6 sm:mb-0 mt-6">
<Header>{intl.formatMessage(messages.searchresults)}</Header>
<div className="mt-4 flex md:mt-0 md:ml-4">
<span className="relative z-0 inline-flex shadow-sm rounded-md">
<button
type="button"
className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-indigo-900 bg-indigo-500 hover:bg-indigo-400 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
{intl.formatMessage(globalMessages.movies)}
</button>
<button
type="button"
className="-ml-px relative inline-flex items-center px-4 py-2 rounded-r-md border border-indigo-900 bg-indigo-500 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
{intl.formatMessage(globalMessages.tvshows)}
</button>
</span>
</div>
</div>
<Header>{intl.formatMessage(messages.searchresults)}</Header>
<ListView
items={titles}
isEmpty={isEmpty}

View File

@@ -25,7 +25,10 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
return (
<button
onClick={setCopied}
onClick={(e) => {
e.preventDefault();
setCopied();
}}
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
<svg

View File

@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import Axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({
save: 'Save Changes',
@@ -14,10 +15,13 @@ const messages = defineMessages({
webhookUrl: 'Webhook URL',
validationWebhookUrlRequired: 'You must provide a webhook URL',
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
discordsettingssaved: 'Discord notification settings saved!',
discordsettingsfailed: 'Discord notification settings failed to save.',
});
const NotificationsDiscord: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/discord'
);
@@ -49,8 +53,15 @@ const NotificationsDiscord: React.FC = () => {
webhookUrl: values.webhookUrl,
},
});
addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
// TODO show error
addToast(intl.formatMessage(messages.discordsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}

View File

@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import Axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({
save: 'Save Changes',
@@ -20,10 +21,13 @@ const messages = defineMessages({
enableSsl: 'Enable SSL',
authUser: 'Auth User',
authPass: 'Auth Pass',
emailsettingssaved: 'Email notification settings saved!',
emailsettingsfailed: 'Email notification settings failed to save.',
});
const NotificationsEmail: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/email'
);
@@ -65,14 +69,21 @@ const NotificationsEmail: React.FC = () => {
options: {
emailFrom: values.emailFrom,
smtpHost: values.smtpHost,
smtpPort: values.smtpPort,
smtpPort: Number(values.smtpPort),
secure: values.secure,
authUser: values.authUser,
authPass: values.authPass,
},
});
addToast(intl.formatMessage(messages.emailsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
// TODO show error
addToast(intl.formatMessage(messages.emailsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}

View File

@@ -11,11 +11,13 @@ import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
createradarr: 'Create New Radarr Server',
editradarr: 'Edit Radarr Server',
validationNameRequired: 'You must provide a server name',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
validationRootFolderRequired: 'You must select a root folder',
validationProfileRequired: 'You must select a profile',
validationMinimumAvailabilityRequired: 'You must select minimum availability',
toastRadarrTestSuccess: 'Radarr connection established!',
toastRadarrTestFailure: 'Failed to connect to Radarr Server',
saving: 'Saving...',
@@ -40,6 +42,10 @@ const messages = defineMessages({
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
selectMinimumAvailability: 'Select minimum availability',
loadingprofiles: 'Loading quality profiles…',
testFirstQualityProfiles: 'Test your connection to load quality profiles',
loadingrootfolders: 'Loading root folders…',
testFirstRootFolders: 'Test your connection to load root folders',
});
interface TestResponse {
@@ -74,6 +80,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
rootFolders: [],
});
const RadarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
),
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
@@ -81,10 +90,15 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
intl.formatMessage(messages.validationPortRequired)
),
apiKey: Yup.string().required(intl.formatMessage(messages.apiKey)),
rootFolder: Yup.string().required(intl.formatMessage(messages.rootfolder)),
rootFolder: Yup.string().required(
intl.formatMessage(messages.validationRootFolderRequired)
),
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
minimumAvailability: Yup.string().required(
intl.formatMessage(messages.validationMinimumAvailabilityRequired)
),
});
const testConnection = useCallback(
@@ -108,7 +122,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
{
hostname,
apiKey,
port,
port: Number(port),
baseUrl,
useSsl,
}
@@ -171,7 +185,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
baseUrl: radarr?.baseUrl,
activeProfileId: radarr?.activeProfileId,
rootFolder: radarr?.activeDirectory,
minimumAvailability: radarr?.minimumAvailability,
minimumAvailability: radarr?.minimumAvailability ?? 'released',
isDefault: radarr?.isDefault ?? false,
is4k: radarr?.is4k ?? false,
}}
@@ -185,11 +199,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
const submission = {
name: values.name,
hostname: values.hostname,
port: values.port,
port: Number(values.port),
apiKey: values.apiKey,
useSsl: values.ssl,
baseUrl: values.baseUrl,
activeProfileId: values.activeProfileId,
activeProfileId: Number(values.activeProfileId),
activeProfileName: profileName,
activeDirectory: values.rootFolder,
is4k: values.is4k,
@@ -218,6 +232,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
handleSubmit,
setFieldValue,
isSubmitting,
isValid,
}) => {
return (
<Modal
@@ -250,7 +265,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
secondaryDisabled={
!values.apiKey || !values.hostname || !values.port || isTesting
}
okDisabled={!isValidated || isSubmitting || isTesting}
okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
onOk={() => handleSubmit()}
title={
!radarr
@@ -312,6 +327,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-600 text-gray-100 sm:text-sm cursor-default">
{values.ssl ? 'https://' : 'http://'}
</span>
<Field
id="hostname"
name="hostname"
@@ -321,7 +339,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
setIsValidated(false);
setFieldValue('hostname', e.target.value);
}}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.hostname && touched.hostname && (
@@ -442,10 +460,17 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
as="select"
id="activeProfileId"
name="activeProfileId"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
disabled={!isValidated || isTesting}
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{intl.formatMessage(messages.selectQualityProfile)}
{isTesting
? intl.formatMessage(messages.loadingprofiles)
: !isValidated
? intl.formatMessage(
messages.testFirstQualityProfiles
)
: intl.formatMessage(messages.selectQualityProfile)}
</option>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
@@ -478,10 +503,15 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
as="select"
id="rootFolder"
name="rootFolder"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
disabled={!isValidated || isTesting}
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{intl.formatMessage(messages.selectRootFolder)}
{isTesting
? intl.formatMessage(messages.loadingrootfolders)
: !isValidated
? intl.formatMessage(messages.testFirstRootFolders)
: intl.formatMessage(messages.selectRootFolder)}
</option>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
@@ -516,17 +546,18 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
name="minimumAvailability"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">
{intl.formatMessage(
messages.selectMinimumAvailability
)}
</option>
<option value="announced">Announced</option>
<option value="inCinemas">In Cinemas</option>
<option value="released">Released</option>
<option value="preDB">PreDB</option>
</Field>
</div>
{errors.minimumAvailability &&
touched.minimumAvailability && (
<div className="text-red-500 mt-2">
{errors.minimumAvailability}
</div>
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">

View File

@@ -0,0 +1,80 @@
import React from 'react';
import useSWR from 'swr';
import Error from '../../../pages/_error';
import List from '../../Common/List';
import LoadingSpinner from '../../Common/LoadingSpinner';
import { SettingsAboutResponse } from '../../../../server/interfaces/api/settingsInterfaces';
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
const messages = defineMessages({
overseerrinformation: 'Overseerr Information',
version: 'Version',
totalmedia: 'Total Media',
totalrequests: 'Total Requests',
gettingsupport: 'Getting Support',
githubdiscussions: 'GitHub Discussions',
clickheretojoindiscord: 'Click here to join our Discord server.',
});
const SettingsAbout: React.FC = () => {
const intl = useIntl();
const { data, error } = useSWR<SettingsAboutResponse>(
'/api/v1/settings/about'
);
if (error) {
return <Error statusCode={500} />;
}
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <LoadingSpinner />;
}
return (
<>
<div className="mb-8">
<List title={intl.formatMessage(messages.overseerrinformation)}>
<List.Item title={intl.formatMessage(messages.version)}>
{data.version}
</List.Item>
<List.Item title={intl.formatMessage(messages.totalmedia)}>
<FormattedNumber value={data.totalMediaItems} />
</List.Item>
<List.Item title={intl.formatMessage(messages.totalrequests)}>
<FormattedNumber value={data.totalRequests} />
</List.Item>
</List>
</div>
<div className="mb-8">
<List title={intl.formatMessage(messages.gettingsupport)}>
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
<a
href="https://github.com/sct/overseerr/discussions"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://github.com/sct/overseerr/discussions
</a>
</List.Item>
<List.Item title="Discord">
<a
href="https://discord.gg/PkCWJSeCk7"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
{intl.formatMessage(messages.clickheretojoindiscord)}
</a>
</List.Item>
</List>
</div>
</>
);
};
export default SettingsAbout;

View File

@@ -46,7 +46,7 @@ const SettingsJobs: React.FC = () => {
</div>
</Table.TD>
<Table.TD alignText="right">
<Button buttonType="primary">
<Button buttonType="primary" disabled>
{intl.formatMessage(messages.runnow)}
</Button>
</Table.TD>

View File

@@ -0,0 +1,17 @@
import React from 'react';
// We will localize this file when the complete version is released.
const SettingsLogs: React.FC = () => {
return (
<>
<div className="leading-loose text-gray-300 text-sm">
Logs page is still being built. For now, you can access your logs
directly in <code>stdout</code> (container logs) or looking in{' '}
<code>/app/config/logs/overseerr.logs</code>
</div>
</>
);
};
export default SettingsLogs;

View File

@@ -8,6 +8,7 @@ import axios from 'axios';
import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import { useUser, Permission } from '../../hooks/useUser';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({
generalsettings: 'General Settings',
@@ -17,15 +18,37 @@ const messages = defineMessages({
saving: 'Saving...',
apikey: 'API Key',
applicationurl: 'Application URL',
toastApiKeySuccess: 'New API Key generated!',
toastApiKeyFailure: 'Something went wrong generating a new API Key.',
toastSettingsSuccess: 'Settings saved.',
toastSettingsFailure: 'Something went wrong saving settings.',
});
const SettingsMain: React.FC = () => {
const { addToast } = useToasts();
const { hasPermission } = useUser();
const intl = useIntl();
const { data, error, revalidate } = useSWR<MainSettings>(
'/api/v1/settings/main'
);
const regenerate = async () => {
try {
await axios.get('/api/v1/settings/main/regenerate');
revalidate();
addToast(intl.formatMessage(messages.toastApiKeySuccess), {
autoDismiss: true,
appearance: 'success',
});
} catch (e) {
addToast(intl.formatMessage(messages.toastApiKeyFailure), {
autoDismiss: true,
appearance: 'error',
});
}
};
if (!data && !error) {
return <LoadingSpinner />;
}
@@ -50,8 +73,16 @@ const SettingsMain: React.FC = () => {
await axios.post('/api/v1/settings/main', {
applicationUrl: values.applicationUrl,
});
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
autoDismiss: true,
appearance: 'success',
});
} catch (e) {
// TODO show error
addToast(intl.formatMessage(messages.toastSettingsFailure), {
autoDismiss: true,
appearance: 'error',
});
} finally {
revalidate();
}
@@ -77,8 +108,17 @@ const SettingsMain: React.FC = () => {
value={data?.apiKey}
readOnly
/>
<CopyButton textToCopy={data?.apiKey ?? ''} />
<button className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium rounded-r-md text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
<CopyButton
textToCopy={data?.apiKey ?? ''}
key={data?.apiKey}
/>
<button
onClick={(e) => {
e.preventDefault();
regenerate();
}}
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium rounded-r-md text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
<svg
className="w-5 h-5"
fill="currentColor"

View File

@@ -2,21 +2,23 @@ import React, { useState } from 'react';
import LoadingSpinner from '../Common/LoadingSpinner';
import type { PlexSettings } from '../../../server/lib/settings';
import useSWR from 'swr';
import { useFormik } from 'formik';
import { Formik, Field } from 'formik';
import Button from '../Common/Button';
import axios from 'axios';
import LibraryItem from './LibraryItem';
import Badge from '../Common/Badge';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import * as Yup from 'yup';
const messages = defineMessages({
plexsettings: 'Plex Settings',
plexsettingsDescription:
'Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.',
servername: 'Server Name (Automatically Set)',
servername: 'Server Name (Automatically set after you save)',
servernamePlaceholder: 'Plex Server Name',
hostname: 'Hostname/IP',
port: 'Port',
ssl: 'SSL',
save: 'Save Changes',
saving: 'Saving...',
plexlibraries: 'Plex Libraries',
@@ -32,6 +34,8 @@ const messages = defineMessages({
librariesRemaining: 'Libraries Remaining: {count}',
startscan: 'Start Scan',
cancelscan: 'Cancel Scan',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
});
interface Library {
@@ -64,33 +68,15 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
}
);
const [isSyncing, setIsSyncing] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const formik = useFormik({
initialValues: {
hostname: data?.ip,
port: data?.port,
},
enableReinitialize: true,
onSubmit: async (values) => {
setSubmitError(null);
setIsUpdating(true);
try {
await axios.post('/api/v1/settings/plex', {
ip: values.hostname,
port: values.port,
} as PlexSettings);
revalidate();
if (onComplete) {
onComplete();
}
} catch (e) {
setSubmitError(e.response.data.message);
} finally {
setIsUpdating(false);
}
},
const PlexSettingsSchema = Yup.object().shape({
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired)
),
});
const activeLibraries =
@@ -164,91 +150,157 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<FormattedMessage {...messages.plexsettingsDescription} />
</p>
</div>
<form onSubmit={formik.handleSubmit}>
<div className="mt-6 sm:mt-5">
{submitError && (
<div className="bg-red-700 text-white p-4 rounded-md mb-6">
{submitError}
</div>
)}
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.servername} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="text"
id="name"
name="name"
placeholder={intl.formatMessage(
messages.servernamePlaceholder
)}
value={data?.name}
readOnly
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
<Formik
initialValues={{
hostname: data?.ip,
port: data?.port,
useSsl: data?.useSsl,
}}
enableReinitialize
validationSchema={PlexSettingsSchema}
onSubmit={async (values) => {
setSubmitError(null);
try {
await axios.post('/api/v1/settings/plex', {
ip: values.hostname,
port: Number(values.port),
useSsl: values.useSsl,
} as PlexSettings);
revalidate();
if (onComplete) {
onComplete();
}
} catch (e) {
setSubmitError(e.response.data.message);
}
}}
>
{({
errors,
touched,
values,
handleSubmit,
setFieldValue,
isSubmitting,
}) => {
return (
<form onSubmit={handleSubmit}>
<div className="mt-6 sm:mt-5">
{submitError && (
<div className="bg-red-700 text-white p-4 rounded-md mb-6">
{submitError}
</div>
)}
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.servername} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="text"
id="name"
name="name"
placeholder={intl.formatMessage(
messages.servernamePlaceholder
)}
value={data?.name}
readOnly
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
</div>
</div>
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.hostname} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-800 text-gray-100 sm:text-sm cursor-default">
{values.useSsl ? 'https://' : 'http://'}
</span>
<Field
type="text"
id="hostname"
name="hostname"
placeholder="127.0.0.1"
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.hostname && touched.hostname && (
<div className="text-red-500 mt-2">{errors.hostname}</div>
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.port} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
<Field
type="text"
id="port"
name="port"
placeholder="32400"
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.port && touched.port && (
<div className="text-red-500 mt-2">{errors.port}</div>
)}
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.hostname} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="text"
id="hostname"
name="hostname"
placeholder="127.0.0.1"
value={formik.values.hostname}
onChange={formik.handleChange}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
{intl.formatMessage(messages.ssl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="useSsl"
name="useSsl"
onChange={() => {
setFieldValue('useSsl', !values.useSsl);
}}
className="form-checkbox h-6 w-6 rounded-md text-indigo-600 transition duration-150 ease-in-out"
/>
</div>
</div>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.port} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
<input
type="text"
id="port"
name="port"
placeholder="32400"
value={formik.values.port}
onChange={formik.handleChange}
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</div>
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button buttonType="primary" type="submit" disabled={isUpdating}>
{isUpdating
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</form>
</form>
);
}}
</Formik>
<div className="mt-10">
<h3 className="text-lg leading-6 font-medium text-gray-200">
<FormattedMessage {...messages.plexlibraries} />

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import useSWR from 'swr';
@@ -31,8 +31,48 @@ const messages = defineMessages({
activeProfile: 'Active Profile',
addradarr: 'Add Radarr Server',
addsonarr: 'Add Sonarr Server',
nodefault: 'No default server selected!',
nodefaultdescription:
'At least one server must be marked as default before any requests will make it to your services.',
no4kimplemented: '(Default 4K servers are not currently implemented)',
});
const NoDefaultAlert: React.FC = () => {
const intl = useIntl();
return (
<div className="rounded-md bg-yellow-600 p-4 mb-8">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-yellow-200"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-200">
{intl.formatMessage(messages.nodefault)}
</h3>
<div className="mt-2 text-sm text-yellow-300">
<p>{intl.formatMessage(messages.nodefaultdescription)}</p>
<p className="mt-2">
{intl.formatMessage(messages.no4kimplemented)}
</p>
</div>
</div>
</div>
</div>
);
};
interface ServerInstanceProps {
name: string;
isDefault?: boolean;
@@ -249,51 +289,57 @@ const SettingsServices: React.FC = () => {
<div className="mt-6 sm:mt-5">
{!radarrData && !radarrError && <LoadingSpinner />}
{radarrData && !radarrError && (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{radarrData.map((radarr) => (
<ServerInstance
key={`radarr-config-${radarr.id}`}
name={radarr.name}
address={radarr.hostname}
profileName={radarr.activeProfileName}
isSSL={radarr.useSsl}
isDefault={radarr.isDefault && !radarr.is4k}
isDefault4K={radarr.is4k && radarr.isDefault}
onEdit={() => setEditRadarrModal({ open: true, radarr })}
onDelete={() =>
setDeleteServerModal({
open: true,
serverId: radarr.id,
type: 'radarr',
})
}
/>
))}
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
<div className="flex items-center justify-center w-full h-full">
<Button
buttonType="ghost"
onClick={() =>
setEditRadarrModal({ open: true, radarr: null })
<>
{radarrData.length > 0 &&
!radarrData.some(
(radarr) => radarr.isDefault && !radarr.is4k
) && <NoDefaultAlert />}
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{radarrData.map((radarr) => (
<ServerInstance
key={`radarr-config-${radarr.id}`}
name={radarr.name}
address={radarr.hostname}
profileName={radarr.activeProfileName}
isSSL={radarr.useSsl}
isDefault={radarr.isDefault && !radarr.is4k}
isDefault4K={radarr.is4k && radarr.isDefault}
onEdit={() => setEditRadarrModal({ open: true, radarr })}
onDelete={() =>
setDeleteServerModal({
open: true,
serverId: radarr.id,
type: 'radarr',
})
}
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
/>
))}
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
<div className="flex items-center justify-center w-full h-full">
<Button
buttonType="ghost"
onClick={() =>
setEditRadarrModal({ open: true, radarr: null })
}
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
<FormattedMessage {...messages.addradarr} />
</Button>
</div>
</li>
</ul>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
<FormattedMessage {...messages.addradarr} />
</Button>
</div>
</li>
</ul>
</>
)}
</div>
<div className="mt-10">
@@ -307,52 +353,58 @@ const SettingsServices: React.FC = () => {
<div className="mt-6 sm:mt-5">
{!sonarrData && !sonarrError && <LoadingSpinner />}
{sonarrData && !sonarrError && (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{sonarrData.map((sonarr) => (
<ServerInstance
key={`sonarr-config-${sonarr.id}`}
name={sonarr.name}
address={sonarr.hostname}
profileName={sonarr.activeProfileName}
isSSL={sonarr.useSsl}
isSonarr
isDefault4K={sonarr.isDefault && sonarr.is4k}
isDefault={sonarr.isDefault && !sonarr.is4k}
onEdit={() => setEditSonarrModal({ open: true, sonarr })}
onDelete={() =>
setDeleteServerModal({
open: true,
serverId: sonarr.id,
type: 'sonarr',
})
}
/>
))}
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
<div className="flex items-center justify-center w-full h-full">
<Button
buttonType="ghost"
onClick={() =>
setEditSonarrModal({ open: true, sonarr: null })
<>
{sonarrData.length > 0 &&
!sonarrData.some(
(sonarr) => sonarr.isDefault && !sonarr.is4k
) && <NoDefaultAlert />}
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{sonarrData.map((sonarr) => (
<ServerInstance
key={`sonarr-config-${sonarr.id}`}
name={sonarr.name}
address={sonarr.hostname}
profileName={sonarr.activeProfileName}
isSSL={sonarr.useSsl}
isSonarr
isDefault4K={sonarr.isDefault && sonarr.is4k}
isDefault={sonarr.isDefault && !sonarr.is4k}
onEdit={() => setEditSonarrModal({ open: true, sonarr })}
onDelete={() =>
setDeleteServerModal({
open: true,
serverId: sonarr.id,
type: 'sonarr',
})
}
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
/>
))}
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
<div className="flex items-center justify-center w-full h-full">
<Button
buttonType="ghost"
onClick={() =>
setEditSonarrModal({ open: true, sonarr: null })
}
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
<FormattedMessage {...messages.addsonarr} />
</Button>
</div>
</li>
</ul>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
<FormattedMessage {...messages.addsonarr} />
</Button>
</div>
</li>
</ul>
</>
)}
</div>
</>

View File

@@ -11,6 +11,7 @@ import { useIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
createsonarr: 'Create New Sonarr Server',
editsonarr: 'Edit Sonarr Server',
validationNameRequired: 'You must provide a server name',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
@@ -35,10 +36,16 @@ const messages = defineMessages({
baseUrlPlaceholder: 'Example: /sonarr',
qualityprofile: 'Quality Profile',
rootfolder: 'Root Folder',
animequalityprofile: 'Anime Quality Profile',
animerootfolder: 'Anime Root Folder',
seasonfolders: 'Season Folders',
server4k: '4K Server',
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
loadingprofiles: 'Loading quality profiles…',
testFirstQualityProfiles: 'Test your connection to load quality profiles',
loadingrootfolders: 'Loading root folders…',
testFirstRootFolders: 'Test your connection to load root folders',
});
interface TestResponse {
@@ -73,6 +80,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
rootFolders: [],
});
const SonarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
),
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
@@ -111,7 +121,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
{
hostname,
apiKey,
port,
port: Number(port),
baseUrl,
useSsl,
}
@@ -174,6 +184,8 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
baseUrl: sonarr?.baseUrl,
activeProfileId: sonarr?.activeProfileId,
rootFolder: sonarr?.activeDirectory,
activeAnimeProfileId: sonarr?.activeAnimeProfileId,
activeAnimeRootFolder: sonarr?.activeAnimeDirectory,
isDefault: sonarr?.isDefault ?? false,
is4k: sonarr?.is4k ?? false,
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
@@ -184,17 +196,25 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
const profileName = testResponse.profiles.find(
(profile) => profile.id === Number(values.activeProfileId)
)?.name;
const animeProfileName = testResponse.profiles.find(
(profile) => profile.id === Number(values.activeAnimeProfileId)
)?.name;
const submission = {
name: values.name,
hostname: values.hostname,
port: values.port,
port: Number(values.port),
apiKey: values.apiKey,
useSsl: values.ssl,
baseUrl: values.baseUrl,
activeProfileId: values.activeProfileId,
activeProfileId: Number(values.activeProfileId),
activeProfileName: profileName,
activeDirectory: values.rootFolder,
activeAnimeProfileId: values.activeAnimeProfileId
? Number(values.activeAnimeProfileId)
: undefined,
activeAnimeProfileName: animeProfileName ?? undefined,
activeAnimeDirectory: values.activeAnimeRootFolder,
is4k: values.is4k,
isDefault: values.isDefault,
enableSeasonFolders: values.enableSeasonFolders,
@@ -221,6 +241,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
handleSubmit,
setFieldValue,
isSubmitting,
isValid,
}) => {
return (
<Modal
@@ -253,7 +274,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
secondaryDisabled={
!values.apiKey || !values.hostname || !values.port || isTesting
}
okDisabled={!isValidated || isSubmitting || isTesting}
okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
onOk={() => handleSubmit()}
title={
!sonarr
@@ -315,6 +336,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-600 text-gray-100 sm:text-sm cursor-default">
{values.ssl ? 'https://' : 'http://'}
</span>
<Field
id="hostname"
name="hostname"
@@ -324,7 +348,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
setIsValidated(false);
setFieldValue('hostname', e.target.value);
}}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.hostname && touched.hostname && (
@@ -445,10 +469,17 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
as="select"
id="activeProfileId"
name="activeProfileId"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
disabled={!isValidated || isTesting}
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{intl.formatMessage(messages.selectQualityProfile)}
{isTesting
? intl.formatMessage(messages.loadingprofiles)
: !isValidated
? intl.formatMessage(
messages.testFirstQualityProfiles
)
: intl.formatMessage(messages.selectQualityProfile)}
</option>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
@@ -481,10 +512,15 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
as="select"
id="rootFolder"
name="rootFolder"
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
disabled={!isValidated || isTesting}
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{intl.formatMessage(messages.selectRootFolder)}
{isTesting
? intl.formatMessage(messages.loadingrootfolders)
: !isValidated
? intl.formatMessage(messages.testFirstRootFolders)
: intl.formatMessage(messages.selectRootFolder)}
</option>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
@@ -504,6 +540,92 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="activeAnimeProfileId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
{intl.formatMessage(messages.animequalityprofile)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<Field
as="select"
id="activeAnimeProfileId"
name="activeAnimeProfileId"
disabled={!isValidated || isTesting}
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{isTesting
? intl.formatMessage(messages.loadingprofiles)
: !isValidated
? intl.formatMessage(
messages.testFirstQualityProfiles
)
: intl.formatMessage(messages.selectQualityProfile)}
</option>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
<option
key={`loaded-profile-${profile.id}`}
value={profile.id}
>
{profile.name}
</option>
))}
</Field>
</div>
{errors.activeAnimeProfileId &&
touched.activeAnimeProfileId && (
<div className="text-red-500 mt-2">
{errors.activeAnimeProfileId}
</div>
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="activeAnimeRootFolder"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
{intl.formatMessage(messages.animerootfolder)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<Field
as="select"
id="activeAnimeRootFolder"
name="activeAnimeRootFolder"
disabled={!isValidated || isTesting}
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{isTesting
? intl.formatMessage(messages.loadingrootfolders)
: !isValidated
? intl.formatMessage(messages.testFirstRootFolders)
: intl.formatMessage(messages.selectRootFolder)}
</option>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
<option
key={`loaded-profile-${folder.id}`}
value={folder.path}
>
{folder.path}
</option>
))}
</Field>
</div>
{errors.activeAnimeRootFolder &&
touched.activeAnimeRootFolder && (
<div className="text-red-500 mt-2">
{errors.rootFolder}
</div>
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="is4k"

View File

@@ -8,6 +8,7 @@ import LoginWithPlex from './LoginWithPlex';
import SetupSteps from './SetupSteps';
import axios from 'axios';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Badge from '../Common/Badge';
const messages = defineMessages({
finish: 'Finish Setup',
@@ -16,6 +17,9 @@ const messages = defineMessages({
loginwithplex: 'Login with Plex',
configureplex: 'Configure Plex',
configureservices: 'Configure Services',
tip: 'Tip',
syncingbackground:
'Syncing will run in the background. You can continue the setup process in the meantime.',
});
const Setup: React.FC = () => {
@@ -85,6 +89,12 @@ const Setup: React.FC = () => {
{currentStep === 2 && (
<div>
<SettingsPlex onComplete={() => setPlexSettingsComplete(true)} />
<div className="mt-4 text-gray-500 text-sm">
<span className="mr-2">
<Badge>{intl.formatMessage(messages.tip)}</Badge>
</span>
{intl.formatMessage(messages.syncingbackground)}
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -10,17 +10,18 @@ import Link from 'next/link';
import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal';
import { defineMessages, useIntl } from 'react-intl';
import { useIsTouch } from '../../hooks/useIsTouch';
const messages = defineMessages({
movie: 'MOVIE',
tvshow: 'SERIES',
movie: 'Movie',
tvshow: 'Series',
});
interface TitleCardProps {
id: number;
image?: string;
summary?: string;
year: string;
year?: string;
title: string;
userScore: number;
mediaType: MediaType;
@@ -38,6 +39,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
mediaType,
canExpand = false,
}) => {
const isTouch = useIsTouch();
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStatus, setCurrentStatus] = useState(status);
@@ -74,9 +76,13 @@ const TitleCard: React.FC<TitleCardProps> = ({
<div
className="titleCard outline-none cursor-default"
style={{
backgroundImage: `url(//image.tmdb.org/t/p/w600_and_h900_bestv2${image})`,
backgroundImage: `url(//image.tmdb.org/t/p/w300_and_h450_face${image})`,
}}
onMouseEnter={() => {
if (!isTouch) {
setShowDetail(true);
}
}}
onMouseEnter={() => setShowDetail(true)}
onMouseLeave={() => setShowDetail(false)}
onClick={() => setShowDetail(true)}
onKeyDown={(e) => {
@@ -146,158 +152,162 @@ const TitleCard: React.FC<TitleCardProps> = ({
<Transition
show={!image || showDetail || showRequestModal}
enter="transition ease-in-out duration-300 transform opacity-0"
enter="transition transform opacity-0"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-in-out duration-300 transform opacity-100"
leave="transition transform opacity-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div
className="absolute w-full text-left top-0 right-0 left-0 bottom-0 rounded-lg overflow-hidden"
style={{
background:
'linear-gradient(180deg, rgba(45, 55, 72, 0.4) 0%, rgba(45, 55, 72, 0.9) 100%)',
}}
>
<div className="absolute bottom-0 w-full left-0 right-0">
<div className="px-2 text-white">
<div className="text-sm">{year}</div>
<Link href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}>
<a
className="absolute w-full text-left top-0 right-0 left-0 bottom-0 rounded-lg overflow-hidden cursor-pointer"
style={{
background:
'linear-gradient(180deg, rgba(45, 55, 72, 0.4) 0%, rgba(45, 55, 72, 0.9) 100%)',
}}
>
<div className="absolute bottom-0 w-full left-0 right-0">
<div className="px-2 text-white">
{year && <div className="text-sm">{year}</div>}
<h1 className="text-xl leading-tight whitespace-normal">
{title}
</h1>
<div
className="text-xs whitespace-normal"
style={{
WebkitLineClamp: 3,
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
}}
>
{summary}
<h1 className="text-xl leading-tight whitespace-normal">
{title}
</h1>
<div
className="text-xs whitespace-normal"
style={{
WebkitLineClamp: 3,
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
}}
>
{summary}
</div>
</div>
<div className="flex justify-between left-0 bottom-0 right-0 top-0 px-2 py-2">
<Link
href={
mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`
}
>
<a className="cursor-pointer flex w-full h-7 text-center text-white bg-indigo-500 rounded-sm hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150">
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
</a>
</Link>
{(!currentStatus ||
currentStatus === MediaStatus.UNKNOWN) && (
<button
onClick={(e) => {
e.preventDefault();
setShowRequestModal(true);
}}
className="w-full h-7 text-center text-white bg-indigo-500 rounded-sm ml-2 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150"
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</button>
)}
{currentStatus === MediaStatus.PENDING && (
<button
className="w-full h-7 text-center text-yellow-500 border border-yellow-500 rounded-sm ml-2 cursor-default"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button>
)}
{currentStatus === MediaStatus.PROCESSING && (
<button
className="w-full h-7 text-center text-red-500 border border-red-500 rounded-sm ml-2 cursor-default"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
)}
{(currentStatus === MediaStatus.AVAILABLE ||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
<button
className="w-full h-7 text-center text-green-400 border border-green-400 rounded-sm ml-2 cursor-default"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</button>
)}
</div>
</div>
<div className="flex justify-between left-0 bottom-0 right-0 top-0 px-2 py-2">
<Link
href={
mediaType === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'
}
as={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}
>
<a className="cursor-pointer flex w-full h-7 text-center text-white bg-indigo-500 rounded-sm hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150">
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
</a>
</Link>
{(!currentStatus ||
currentStatus === MediaStatus.UNKNOWN) && (
<button
onClick={() => setShowRequestModal(true)}
className="w-full h-7 text-center text-white bg-indigo-500 rounded-sm ml-2 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150"
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</button>
)}
{currentStatus === MediaStatus.PENDING && (
<button
className="w-full h-7 text-center text-yellow-500 border border-yellow-500 rounded-sm ml-2 cursor-default"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button>
)}
{currentStatus === MediaStatus.PROCESSING && (
<button
className="w-full h-7 text-center text-red-500 border border-red-500 rounded-sm ml-2 cursor-default"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
)}
{(currentStatus === MediaStatus.AVAILABLE ||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
<button
className="w-full h-7 text-center text-green-400 border border-green-400 rounded-sm ml-2 cursor-default"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</button>
)}
</div>
</div>
</div>
</a>
</Link>
</Transition>
</div>
</div>

View File

@@ -0,0 +1,66 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv';
import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullseriescast: 'Full Series Cast',
});
const TvCast: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const { data, error } = useSWR<TvDetails>(
`/api/v1/tv/${router.query.tvId}?language=${locale}`
);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <Error statusCode={404} />;
}
return (
<>
<Header
subtext={
<Link href={`/tv/${data.id}`}>
<a className="hover:underline">{data.name}</a>
</Link>
}
>
{intl.formatMessage(messages.fullseriescast)}
</Header>
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
{data?.credits.cast.map((person) => {
return (
<li
key={person.id}
className="col-span-1 flex flex-col text-center items-center"
>
<PersonCard
name={person.name}
personId={person.id}
subName={person.character}
profilePath={person.profilePath}
canExpand
/>
</li>
);
})}
</ul>
</>
);
};
export default TvCast;

View File

@@ -28,6 +28,7 @@ import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes';
import Head from 'next/head';
import globalMessages from '../../i18n/globalMessages';
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb';
const messages = defineMessages({
userrating: 'User Rating',
@@ -56,6 +57,9 @@ const messages = defineMessages({
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
approve: 'Approve',
decline: 'Decline',
showtype: 'Show Type',
anime: 'Anime',
network: 'Network',
});
interface TvDetailsProps {
@@ -196,14 +200,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
)}
</SlideOver>
<div className="flex flex-col items-center md:flex-row md:items-end pt-4">
<div className="mr-4 flex-shrink-0">
<div className="md:mr-4 flex-shrink-0">
<img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
alt=""
className="rounded md:rounded-lg shadow md:shadow-2xl w-32 md:w-52"
/>
</div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="text-white flex flex-col md:mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="mb-2">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">
@@ -227,8 +231,12 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
)}
</div>
<h1 className="text-2xl md:text-4xl">
{data.name}{' '}
<span className="text-2xl">({data.firstAirDate.slice(0, 4)})</span>
<span>{data.name}</span>
{data.firstAirDate && (
<span className="text-2xl ml-2">
({data.firstAirDate.slice(0, 4)})
</span>
)}
</h1>
<span className="text-xs md:text-base mt-1 md:mt-0">
{data.genres.map((g) => g.name).join(', ')}
@@ -344,7 +352,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="default"
className="ml-2"
className="ml-2 first:ml-0"
onClick={() => setShowManager(true)}
>
<svg
@@ -427,6 +435,18 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
)}
</div>
)}
{data.keywords.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID
) && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
{intl.formatMessage(messages.showtype)}
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{intl.formatMessage(messages.anime)}
</span>
</div>
)}
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.status} />
@@ -435,14 +455,32 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
{data.status}
</span>
</div>
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.originallanguage} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{data.originalLanguage}
</span>
</div>
{data.spokenLanguages.some(
(lng) => lng.iso_639_1 === data.originalLanguage
) && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.originallanguage} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{
data.spokenLanguages.find(
(lng) => lng.iso_639_1 === data.originalLanguage
)?.name
}
</span>
</div>
)}
{data.networks.length > 0 && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.network} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{data.networks.map((n) => n.name).join(', ')}
</span>
</div>
)}
</div>
</div>
</div>

View File

@@ -23,13 +23,12 @@ const messages = defineMessages({
'Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.',
settings: 'Manage Settings',
settingsDescription:
'Grants permission to modify all Overseerr settings. User must have this permission to be able to grant it to others.',
'Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.',
managerequests: 'Manage Requests',
managerequestsDescription:
'Grants permission to manage Overseerr requests. This includes approving and denying requests.',
request: 'Request',
requestDescription:
'Grants permission to make requests for movies or tv shows.',
requestDescription: 'Grants permission to request movies and series.',
vote: 'Vote',
voteDescription:
'Grants permission to vote on requests (voting not yet implemented)',
@@ -38,7 +37,7 @@ const messages = defineMessages({
'Grants auto approval for any requests made by this user.',
save: 'Save',
saving: 'Saving...',
usersaved: 'User succesfully saved',
usersaved: 'User saved',
userfail: 'Something went wrong saving the user.',
});

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import useSWR from 'swr';
import LoadingSpinner from '../Common/LoadingSpinner';
import type { User } from '../../../server/entity/User';
@@ -10,6 +10,11 @@ import { Permission } from '../../hooks/useUser';
import { useRouter } from 'next/router';
import Header from '../Common/Header';
import Table from '../Common/Table';
import Transition from '../Transition';
import Modal from '../Common/Modal';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
userlist: 'User List',
@@ -24,12 +29,46 @@ const messages = defineMessages({
admin: 'Admin',
user: 'User',
plexuser: 'Plex User',
deleteuser: 'Delete User',
userdeleted: 'User deleted',
userdeleteerror: 'Something went wrong deleting the user',
deleteconfirm:
'Are you sure you want to delete this user? All existing request data from this user will be removed.',
});
const UserList: React.FC = () => {
const intl = useIntl();
const router = useRouter();
const { data, error } = useSWR<User[]>('/api/v1/user');
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR<User[]>('/api/v1/user');
const [isDeleting, setDeleting] = useState(false);
const [deleteModal, setDeleteModal] = useState<{
isOpen: boolean;
user?: User;
}>({
isOpen: false,
});
const deleteUser = async () => {
setDeleting(true);
try {
await axios.delete(`/api/v1/user/${deleteModal.user?.id}`);
addToast(intl.formatMessage(messages.userdeleted), {
autoDismiss: true,
appearance: 'success',
});
setDeleteModal({ isOpen: false });
} catch (e) {
addToast(intl.formatMessage(messages.userdeleteerror), {
autoDismiss: true,
appearance: 'error',
});
} finally {
revalidate();
}
};
if (!data && !error) {
return <LoadingSpinner />;
@@ -37,6 +76,46 @@ const UserList: React.FC = () => {
return (
<>
<Transition
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={deleteModal.isOpen}
>
<Modal
onOk={() => deleteUser()}
okText={
isDeleting
? intl.formatMessage(globalMessages.deleting)
: intl.formatMessage(globalMessages.delete)
}
okDisabled={isDeleting}
okButtonType="danger"
onCancel={() => setDeleteModal({ isOpen: false })}
title={intl.formatMessage(messages.deleteuser)}
iconSvg={
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
}
>
{intl.formatMessage(messages.deleteconfirm)}
</Modal>
</Transition>
<Header extraMargin={4}>{intl.formatMessage(messages.userlist)}</Header>
<Table>
<thead>
@@ -104,7 +183,11 @@ const UserList: React.FC = () => {
>
{intl.formatMessage(messages.edit)}
</Button>
<Button buttonType="danger">
<Button
buttonType="danger"
disabled={hasPermission(Permission.ADMIN, user.permissions)}
onClick={() => setDeleteModal({ isOpen: true, user })}
>
{intl.formatMessage(messages.delete)}
</Button>
</Table.TD>

View File

@@ -0,0 +1,20 @@
import React from 'react';
import useInteraction from '../hooks/useInteraction';
interface InteractionContextProps {
isTouch: boolean;
}
export const InteractionContext = React.createContext<InteractionContextProps>({
isTouch: false,
});
export const InteractionProvider: React.FC = ({ children }) => {
const isTouch = useInteraction();
return (
<InteractionContext.Provider value={{ isTouch }}>
{children}
</InteractionContext.Provider>
);
};

View File

@@ -7,7 +7,8 @@ export type AvailableLocales =
| 'nb-NO'
| 'de'
| 'ru'
| 'nl';
| 'nl'
| 'es';
interface LanguageContextProps {
locale: AvailableLocales;

View File

@@ -15,7 +15,7 @@ const useClickOutside = (
): void => {
useEffect(() => {
const handleBodyClick = (e: MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) {
if (ref.current && !ref.current.contains(e.target as Node)) {
callback(e);
}
};

View File

@@ -0,0 +1,78 @@
import { useState, useEffect } from 'react';
export const INTERACTION_TYPE = {
MOUSE: 'mouse',
PEN: 'pen',
TOUCH: 'touch',
};
const UPDATE_INTERVAL = 1000; // Throttle updates to the type to prevent flip flopping
const useInteraction = (): boolean => {
const [isTouch, setIsTouch] = useState(false);
useEffect(() => {
const hasTapEvent = 'ontouchstart' in window;
setIsTouch(hasTapEvent);
let localTouch = hasTapEvent;
let lastTouchUpdate = Date.now();
const shouldUpdate = (): boolean =>
lastTouchUpdate + UPDATE_INTERVAL < Date.now();
const onMouseMove = (): void => {
if (localTouch && shouldUpdate()) {
setTimeout(() => {
if (shouldUpdate()) {
setIsTouch(false);
localTouch = false;
}
}, UPDATE_INTERVAL);
}
};
const onTouchStart = (): void => {
lastTouchUpdate = Date.now();
if (!localTouch) {
setIsTouch(true);
localTouch = true;
}
};
const onPointerMove = (e: PointerEvent): void => {
const { pointerType } = e;
switch (pointerType) {
case INTERACTION_TYPE.TOUCH:
case INTERACTION_TYPE.PEN:
return onTouchStart();
default:
return onMouseMove();
}
};
if (hasTapEvent) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchstart', onTouchStart);
} else {
window.addEventListener('pointerdown', onPointerMove);
window.addEventListener('pointermove', onPointerMove);
}
return () => {
if (hasTapEvent) {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchstart', onTouchStart);
} else {
window.removeEventListener('pointerdown', onPointerMove);
window.removeEventListener('pointermove', onPointerMove);
}
};
}, []);
return isTouch;
};
export default useInteraction;

7
src/hooks/useIsTouch.ts Normal file
View File

@@ -0,0 +1,7 @@
import { useContext } from 'react';
import { InteractionContext } from '../context/InteractionContext';
export const useIsTouch = (): boolean => {
const { isTouch } = useContext(InteractionContext);
return isTouch;
};

View File

@@ -7,6 +7,10 @@ import type { Nullable } from '../utils/typeHelpers';
type Url = string | UrlObject;
const encodeURIExtraParams = (string: string): string => {
return encodeURIComponent(string).replace(/!/g, '%21');
};
interface SearchObject {
searchValue: string;
searchOpen: boolean;
@@ -35,14 +39,17 @@ const useSearchInput = (): SearchObject => {
if (router.pathname.startsWith('/search')) {
router.replace({
pathname: router.pathname,
query: { ...router.query, query: debouncedValue },
query: {
...router.query,
query: encodeURIExtraParams(debouncedValue),
},
});
} else {
setLastRoute(router.asPath);
router
.push({
pathname: '/search',
query: { query: debouncedValue },
query: { query: encodeURIExtraParams(debouncedValue) },
})
.then(() => window.scrollTo(0, 0));
}
@@ -85,8 +92,12 @@ const useSearchInput = (): SearchObject => {
* is on /search
*/
useEffect(() => {
if (router.query.query !== debouncedValue) {
setSearchValue((router.query.query as string) ?? '');
if (router.query.query !== encodeURIExtraParams(debouncedValue)) {
setSearchValue(
router.query.query
? decodeURIComponent(router.query.query as string)
: ''
);
if (!router.pathname.startsWith('/search') && !router.query.query) {
setIsOpen(false);

View File

@@ -14,6 +14,7 @@ const globalMessages = defineMessages({
approve: 'Approve',
decline: 'Decline',
delete: 'Delete',
deleting: 'Deleting…',
});
export default globalMessages;

View File

@@ -1,9 +1,9 @@
{
"components.Discover.discovermovies": "Beliebte Filme",
"components.Discover.discovertv": "Beliebte Serie",
"components.Discover.discovertv": "Beliebte Serien",
"components.Discover.nopending": "Keine ausstehenden Anträge",
"components.Discover.popularmovies": "Beliebte Filme",
"components.Discover.populartv": "Beliebte Serie",
"components.Discover.populartv": "Beliebte Serien",
"components.Discover.recentlyAdded": "Kürzlich hinzugefügt",
"components.Discover.recentrequests": "Letzte Anträge",
"components.Discover.trending": "Trends",
@@ -45,24 +45,24 @@
"components.MovieDetails.unavailable": "Nicht verfügbar",
"components.MovieDetails.userrating": "Benutzerbewertung",
"components.MovieDetails.viewrequest": "Antrag anzeigen",
"components.PersonDetails.appearsin": "",
"components.PersonDetails.ascharacter": "",
"components.PersonDetails.nobiography": "",
"components.PersonDetails.appearsin": "Erscheint in",
"components.PersonDetails.ascharacter": "als {character}",
"components.PersonDetails.nobiography": "Keine Biografie verfügbar.",
"components.PlexLoginButton.loading": "Wird geladen …",
"components.PlexLoginButton.loggingin": "Wird angemeldet …",
"components.PlexLoginButton.loginwithplex": "Anmeldung mit Plex",
"components.RequestBlock.seasons": "Staffeln",
"components.RequestCard.all": "",
"components.RequestCard.all": "Alle",
"components.RequestCard.requestedby": "Antrag von {username}",
"components.RequestCard.seasons": "Staffeln",
"components.RequestList.RequestItem.notavailable": "entf.",
"components.RequestList.RequestItem.requestedby": "Antrag von {username}",
"components.RequestList.RequestItem.seasons": "Staffeln",
"components.RequestList.mediaInfo": "Medieninformationen",
"components.RequestList.mediaInfo": "Medieninfos",
"components.RequestList.modifiedBy": "Zuletzt geändert von",
"components.RequestList.next": "Nächste",
"components.RequestList.previous": "Vorherige",
"components.RequestList.requestedAt": "Antrag erstellt um",
"components.RequestList.requestedAt": "Antrag erstellt am",
"components.RequestList.requests": "Anträge",
"components.RequestList.showingresults": "Anzeigen von <strong>{from}</strong> bis <strong> {to} </strong> von <strong> {total} </strong> Ergebnissen",
"components.RequestList.status": "Status",
@@ -88,22 +88,22 @@
"components.RequestModal.status": "Status",
"components.Search.searchresults": "Suchergebnisse",
"components.Settings.Notifications.agentenabled": "Agent aktiviert",
"components.Settings.Notifications.authPass": "",
"components.Settings.Notifications.authUser": "",
"components.Settings.Notifications.authPass": "Authentifizierungspasswort",
"components.Settings.Notifications.authUser": "Authentifizierungsbenutzername",
"components.Settings.Notifications.emailsender": "E-Mail-Absenderadresse",
"components.Settings.Notifications.enableSsl": "SSL aktivieren",
"components.Settings.Notifications.save": "",
"components.Settings.Notifications.saving": "",
"components.Settings.Notifications.smtpHost": "",
"components.Settings.Notifications.smtpPort": "",
"components.Settings.Notifications.validationFromRequired": "",
"components.Settings.Notifications.validationSmtpHostRequired": "",
"components.Settings.Notifications.save": "Änderungen speichern",
"components.Settings.Notifications.saving": "Speichern ",
"components.Settings.Notifications.smtpHost": "SMTP-Host",
"components.Settings.Notifications.smtpPort": "SMTP-Port",
"components.Settings.Notifications.validationFromRequired": "Sie müssen eine E-Mail-Absenderadresse angeben",
"components.Settings.Notifications.validationSmtpHostRequired": "Sie müssen einen SMTP-Host bereitstellen",
"components.Settings.Notifications.validationSmtpPortRequired": "Sie müssen einen SMTP-Port bereitstellen",
"components.Settings.Notifications.validationWebhookUrlRequired": "Sie müssen eine Webhook-URL angeben",
"components.Settings.Notifications.webhookUrl": "Webhook-URL",
"components.Settings.Notifications.webhookUrlPlaceholder": "Servereinstellungen -> Integrationen -> Webhooks",
"components.Settings.RadarrModal.add": "",
"components.Settings.RadarrModal.apiKey": "",
"components.Settings.RadarrModal.add": "Server hinzufügen",
"components.Settings.RadarrModal.apiKey": "API-Schlüssel",
"components.Settings.RadarrModal.apiKeyPlaceholder": "Ihr Radarr-API-Schlüssel",
"components.Settings.RadarrModal.baseUrl": "Basis-URL",
"components.Settings.RadarrModal.baseUrlPlaceholder": "Beispiel: /radarr",
@@ -138,169 +138,188 @@
"components.Settings.SonarrModal.apiKeyPlaceholder": "Ihr Sonarr-API-Schlüssel",
"components.Settings.SonarrModal.baseUrl": "Basis-URL",
"components.Settings.SonarrModal.baseUrlPlaceholder": "Beispiel: /sonarr",
"components.Settings.SonarrModal.createsonarr": "",
"components.Settings.SonarrModal.defaultserver": "",
"components.Settings.SonarrModal.editsonarr": "",
"components.Settings.SonarrModal.hostname": "",
"components.Settings.SonarrModal.port": "",
"components.Settings.SonarrModal.qualityprofile": "",
"components.Settings.SonarrModal.rootfolder": "",
"components.Settings.SonarrModal.save": "",
"components.Settings.SonarrModal.saving": "",
"components.Settings.SonarrModal.seasonfolders": "",
"components.Settings.SonarrModal.selectQualityProfile": "",
"components.Settings.SonarrModal.selectRootFolder": "",
"components.Settings.SonarrModal.server4k": "",
"components.Settings.SonarrModal.servername": "",
"components.Settings.SonarrModal.servernamePlaceholder": "",
"components.Settings.SonarrModal.ssl": "",
"components.Settings.SonarrModal.test": "",
"components.Settings.SonarrModal.testing": "",
"components.Settings.SonarrModal.toastRadarrTestFailure": "",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "",
"components.Settings.SonarrModal.validationApiKeyRequired": "",
"components.Settings.SonarrModal.validationHostnameRequired": "",
"components.Settings.SonarrModal.validationPortRequired": "",
"components.Settings.SonarrModal.validationProfileRequired": "",
"components.Settings.SonarrModal.validationRootFolderRequired": "",
"components.Settings.activeProfile": "",
"components.Settings.addradarr": "",
"components.Settings.address": "",
"components.Settings.addsonarr": "",
"components.Settings.apikey": "",
"components.Settings.applicationurl": "",
"components.Settings.cancelscan": "",
"components.Settings.copied": "",
"components.Settings.currentlibrary": "",
"components.Settings.default": "",
"components.Settings.default4k": "",
"components.Settings.delete": "",
"components.Settings.deleteserverconfirm": "",
"components.Settings.edit": "",
"components.Settings.generalsettings": "",
"components.Settings.generalsettingsDescription": "",
"components.Settings.hostname": "",
"components.Settings.jobname": "",
"components.Settings.librariesRemaining": "",
"components.Settings.manualscan": "",
"components.Settings.manualscanDescription": "",
"components.Settings.menuAbout": "",
"components.Settings.menuGeneralSettings": "",
"components.Settings.menuJobs": "",
"components.Settings.menuLogs": "",
"components.Settings.menuNotifications": "",
"components.Settings.menuPlexSettings": "",
"components.Settings.menuServices": "",
"components.Settings.nextexecution": "",
"components.Settings.notificationsettings": "",
"components.Settings.notificationsettingsDescription": "",
"components.Settings.notrunning": "",
"components.Settings.plexlibraries": "",
"components.Settings.plexlibrariesDescription": "",
"components.Settings.plexsettings": "",
"components.Settings.plexsettingsDescription": "",
"components.Settings.port": "",
"components.Settings.radarrSettingsDescription": "",
"components.Settings.radarrsettings": "",
"components.Settings.runnow": "",
"components.Settings.save": "",
"components.Settings.saving": "",
"components.Settings.servername": "",
"components.Settings.servernamePlaceholder": "",
"components.Settings.sonarrSettingsDescription": "",
"components.Settings.sonarrsettings": "",
"components.Settings.ssl": "",
"components.Settings.startscan": "",
"components.Settings.sync": "",
"components.Settings.syncing": "",
"components.Setup.configureplex": "",
"components.Setup.configureservices": "",
"components.Setup.continue": "",
"components.Setup.finish": "",
"components.Setup.finishing": "",
"components.Setup.loginwithplex": "",
"components.Setup.signinMessage": "",
"components.Setup.welcome": "",
"components.Slider.noresults": "",
"components.TitleCard.movie": "",
"components.TitleCard.tvshow": "",
"components.TvDetails.approve": "",
"components.TvDetails.approverequests": "",
"components.TvDetails.available": "",
"components.TvDetails.cancelrequest": "",
"components.TvDetails.cast": "",
"components.TvDetails.decline": "",
"components.TvDetails.declinerequests": "",
"components.TvDetails.manageModalClearMedia": "",
"components.TvDetails.manageModalClearMediaWarning": "",
"components.TvDetails.manageModalNoRequests": "",
"components.TvDetails.manageModalRequests": "",
"components.TvDetails.manageModalTitle": "",
"components.TvDetails.originallanguage": "",
"components.TvDetails.overview": "",
"components.TvDetails.overviewunavailable": "",
"components.TvDetails.pending": "",
"components.TvDetails.recommendations": "",
"components.TvDetails.recommendationssubtext": "",
"components.TvDetails.request": "",
"components.TvDetails.requestmore": "",
"components.TvDetails.similar": "",
"components.TvDetails.similarsubtext": "",
"components.TvDetails.status": "",
"components.TvDetails.unavailable": "",
"components.TvDetails.userrating": "",
"components.UserEdit.admin": "",
"components.UserEdit.adminDescription": "",
"components.UserEdit.autoapprove": "",
"components.UserEdit.autoapproveDescription": "",
"components.UserEdit.avatar": "",
"components.UserEdit.edituser": "",
"components.UserEdit.email": "",
"components.UserEdit.managerequests": "",
"components.UserEdit.managerequestsDescription": "",
"components.UserEdit.permissions": "",
"components.UserEdit.request": "",
"components.UserEdit.requestDescription": "",
"components.UserEdit.save": "",
"components.UserEdit.saving": "",
"components.UserEdit.settings": "",
"components.UserEdit.settingsDescription": "",
"components.UserEdit.userfail": "",
"components.UserEdit.username": "",
"components.UserEdit.users": "",
"components.UserEdit.usersDescription": "",
"components.UserEdit.usersaved": "",
"components.UserEdit.vote": "",
"components.UserEdit.voteDescription": "",
"components.UserList.admin": "",
"components.UserList.created": "",
"components.UserList.delete": "",
"components.UserList.edit": "",
"components.UserList.lastupdated": "",
"components.UserList.plexuser": "",
"components.UserList.role": "",
"components.UserList.totalrequests": "",
"components.UserList.user": "",
"components.UserList.userlist": "",
"components.UserList.username": "",
"components.UserList.usertype": "",
"i18n.approve": "",
"i18n.approved": "",
"i18n.available": "",
"i18n.cancel": "",
"i18n.decline": "",
"i18n.declined": "",
"i18n.delete": "",
"i18n.movies": "",
"i18n.partiallyavailable": "",
"i18n.pending": "",
"i18n.processing": "",
"i18n.tvshows": "",
"i18n.unavailable": "",
"pages.internalServerError": "",
"pages.oops": "",
"pages.pageNotFound": "",
"pages.returnHome": "",
"pages.serviceUnavailable": "",
"pages.somethingWentWrong": ""
"components.Settings.SonarrModal.createsonarr": "Neuen Sonarr-Server erstellen",
"components.Settings.SonarrModal.defaultserver": "Standardserver",
"components.Settings.SonarrModal.editsonarr": "Sonarr-Server bearbeiten",
"components.Settings.SonarrModal.hostname": "Hostname",
"components.Settings.SonarrModal.port": "Port",
"components.Settings.SonarrModal.qualityprofile": "Qualitätsprofil",
"components.Settings.SonarrModal.rootfolder": "Stammordner",
"components.Settings.SonarrModal.save": "Änderungen speichern",
"components.Settings.SonarrModal.saving": "Speichern ",
"components.Settings.SonarrModal.seasonfolders": "Staffelnordner",
"components.Settings.SonarrModal.selectQualityProfile": "Wählen Sie ein Qualitätsprofil",
"components.Settings.SonarrModal.selectRootFolder": "Wählen Sie einen Stammordner",
"components.Settings.SonarrModal.server4k": "4K-Server",
"components.Settings.SonarrModal.servername": "Servername",
"components.Settings.SonarrModal.servernamePlaceholder": "Ein Sonarr-Server",
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.test": "Test",
"components.Settings.SonarrModal.testing": "Testen ",
"components.Settings.SonarrModal.toastRadarrTestFailure": "Es konnte keine Verbindung zu Sonarr-Server hergestellt werden",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr-Verbindung hergestellt!",
"components.Settings.SonarrModal.validationApiKeyRequired": "Sie müssen einen API-Schlüssel angeben",
"components.Settings.SonarrModal.validationHostnameRequired": "Sie müssen einen Hostnamen/IP angeben",
"components.Settings.SonarrModal.validationPortRequired": "Sie müssen einen Port angeben",
"components.Settings.SonarrModal.validationProfileRequired": "Sie müssen ein Profil auswählen",
"components.Settings.SonarrModal.validationRootFolderRequired": "Sie müssen einen Stammordner auswählen",
"components.Settings.activeProfile": "Aktives Profil",
"components.Settings.addradarr": "Radarr-Server hinzufügen",
"components.Settings.address": "Adresse",
"components.Settings.addsonarr": "Sonarr-Server hinzufügen",
"components.Settings.apikey": "API-Schlüssel",
"components.Settings.applicationurl": "Anwendungs-URL",
"components.Settings.cancelscan": "Scan abbrechen",
"components.Settings.copied": "Kopierter API-Schlüssel in die Zwischenablage",
"components.Settings.currentlibrary": "Aktuelle Bibliothek: {name}",
"components.Settings.default": "Standardmäßig",
"components.Settings.default4k": "Standard-4K",
"components.Settings.delete": "Löschen",
"components.Settings.deleteserverconfirm": "Sind Sie sicher, dass Sie diesen Server löschen möchten?",
"components.Settings.edit": "Bearbeiten",
"components.Settings.generalsettings": "Allgemeine Einstellungen",
"components.Settings.generalsettingsDescription": "Dies sind Einstellungen, die sich auf die allgemeine Overseerr-Konfiguration beziehen.",
"components.Settings.hostname": "Hostname/IP",
"components.Settings.jobname": "Auftragsname",
"components.Settings.librariesRemaining": "Verbleibende Bibliotheken: {count}",
"components.Settings.manualscan": "Manueller Bibliotheksscan",
"components.Settings.manualscanDescription": "Normalerweise wird dies nur einmal alle 6 Stunden ausgeführt. Overseerr überprüft die kürzlich hinzugefügten Plex-Server aggressiver. Wenn Sie Plex zum ersten Mal konfigurieren, wird ein einmaliger vollständiger manueller Bibliotheksscan empfohlen!",
"components.Settings.menuAbout": "Über",
"components.Settings.menuGeneralSettings": "Allgemeine Einstellungen",
"components.Settings.menuJobs": "Anträge",
"components.Settings.menuLogs": "Protokolle",
"components.Settings.menuNotifications": "Benachrichtigungen",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Dienste",
"components.Settings.nextexecution": "Nächste Ausführung",
"components.Settings.notificationsettings": "Benachrichtigungseinstellungen",
"components.Settings.notificationsettingsDescription": "Hier können Sie auswählen, welche Arten von Benachrichtigungen gesendet werden sollen und über welche Arten von Diensten.",
"components.Settings.notrunning": "Nicht aktiv",
"components.Settings.plexlibraries": "Plex-Bibliotheken",
"components.Settings.plexlibrariesDescription": "Die Bibliotheken-Overseerr sucht nach Titeln. Richten Sie Ihre Plex-Verbindungseinstellungen ein und speichern Sie sie, und klicken Sie auf die Schaltfläche unten, wenn keine aufgeführt sind.",
"components.Settings.plexsettings": "Plex-Einstellungen",
"components.Settings.plexsettingsDescription": "Konfigurieren Sie die Einstellungen für Ihren Plex-Server. Overseerr verwendet Ihren Plex-Server, um Ihre Bibliothek in regelmäßigen Abständen zu scannen und festzustellen, welche Inhalte verfügbar sind.",
"components.Settings.port": "Port",
"components.Settings.radarrSettingsDescription": "Richten Sie unten Ihre Radarr-Verbindung ein. Sie können jederzeit mehrere, aber nur zwei standardmäßig aktiv haben (eine für Standard-HD und eine für 4K). Administratoren können überschreiben, welcher Server für neue Anforderungen verwendet wird.",
"components.Settings.radarrsettings": "Radarr-Einstellungen",
"components.Settings.runnow": "Jetzt ausführen",
"components.Settings.save": "Änderungen speichern",
"components.Settings.saving": "Speichern ",
"components.Settings.servername": "Servername (Wird nach dem Speichern automatisch festgelegt)",
"components.Settings.servernamePlaceholder": "Plex-Servername",
"components.Settings.sonarrSettingsDescription": "Richten Sie unten Ihre Sonarr-Verbindung ein. Sie können jederzeit mehrere, aber nur zwei standardmäßig aktiv haben (eine für Standard-HD und eine für 4K). Administratoren können überschreiben, welcher Server für neue Anforderungen verwendet wird.",
"components.Settings.sonarrsettings": "Sonarr-Einstellungen",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Scan starten",
"components.Settings.sync": "Plex-Bibliotheken synchronisieren",
"components.Settings.syncing": "Synchronisierung ",
"components.Setup.configureplex": "Plex konfigurieren",
"components.Setup.configureservices": "Dienste konfigurieren",
"components.Setup.continue": "Fortfahren",
"components.Setup.finish": "Konfiguration beenden",
"components.Setup.finishing": "Fertigstellung ",
"components.Setup.loginwithplex": "Anmeldung mit Plex",
"components.Setup.signinMessage": "Melden Sie sich zunächst mit Ihrem Plex-Konto an",
"components.Setup.welcome": "Willkommen bei Overseerr",
"components.Slider.noresults": "Keine Ergebnisse",
"components.TitleCard.movie": "Film",
"components.TitleCard.tvshow": "Serie",
"components.TvDetails.approve": "Genehmigen",
"components.TvDetails.approverequests": "{requestCount} {requestCount, plural, one {Request} other {Requests}} genehmigen",
"components.TvDetails.available": "Verfügbar",
"components.TvDetails.cancelrequest": "Antrag abbrechen",
"components.TvDetails.cast": "Besetzung",
"components.TvDetails.decline": "Ablehnen",
"components.TvDetails.declinerequests": "{requestCount} {requestCount, plural, one {Request} andere {Requests}} ablehnen",
"components.TvDetails.manageModalClearMedia": "Alle Mediendaten löschen",
"components.TvDetails.manageModalClearMediaWarning": "Dadurch werden alle Mediendaten einschließlich aller Anträge für dieses Element irreversibel entfernt. Wenn dieses Element in Ihrer Plex-Bibliothek vorhanden ist, werden die Medieninformationen bei der nächsten Synchronisierung neu erstellt.",
"components.TvDetails.manageModalNoRequests": "Keine Anträge",
"components.TvDetails.manageModalRequests": "Anträge",
"components.TvDetails.manageModalTitle": "Serien verwalten",
"components.TvDetails.originallanguage": "Originalsprache",
"components.TvDetails.overview": "Übersicht",
"components.TvDetails.overviewunavailable": "Übersicht nicht verfügbar",
"components.TvDetails.pending": "Ausstehend",
"components.TvDetails.recommendations": "Empfehlungen",
"components.TvDetails.recommendationssubtext": "Wenn Ihnen {title} gefallen hat, könnte Ihnen auch gefallen ",
"components.TvDetails.request": "Antrag",
"components.TvDetails.requestmore": "Mehr anfordern",
"components.TvDetails.similar": "Ähnliche Serien",
"components.TvDetails.similarsubtext": "Andere Serien {title} ähnlich",
"components.TvDetails.status": "Status",
"components.TvDetails.unavailable": "Nicht verfügbar",
"components.TvDetails.userrating": "Benutzerbewertung",
"components.UserEdit.admin": "Administrator/in",
"components.UserEdit.adminDescription": "Voller Administratorzugriff. Umgeht alle Berechtigungsprüfungen.",
"components.UserEdit.autoapprove": "Automatische Genehmigung",
"components.UserEdit.autoapproveDescription": "Gewährt die automatische Genehmigung für alle Anträge dieses Benutzers.",
"components.UserEdit.avatar": "Avatar",
"components.UserEdit.edituser": "Benutzer/in bearbeiten",
"components.UserEdit.email": "E-Mail",
"components.UserEdit.managerequests": "Anträge verwalten",
"components.UserEdit.managerequestsDescription": "Erteilt die Berechtigung zum Verwalten von Overseerr-Anträgen. Dies umfasst das Genehmigen und Ablehnen von Anträgen.",
"components.UserEdit.permissions": "Berechtigungen",
"components.UserEdit.request": "Antrag",
"components.UserEdit.requestDescription": "Erteilt die Berechtigung zum Anfordern von Filmen und Serien.",
"components.UserEdit.save": "Speichern",
"components.UserEdit.saving": "Speichern ",
"components.UserEdit.settings": "Einstellungen verwalten",
"components.UserEdit.settingsDescription": "Erteilt die Berechtigung zum Ändern aller Overseerr-Einstellungen. Ein/e Benutzer/in muss über diese Berechtigung verfügen, um sie anderen Personen erteilen zu können.",
"components.UserEdit.userfail": "Beim Speichern des Benutzers ist ein Fehler aufgetreten.",
"components.UserEdit.username": "Benutzername",
"components.UserEdit.users": "Benutzer verwalten",
"components.UserEdit.usersDescription": "Erteilt die Berechtigung zum Verwalten von Overseerr-Benutzern. Benutzer/innen mit dieser Berechtigung können Benutzer/innen mit Administratorrechten nicht ändern oder gewähren.",
"components.UserEdit.usersaved": "Benutzer/in gespeichert",
"components.UserEdit.vote": "Abstimmen",
"components.UserEdit.voteDescription": "Erteilt die Erlaubnis, über Anträge abzustimmen (Abstimmung noch nicht durchgeführt)",
"components.UserList.admin": "Admin",
"components.UserList.created": "Erstellt",
"components.UserList.delete": "Löschen",
"components.UserList.edit": "Bearbeiten",
"components.UserList.lastupdated": "Zuletzt aktualisiert",
"components.UserList.plexuser": "Plex-Benutzer/in",
"components.UserList.role": "Rolle",
"components.UserList.totalrequests": "Anträge insgesamt",
"components.UserList.user": "Benutzer/in",
"components.UserList.userlist": "Benutzer/innen-Liste",
"components.UserList.username": "Benutzername",
"components.UserList.usertype": "Benutzertyp",
"i18n.approve": "Genehmigen",
"i18n.approved": "Genehmigt",
"i18n.available": "Verfügbar",
"i18n.cancel": "Abbrechen",
"i18n.decline": "Ablehnen",
"i18n.declined": "Abgelehnt",
"i18n.delete": "Löschen",
"i18n.movies": "Films",
"i18n.partiallyavailable": "Teilweise verfügbar",
"i18n.pending": "Ausstehend",
"i18n.processing": "Verarbeitung ",
"i18n.tvshows": "Serien",
"i18n.unavailable": "Nicht verfügbar",
"pages.internalServerError": "{statusCode} Interner Serverfehler",
"pages.oops": "Hoppla",
"pages.pageNotFound": "404 Seite nicht gefunden",
"pages.returnHome": "Zur Starseite",
"pages.serviceUnavailable": "{statusCode} Dienst nicht verfügbar",
"pages.somethingWentWrong": "{statusCode} Es ist ein Fehler aufgetreten",
"components.TvDetails.TvCast.fullseriescast": "Vollserienbesetzung",
"components.MovieDetails.MovieCast.fullcast": "Vollständige Besetzung",
"components.Settings.Notifications.emailsettingssaved": "E-Mail-Benachrichtigungseinstellungen gespeichert!",
"components.Settings.Notifications.emailsettingsfailed": "Die Einstellungen für die E-Mail-Benachrichtigungen konnten nicht gespeichert werden.",
"components.Settings.Notifications.discordsettingssaved": "Discord-Benachrichtigungseinstellungen gespeichert!",
"components.Settings.Notifications.discordsettingsfailed": "Die Einstellungen für die Discord-Benachrichtigungen konnten nicht gespeichert werden.",
"components.Settings.validationPortRequired": "Sie müssen einen Port angeben",
"components.Settings.validationHostnameRequired": "Sie müssen einen Hostnamen/IP angeben",
"components.Settings.SonarrModal.validationNameRequired": "Sie müssen einen Servernamen angeben",
"components.Settings.SettingsAbout.version": "Version",
"components.Settings.SettingsAbout.totalrequests": "Anträge insgesamt",
"components.Settings.SettingsAbout.totalmedia": "Medien insgesamt",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr-Informationen",
"components.Settings.SettingsAbout.githubdiscussions": "GitHub-Diskussionen",
"components.Settings.SettingsAbout.gettingsupport": "Hilfe erhalten",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Klicken Sie hier, um unserem Discord-Server beizutreten.",
"components.Settings.RadarrModal.validationNameRequired": "Sie müssen einen Servernamen angeben",
"components.Setup.tip": "Tipp",
"components.Setup.syncingbackground": "Die Synchronisierung wird im Hintergrund ausgeführt. Sie können die Konfiguration in der Zwischenzeit fortsetzen."
}

View File

@@ -18,6 +18,7 @@
"components.Layout.UserDropdown.signout": "Sign Out",
"components.Layout.alphawarning": "This is ALPHA software. Almost everything is bound to be nearly broken and/or unstable. Please report issues to the Overseerr GitHub!",
"components.Login.signinplex": "Sign in to continue",
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
"components.MovieDetails.approve": "Approve",
"components.MovieDetails.available": "Available",
"components.MovieDetails.budget": "Budget",
@@ -42,6 +43,7 @@
"components.MovieDetails.similar": "Similar Titles",
"components.MovieDetails.similarsubtext": "Other movies similar to {title}",
"components.MovieDetails.status": "Status",
"components.MovieDetails.studio": "Studio",
"components.MovieDetails.unavailable": "Unavailable",
"components.MovieDetails.userrating": "User Rating",
"components.MovieDetails.viewrequest": "View Request",
@@ -90,7 +92,11 @@
"components.Settings.Notifications.agentenabled": "Agent Enabled",
"components.Settings.Notifications.authPass": "Auth Pass",
"components.Settings.Notifications.authUser": "Auth User",
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved!",
"components.Settings.Notifications.emailsender": "Email Sender Address",
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved!",
"components.Settings.Notifications.enableSsl": "Enable SSL",
"components.Settings.Notifications.save": "Save Changes",
"components.Settings.Notifications.saving": "Saving...",
@@ -111,6 +117,8 @@
"components.Settings.RadarrModal.defaultserver": "Default Server",
"components.Settings.RadarrModal.editradarr": "Edit Radarr Server",
"components.Settings.RadarrModal.hostname": "Hostname",
"components.Settings.RadarrModal.loadingprofiles": "Loading quality profiles…",
"components.Settings.RadarrModal.loadingrootfolders": "Loading root folders…",
"components.Settings.RadarrModal.minimumAvailability": "Minimum Availability",
"components.Settings.RadarrModal.port": "Port",
"components.Settings.RadarrModal.qualityprofile": "Quality Profile",
@@ -125,15 +133,28 @@
"components.Settings.RadarrModal.servernamePlaceholder": "A Radarr Server",
"components.Settings.RadarrModal.ssl": "SSL",
"components.Settings.RadarrModal.test": "Test",
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test your connection to load quality profiles",
"components.Settings.RadarrModal.testFirstRootFolders": "Test your connection to load root folders",
"components.Settings.RadarrModal.testing": "Testing...",
"components.Settings.RadarrModal.toastRadarrTestFailure": "Failed to connect to Radarr Server",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!",
"components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key",
"components.Settings.RadarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "You must select minimum availability",
"components.Settings.RadarrModal.validationNameRequired": "You must provide a server name",
"components.Settings.RadarrModal.validationPortRequired": "You must provide a port",
"components.Settings.RadarrModal.validationProfileRequired": "You must select a profile",
"components.Settings.RadarrModal.validationRootFolderRequired": "You must select a root folder",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Click here to join our Discord server.",
"components.Settings.SettingsAbout.gettingsupport": "Getting Support",
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr Information",
"components.Settings.SettingsAbout.totalmedia": "Total Media",
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
"components.Settings.SettingsAbout.version": "Version",
"components.Settings.SonarrModal.add": "Add Server",
"components.Settings.SonarrModal.animequalityprofile": "Anime Quality Profile",
"components.Settings.SonarrModal.animerootfolder": "Anime Root Folder",
"components.Settings.SonarrModal.apiKey": "API Key",
"components.Settings.SonarrModal.apiKeyPlaceholder": "Your Sonarr API Key",
"components.Settings.SonarrModal.baseUrl": "Base URL",
@@ -142,6 +163,8 @@
"components.Settings.SonarrModal.defaultserver": "Default Server",
"components.Settings.SonarrModal.editsonarr": "Edit Sonarr Server",
"components.Settings.SonarrModal.hostname": "Hostname",
"components.Settings.SonarrModal.loadingprofiles": "Loading quality profiles…",
"components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…",
"components.Settings.SonarrModal.port": "Port",
"components.Settings.SonarrModal.qualityprofile": "Quality Profile",
"components.Settings.SonarrModal.rootfolder": "Root Folder",
@@ -155,11 +178,14 @@
"components.Settings.SonarrModal.servernamePlaceholder": "A Sonarr Server",
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.test": "Test",
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test your connection to load quality profiles",
"components.Settings.SonarrModal.testFirstRootFolders": "Test your connection to load root folders",
"components.Settings.SonarrModal.testing": "Testing...",
"components.Settings.SonarrModal.toastRadarrTestFailure": "Could not connect to Sonarr Server",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!",
"components.Settings.SonarrModal.validationApiKeyRequired": "You must provide an API key",
"components.Settings.SonarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.SonarrModal.validationNameRequired": "You must provide a server name",
"components.Settings.SonarrModal.validationPortRequired": "You must provide a port",
"components.Settings.SonarrModal.validationProfileRequired": "You must select a profile",
"components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder",
@@ -192,6 +218,9 @@
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.nextexecution": "Next Execution",
"components.Settings.no4kimplemented": "(Default 4K servers are not currently implemented)",
"components.Settings.nodefault": "No default server selected!",
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
"components.Settings.notificationsettings": "Notification Settings",
"components.Settings.notificationsettingsDescription": "Here you can pick and choose what types of notifications to send and through what types of services.",
"components.Settings.notrunning": "Not Running",
@@ -205,14 +234,20 @@
"components.Settings.runnow": "Run Now",
"components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving...",
"components.Settings.servername": "Server Name (Automatically Set)",
"components.Settings.servername": "Server Name (Automatically set after you save)",
"components.Settings.servernamePlaceholder": "Plex Server Name",
"components.Settings.sonarrSettingsDescription": "Set up your Sonarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD and one for 4K). Administrators can override which server is used for new requests.",
"components.Settings.sonarrsettings": "Sonarr Settings",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Start Scan",
"components.Settings.sync": "Sync Plex Libraries",
"components.Settings.syncing": "Syncing",
"components.Settings.syncing": "Syncing",
"components.Settings.toastApiKeyFailure": "Something went wrong generating a new API Key.",
"components.Settings.toastApiKeySuccess": "New API Key generated!",
"components.Settings.toastSettingsFailure": "Something went wrong saving settings.",
"components.Settings.toastSettingsSuccess": "Settings saved.",
"components.Settings.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.validationPortRequired": "You must provide a port",
"components.Setup.configureplex": "Configure Plex",
"components.Setup.configureservices": "Configure Services",
"components.Setup.continue": "Continue",
@@ -220,10 +255,14 @@
"components.Setup.finishing": "Finishing...",
"components.Setup.loginwithplex": "Login with Plex",
"components.Setup.signinMessage": "Get started by logging in with your Plex account",
"components.Setup.syncingbackground": "Syncing will run in the background. You can continue the setup process in the meantime.",
"components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Overseerr",
"components.Slider.noresults": "No Results",
"components.TitleCard.movie": "Movie",
"components.TitleCard.tvshow": "Series",
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
"components.TvDetails.anime": "Anime",
"components.TvDetails.approve": "Approve",
"components.TvDetails.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.available": "Available",
@@ -236,6 +275,7 @@
"components.TvDetails.manageModalNoRequests": "No Requests",
"components.TvDetails.manageModalRequests": "Requests",
"components.TvDetails.manageModalTitle": "Manage Series",
"components.TvDetails.network": "Network",
"components.TvDetails.originallanguage": "Original Language",
"components.TvDetails.overview": "Overview",
"components.TvDetails.overviewunavailable": "Overview unavailable",
@@ -244,6 +284,7 @@
"components.TvDetails.recommendationssubtext": "If you liked {title}, you might also like...",
"components.TvDetails.request": "Request",
"components.TvDetails.requestmore": "Request More",
"components.TvDetails.showtype": "Show Type",
"components.TvDetails.similar": "Similar Series",
"components.TvDetails.similarsubtext": "Other series similar to {title}",
"components.TvDetails.status": "Status",
@@ -260,7 +301,7 @@
"components.UserEdit.managerequestsDescription": "Grants permission to manage Overseerr requests. This includes approving and denying requests.",
"components.UserEdit.permissions": "Permissions",
"components.UserEdit.request": "Request",
"components.UserEdit.requestDescription": "Grants permission to make requests for movies or tv shows.",
"components.UserEdit.requestDescription": "Grants permission to request movies and series.",
"components.UserEdit.save": "Save",
"components.UserEdit.saving": "Saving...",
"components.UserEdit.settings": "Manage Settings",
@@ -275,12 +316,16 @@
"components.UserList.admin": "Admin",
"components.UserList.created": "Created",
"components.UserList.delete": "Delete",
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All existing request data from this user will be removed.",
"components.UserList.deleteuser": "Delete User",
"components.UserList.edit": "Edit",
"components.UserList.lastupdated": "Last Updated",
"components.UserList.plexuser": "Plex User",
"components.UserList.role": "Role",
"components.UserList.totalrequests": "Total Requests",
"components.UserList.user": "User",
"components.UserList.userdeleted": "User deleted",
"components.UserList.userdeleteerror": "Something went wrong deleting the user",
"components.UserList.userlist": "User List",
"components.UserList.username": "Username",
"components.UserList.usertype": "User Type",
@@ -291,10 +336,11 @@
"i18n.decline": "Decline",
"i18n.declined": "Declined",
"i18n.delete": "Delete",
"i18n.deleting": "Deleting…",
"i18n.movies": "Movies",
"i18n.partiallyavailable": "Partially Available",
"i18n.pending": "Pending",
"i18n.processing": "Processing",
"i18n.processing": "Processing",
"i18n.tvshows": "Series",
"i18n.unavailable": "Unavailable",
"pages.internalServerError": "{statusCode} - Internal Server Error",

325
src/i18n/locale/es.json Normal file
View File

@@ -0,0 +1,325 @@
{
"components.Settings.SonarrModal.toastRadarrTestFailure": "Error al connectar al Servidor Sonarr",
"components.Settings.SonarrModal.testing": "Comprobando…",
"components.Settings.SonarrModal.test": "Comprobar",
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.servernamePlaceholder": "Un servidor Sonarr",
"components.Settings.SonarrModal.servername": "Nombre del Servidor",
"components.Settings.SonarrModal.server4k": "Servidor 4K",
"components.Settings.SonarrModal.selectRootFolder": "Selecciona la Carpeta Raíz",
"components.Settings.SonarrModal.selectQualityProfile": "Selecciona un Perfil de Calidad",
"components.Settings.SonarrModal.seasonfolders": "Carpetas por Temporada",
"components.Settings.SonarrModal.saving": "Guardando…",
"components.Settings.SonarrModal.save": "Guardar Cambios",
"components.Settings.SonarrModal.rootfolder": "Carpeta Raíz",
"components.Settings.SonarrModal.qualityprofile": "Perfil de Calidad",
"components.Settings.SonarrModal.port": "Puerto",
"components.Settings.SonarrModal.hostname": "Nombre de Host",
"components.Settings.SonarrModal.editsonarr": "Editar Servidor Sonarr",
"components.Settings.SonarrModal.defaultserver": "Servidor por Defecto",
"components.Settings.SonarrModal.createsonarr": "Crear Nuevo Servidor Sonarr",
"components.Settings.SonarrModal.baseUrlPlaceholder": "Ejemplo: /sonarr",
"components.Settings.SonarrModal.baseUrl": "URL Base",
"components.Settings.SonarrModal.apiKeyPlaceholder": "Tu Clave API de Sonarr",
"components.Settings.SonarrModal.apiKey": "Clave API",
"components.Settings.SonarrModal.add": "Agregar Servidor",
"components.Settings.SettingsAbout.version": "Versión",
"components.Settings.SettingsAbout.totalrequests": "Peticiones Totales",
"components.Settings.SettingsAbout.totalmedia": "Contenido Total",
"components.Settings.SettingsAbout.overseerrinformation": "Información de Overseerr",
"components.Settings.SettingsAbout.githubdiscussions": "Discursiones en GitHub",
"components.Settings.SettingsAbout.gettingsupport": "Soporte",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Click aquín para unirte a nuestro servidor de Discord.",
"components.Settings.RadarrModal.validationRootFolderRequired": "Debes seleccionar una carpeta raíz",
"components.Settings.RadarrModal.validationProfileRequired": "Debes seleccionar un perfil",
"components.Settings.RadarrModal.validationPortRequired": "Debes proporcionar un puerto",
"components.Settings.RadarrModal.validationNameRequired": "Debes proporcionar un nombre de servidor",
"components.Settings.RadarrModal.validationHostnameRequired": "Debes proporcionar un hostname/IP",
"components.Settings.RadarrModal.validationApiKeyRequired": "Debes proporcionar la clave API",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "¡Conexión con Radarr establecida!",
"components.Settings.RadarrModal.toastRadarrTestFailure": "Error al connectar al Servidor Radarr",
"components.Settings.RadarrModal.testing": "Comprobando…",
"components.Settings.RadarrModal.test": "Comprobar",
"components.Settings.RadarrModal.ssl": "SSL",
"components.Settings.RadarrModal.servernamePlaceholder": "Un Servidor Radarr",
"components.Settings.RadarrModal.servername": "Nombre del Servidor",
"components.Settings.RadarrModal.server4k": "Servidor 4K",
"components.Settings.RadarrModal.selectRootFolder": "Selecciona la Carpeta Raíz",
"components.Settings.RadarrModal.selectQualityProfile": "Selecciona un Perfil de Calidad",
"components.Settings.RadarrModal.selectMinimumAvailability": "Selecciona Disponibilidad Mínima",
"components.Settings.RadarrModal.saving": "Guardando…",
"components.Settings.RadarrModal.save": "Guardar Cambios",
"components.Settings.RadarrModal.rootfolder": "Carpeta Raíz",
"components.Settings.RadarrModal.qualityprofile": "Perfil de Calidad",
"components.Settings.RadarrModal.port": "Puerto",
"components.Settings.RadarrModal.minimumAvailability": "Disponibilidad Mínima",
"components.Settings.RadarrModal.hostname": "Nombre de Host",
"components.Settings.RadarrModal.editradarr": "Editar Servidor Radarr",
"components.Settings.RadarrModal.defaultserver": "Servidor por Defecto",
"components.Settings.RadarrModal.createradarr": "Crear Nuevo Servidor Radarr",
"components.Settings.RadarrModal.baseUrlPlaceholder": "Ejemplo: /radarr",
"components.Settings.RadarrModal.baseUrl": "URL Base",
"components.Settings.RadarrModal.apiKeyPlaceholder": "Tu clave API de Radarr",
"components.Settings.RadarrModal.apiKey": "Clave API",
"components.Settings.RadarrModal.add": "Agregar Servidor",
"components.Settings.Notifications.webhookUrlPlaceholder": "Ajustes del servidor -> Integraciones -> Webhooks",
"components.Settings.Notifications.webhookUrl": "URL de Webhook",
"components.Settings.Notifications.validationWebhookUrlRequired": "Debes proporcionar una URL del webhook",
"components.Settings.Notifications.validationSmtpPortRequired": "Debes proporcionar un puerto SMTP",
"components.Settings.Notifications.validationSmtpHostRequired": "Debes proporcionar un host SMTP",
"components.Settings.Notifications.validationFromRequired": "Debes proporcionar una dirección de envío de email",
"components.Settings.Notifications.smtpPort": "Puerto SMTP",
"components.Settings.Notifications.smtpHost": "Host SMTP",
"components.Settings.Notifications.saving": "Guardando…",
"components.Settings.Notifications.save": "Guardar Cambios",
"components.Settings.Notifications.enableSsl": "Activar SSL",
"components.Settings.Notifications.emailsettingssaved": "¡Ajustes de notificación de Email guardados!",
"components.Settings.Notifications.emailsettingsfailed": "Fallo al guardar ajustes de notificación de Email.",
"components.Settings.Notifications.emailsender": "Dirección del Remitente de Email",
"components.Settings.Notifications.discordsettingssaved": "¡Ajustes de notificación de Discord guardados!",
"components.Settings.Notifications.discordsettingsfailed": "Fallo al guardar ajustes de notificación de Discord.",
"components.Settings.Notifications.authUser": "Usuario",
"components.Settings.Notifications.authPass": "Contraseña",
"components.Settings.Notifications.agentenabled": "Agente Activado",
"components.Search.searchresults": "Resultado de la búsqueda",
"components.RequestModal.status": "Estado",
"components.RequestModal.selectseason": "Seleccionar temporada(s)",
"components.RequestModal.seasonnumber": "Temporada {number}",
"components.RequestModal.season": "Temporada",
"components.RequestModal.requesttitle": "Solicitar {title}",
"components.RequestModal.requestseasons": "Solicitar {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestModal.requesting": "Solicitando…",
"components.RequestModal.requestfrom": "Hay una petición pendiente de {username}",
"components.RequestModal.requestadmin": "Tu petición será aprovada inmediatamente.",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> solicitado.",
"components.RequestList.showingresults": "Mostrando <strong>{from}</strong> a <strong>{to}</strong> de <strong>{total}</strong> resultados",
"components.RequestModal.requestCancel": "Solicitud para <strong>{title}</strong> cancelada",
"components.RequestModal.request": "Solicitar",
"components.RequestModal.pendingrequest": "Solicitud pendiente para {title}",
"components.RequestModal.numberofepisodes": "# de Episodios",
"components.RequestModal.notrequested": "No Solicitado",
"components.RequestModal.extras": "Extras",
"components.RequestModal.close": "Cerrar",
"components.RequestModal.cancelrequest": "Esto borrará tu petición. ¿Quieres continuar?",
"components.RequestModal.cancelling": "Cancelando…",
"components.RequestModal.cancel": "Cancelar Petición",
"components.RequestList.status": "Estado",
"components.RequestList.requests": "Peticiones",
"components.RequestList.requestedAt": "Solicitado",
"components.RequestList.previous": "Anterior",
"components.RequestList.next": "Siguiente",
"components.RequestList.modifiedBy": "Última Edición Por",
"components.RequestList.mediaInfo": "Información",
"components.RequestList.RequestItem.seasons": "Temporadas",
"components.RequestList.RequestItem.requestedby": "Solicitado por {username}",
"components.RequestList.RequestItem.notavailable": "N/D",
"components.RequestCard.seasons": "Temporadas",
"components.RequestCard.requestedby": "Solicitado por {username}",
"components.RequestCard.all": "Todas",
"components.RequestBlock.seasons": "Temporadas",
"components.PlexLoginButton.loginwithplex": "Iniciar Sesión con Plex",
"components.PlexLoginButton.loggingin": "Iniciando Sesión…",
"components.PlexLoginButton.loading": "Cargando…",
"components.PersonDetails.nobiography": "No hay biografía disponible.",
"components.PersonDetails.ascharacter": "como {character}",
"components.PersonDetails.appearsin": "Aparece en",
"components.MovieDetails.viewrequest": "Ver Petición",
"components.MovieDetails.userrating": "Puntuación de los Usuarios",
"components.MovieDetails.unavailable": "No Disponible",
"components.MovieDetails.status": "Estado",
"components.MovieDetails.similarsubtext": "Otras películas parecidas a {title}",
"components.MovieDetails.similar": "Títulos Similares",
"components.MovieDetails.runtime": "{minutes} minutos",
"components.MovieDetails.revenue": "Recaudado",
"components.MovieDetails.request": "Solicitar",
"components.MovieDetails.releasedate": "Fecha de Lanzamiento",
"components.MovieDetails.recommendationssubtext": "Si te gustó {title}, también te puede gustar…",
"components.MovieDetails.cast": "Reparto",
"components.MovieDetails.MovieCast.fullcast": "Reparto Completo",
"components.MovieDetails.recommendations": "Recomendaciones",
"components.MovieDetails.pending": "Pendiente",
"components.MovieDetails.overviewunavailable": "Resumen no disponible",
"components.MovieDetails.overview": "Resumen",
"components.MovieDetails.originallanguage": "Idioma Original",
"components.MovieDetails.manageModalTitle": "Gestionar Película",
"components.MovieDetails.manageModalRequests": "Peticiones",
"components.MovieDetails.manageModalNoRequests": "Sin Peticiones",
"components.MovieDetails.manageModalClearMediaWarning": "Esto borrará todos los datos de los medios, incluyendo las peticiones. Si el elemento existe en tu librería de Plex, la información del elemento se recreará en la siguiente sincronización.",
"components.MovieDetails.manageModalClearMedia": "Borrar todos los datos de medios",
"components.MovieDetails.decline": "Rechazar",
"components.MovieDetails.cancelrequest": "Cancelar Petición",
"components.MovieDetails.budget": "Presupuesto",
"components.MovieDetails.available": "Disponible",
"components.MovieDetails.approve": "Aprobar",
"components.Login.signinplex": "Inicia Sesión para continuar",
"components.Layout.UserDropdown.signout": "Cerrar Sesión",
"components.Layout.Sidebar.users": "Usuarios",
"components.Layout.Sidebar.settings": "Ajustes",
"components.Layout.Sidebar.requests": "Peticiones",
"components.Layout.Sidebar.dashboard": "Descubrir",
"components.Layout.SearchInput.searchPlaceholder": "Buscar Películas y Series",
"components.Layout.LanguagePicker.changelanguage": "Cambiar Idioma",
"components.Discover.upcomingmovies": "Próximas Películas",
"components.Discover.upcoming": "Próximas Películas",
"components.Discover.trending": "Tendencias",
"components.Discover.recentrequests": "Peticiones Recientes",
"components.Discover.recentlyAdded": "Agregado Recientemente",
"components.Discover.populartv": "Series Populares",
"components.Discover.popularmovies": "Películas Populares",
"components.Discover.nopending": "Sin Peticiones Pendientes",
"components.Discover.discovertv": "Series Populares",
"components.Discover.discovermovies": "Películas Populares",
"components.Layout.alphawarning": "Este software está en fase ALFA. Muchas cosas pueden ser inestables o fallar. ¡Por favor reporta estos problemas en el GitHub de Overseerr!",
"components.Settings.addsonarr": "Agregar servidor Sonarr",
"components.Settings.address": "Dirección",
"components.Settings.addradarr": "Agregar servidor Radarr",
"components.Settings.activeProfile": "Perfil activo",
"components.Settings.SonarrModal.validationRootFolderRequired": "Debes seleccionar una carpeta raíz",
"components.Settings.SonarrModal.validationProfileRequired": "Debes seleccionar un perfil",
"components.Settings.SonarrModal.validationPortRequired": "Debes proporcionar un puerto",
"components.Settings.SonarrModal.validationNameRequired": "Debes proporcionar un nombre de servidor",
"components.Settings.SonarrModal.validationHostnameRequired": "Debes proporcionar un hostname/IP",
"components.Settings.SonarrModal.validationApiKeyRequired": "Debes proporcionar la clave API",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "¡Conexión con Sonarr establecida!",
"components.Settings.menuLogs": "Registro",
"pages.somethingWentWrong": "{statusCode} Algo salió mal",
"pages.serviceUnavailable": "{statusCode}: Servicio no Disponible",
"pages.returnHome": "Volver al Inicio",
"pages.pageNotFound": "404  Página no encontrada",
"pages.oops": "Ups",
"pages.internalServerError": "{statusCode}: Error interno del servidor",
"i18n.unavailable": "No Disponible",
"i18n.tvshows": "Series",
"i18n.processing": "Procesando…",
"i18n.pending": "Pendiente",
"i18n.partiallyavailable": "Parcialmente Disponible",
"i18n.movies": "Películas",
"i18n.delete": "Eliminar",
"i18n.declined": "Rechazado",
"i18n.decline": "Rechazar",
"i18n.cancel": "Cancelar",
"i18n.available": "Disponible",
"i18n.approved": "Aprobado",
"i18n.approve": "Aprobar",
"components.UserList.usertype": "Tipo de usuario",
"components.UserList.username": "Nombre de usuario",
"components.UserList.userlist": "Lista de usuarios",
"components.UserList.user": "Usuario",
"components.UserList.totalrequests": "Solicitudes totales",
"components.UserList.role": "Rol",
"components.UserList.plexuser": "Usuario de Plex",
"components.UserList.lastupdated": "Última actualización",
"components.UserList.edit": "Editar",
"components.UserList.delete": "Eliminar",
"components.UserList.created": "Creado",
"components.UserList.admin": "Administrador",
"components.UserEdit.voteDescription": "Otorga permiso para votar en las solicitudes (votación aún no implementada)",
"components.UserEdit.vote": "Votar",
"components.UserEdit.usersaved": "Usuario guardado",
"components.UserEdit.usersDescription": "Otorga permiso para administrar usuarios de Overseerr. Los usuarios con este permiso no pueden modificar a los usuarios con privilegios de administrador, o concederlos.",
"components.UserEdit.users": "Administrar usuarios",
"components.UserEdit.username": "Nombre de usuario",
"components.UserEdit.userfail": "Algo salió mal al guardar al usuario.",
"components.UserEdit.settingsDescription": "Otorga permiso para modificar todas las configuraciones de Overseerr. El usuario debe tener este permiso para otorgarlo a otros.",
"components.UserEdit.settings": "Administrar configuración",
"components.UserEdit.saving": "Guardando…",
"components.UserEdit.save": "Guardar",
"components.UserEdit.requestDescription": "Otorga permiso para solicitar películas y series.",
"components.UserEdit.request": "Solicitar",
"components.UserEdit.permissions": "Permisos",
"components.UserEdit.managerequestsDescription": "Otorga permiso para administrar las solicitudes de Overseerr. Esto incluye aprobar y rechazar solicitudes.",
"components.UserEdit.managerequests": "Gestionar solicitudes",
"components.UserEdit.email": "Correo electrónico",
"components.UserEdit.edituser": "Editar Usuario",
"components.UserEdit.avatar": "Avatar",
"components.UserEdit.autoapproveDescription": "Otorga aprobación automática para cualquier solicitud de este usuario.",
"components.UserEdit.autoapprove": "Aprobación automática",
"components.UserEdit.adminDescription": "Acceso total de administrador. Ignoran todas las comprobaciones de permisos.",
"components.UserEdit.admin": "Administrador",
"components.TvDetails.userrating": "Puntuación de los Usuarios",
"components.TvDetails.unavailable": "No Disponible",
"components.TvDetails.status": "Estado",
"components.TvDetails.similarsubtext": "Otras series similares a {title}",
"components.TvDetails.similar": "Series similares",
"components.TvDetails.requestmore": "Solicitar más",
"components.TvDetails.request": "Solicitar",
"components.TvDetails.recommendationssubtext": "Si te gustó {title}, también te puede gustar...",
"components.TvDetails.recommendations": "Recomendaciones",
"components.TvDetails.pending": "Pendiente",
"components.TvDetails.overviewunavailable": "Resumen no disponible",
"components.TvDetails.overview": "Resumen",
"components.TvDetails.originallanguage": "Idioma original",
"components.TvDetails.manageModalTitle": "Gestionar Series",
"components.TvDetails.manageModalRequests": "Peticiones",
"components.TvDetails.manageModalNoRequests": "Sin Peticiones",
"components.TvDetails.manageModalClearMediaWarning": "Esto borrará todos los datos de los medios, incluyendo las peticiones. Si el elemento existe en tu librería de Plex, la información del elemento se recreará en la siguiente sincronización.",
"components.TvDetails.manageModalClearMedia": "Borrar todos los datos de medios",
"components.TvDetails.declinerequests": "Rechazar {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.decline": "Rechazar",
"components.TvDetails.cast": "Reparto",
"components.TvDetails.cancelrequest": "Cancelar Petición",
"components.TvDetails.available": "Disponible",
"components.TvDetails.approverequests": "Aprobar {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.approve": "Aprobar",
"components.TvDetails.TvCast.fullseriescast": "Reparto completo de la serie",
"components.TitleCard.tvshow": "Series",
"components.Settings.jobname": "Nombre de Tarea",
"components.TitleCard.movie": "Película",
"components.Slider.noresults": "Sin resultados",
"components.Setup.welcome": "Bienvenido a Overseerr",
"components.Setup.signinMessage": "Comience iniciando sesión con su cuenta de Plex",
"components.Setup.loginwithplex": "Iniciar sesión con Plex",
"components.Setup.finishing": "Finalizando…",
"components.Setup.finish": "Finalizar configuración",
"components.Setup.continue": "Continuar",
"components.Setup.configureplex": "Configurar Plex",
"components.Setup.configureservices": "Configurar servicios",
"components.Settings.validationPortRequired": "Debes proporcionar un puerto",
"components.Settings.validationHostnameRequired": "Debe proporcionar un nombre de host / IP",
"components.Settings.syncing": "Sincronizando…",
"components.Settings.sync": "Sincronizar bibliotecas de Plex",
"components.Settings.startscan": "Iniciar escaneo",
"components.Settings.ssl": "SSL",
"components.Settings.sonarrsettings": "Ajustes de Sonarr",
"components.Settings.sonarrSettingsDescription": "Configure su conexión Sonarr a continuación. Puede tener varias instancias, pero sólo dos activas como predeterminadas en cualquier momento (una para HD estándar y otro para 4K). Los administradores pueden elegir qué servidor se utiliza para nuevas solicitudes.",
"components.Settings.servernamePlaceholder": "Nombre del servidor Plex",
"components.Settings.servername": "Nombre del servidor (Establecido automáticamente después de guardar)",
"components.Settings.saving": "Guardando…",
"components.Settings.save": "Guardar cambios",
"components.Settings.runnow": "Ejecutar ahora",
"components.Settings.radarrsettings": "Ajustes de Radarr",
"components.Settings.radarrSettingsDescription": "Configure su conexión a Radarr a continuación. Puede tener varias instancias, pero sólo dos activas como predeterminadas en cualquier momento (una para HD estándar y otro para 4K). Los administradores pueden elegir qué servidor se utiliza para nuevas solicitudes.",
"components.Settings.port": "Puerto",
"components.Settings.plexsettingsDescription": "Configure los ajustes de su servidor Plex. Overseerr usa su servidor Plex para escanear su biblioteca en un intervalo y ver qué contenido está disponible.",
"components.Settings.plexsettings": "Ajustes de Plex",
"components.Settings.plexlibrariesDescription": "Las bibliotecas en las que Overseerr busca títulos. Configure y guarde la configuración de conexión Plex y haga clic en el botón de abajo si no aparece ninguna.",
"components.Settings.plexlibraries": "Bibliotecas Plex",
"components.Settings.notrunning": "Sin ejecutarse",
"components.Settings.notificationsettingsDescription": "Aquí puedes elegir qué tipos de notificaciones enviar y a través de qué tipos de servicios.",
"components.Settings.notificationsettings": "Configuración de notificaciones",
"components.Settings.nextexecution": "Siguiente ejecución",
"components.Settings.menuServices": "Servicios",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuNotifications": "Notificaciones",
"components.Settings.menuJobs": "Tareas",
"components.Settings.menuGeneralSettings": "Ajustes Generales",
"components.Settings.menuAbout": "Acerca de",
"components.Settings.manualscanDescription": "Normalmente, esto sólo se ejecutará una vez cada 6 horas. Overseerr comprobará de forma más agresiva los añadidos recientemente de su servidor Plex. ¡Si es la primera vez que configura Plex se recomienda un escaneo manual completo de la biblioteca!",
"components.Settings.manualscan": "Escaneo manual de biblioteca",
"components.Settings.librariesRemaining": "Bibliotecas restantes: {count}",
"components.Settings.hostname": "Nombre de host / IP",
"components.Settings.generalsettingsDescription": "Estos son ajustes relacionados con la configuración general de Overseerr.",
"components.Settings.generalsettings": "Configuración general",
"components.Settings.edit": "Editar",
"components.Settings.deleteserverconfirm": "¿Está seguro de que desea eliminar este servidor?",
"components.Settings.delete": "Eliminar",
"components.Settings.default4k": "4K predeterminado",
"components.Settings.default": "Predeterminado",
"components.Settings.currentlibrary": "Biblioteca actual: {name}",
"components.Settings.copied": "Clave API copiada en el portapapeles",
"components.Settings.cancelscan": "Cancelar escaneo",
"components.Settings.applicationurl": "URL de la aplicación",
"components.Settings.apikey": "Clave API",
"components.Setup.tip": "Consejo",
"components.Setup.syncingbackground": "La sincronización se ejecutará en segundo plano. Mientras tanto, puede continuar con el proceso de configuración."
}

View File

@@ -10,7 +10,7 @@
"components.Discover.upcoming": "Prochaines sorties",
"components.Discover.upcomingmovies": "Prochaines sorties",
"components.Layout.LanguagePicker.changelanguage": "Changer la langue",
"components.Layout.SearchInput.searchPlaceholder": "Rechercher films et TV",
"components.Layout.SearchInput.searchPlaceholder": "Rechercher des films et des séries",
"components.Layout.Sidebar.dashboard": "Découverte",
"components.Layout.Sidebar.requests": "Demandes d'ajouts",
"components.Layout.Sidebar.settings": "Paramètres",
@@ -22,12 +22,12 @@
"components.MovieDetails.available": "Disponible",
"components.MovieDetails.budget": "Budget",
"components.MovieDetails.cancelrequest": "Annuler la demande",
"components.MovieDetails.cast": "Distribution",
"components.MovieDetails.cast": "Casting",
"components.MovieDetails.decline": "Refuser",
"components.MovieDetails.manageModalClearMedia": "Effacer toutes les données médias",
"components.MovieDetails.manageModalClearMediaWarning": "Toutes les donnés de médias vont être éffacées pour cet élément irréversiblement. Si cet élément existe dans votre bibliothèque Plex, les informations du média seront recrées à la prochaine synchronisation.",
"components.MovieDetails.manageModalNoRequests": "Aucune demande",
"components.MovieDetails.manageModalRequests": "Demandes d'ajouts",
"components.MovieDetails.manageModalRequests": "Demandes d'ajout",
"components.MovieDetails.manageModalTitle": "Gérer les films",
"components.MovieDetails.originallanguage": "Langue originale",
"components.MovieDetails.overview": "Résumé",
@@ -45,14 +45,14 @@
"components.MovieDetails.unavailable": "Indisponible",
"components.MovieDetails.userrating": "Note utilisateur",
"components.MovieDetails.viewrequest": "Voir la demande",
"components.PersonDetails.appearsin": "",
"components.PersonDetails.ascharacter": "",
"components.PersonDetails.nobiography": "",
"components.PersonDetails.appearsin": "Apparaît dans",
"components.PersonDetails.ascharacter": "comme {character}",
"components.PersonDetails.nobiography": "Aucune biographie disponible.",
"components.PlexLoginButton.loading": "Chargement…",
"components.PlexLoginButton.loggingin": "Connexion en cours…",
"components.PlexLoginButton.loginwithplex": "Se connecter avec Plex",
"components.RequestBlock.seasons": "Saisons",
"components.RequestCard.all": "",
"components.RequestCard.all": "Toutes",
"components.RequestCard.requestedby": "Demandé par {username}",
"components.RequestCard.seasons": "Saisons",
"components.RequestList.RequestItem.notavailable": "s.o.",
@@ -62,7 +62,7 @@
"components.RequestList.modifiedBy": "Dernière modification par",
"components.RequestList.next": "Suivant",
"components.RequestList.previous": "Précédent",
"components.RequestList.requestedAt": "Demandé à",
"components.RequestList.requestedAt": "Demandé le",
"components.RequestList.requests": "Demandes",
"components.RequestList.showingresults": "Affichage de <strong>{from}</strong> à <strong>{to}</strong> sur <strong>{total </strong> résultats",
"components.RequestList.status": "Statut",
@@ -90,13 +90,13 @@
"components.Settings.Notifications.agentenabled": "Agent activé",
"components.Settings.Notifications.authPass": "Auth Pass",
"components.Settings.Notifications.authUser": "Auth User",
"components.Settings.Notifications.emailsender": "Adresse courriel de l'expéditeur",
"components.Settings.Notifications.emailsender": "e-mail de l'expéditeur",
"components.Settings.Notifications.enableSsl": "Activer le SSL",
"components.Settings.Notifications.save": "Sauvegarder les changements",
"components.Settings.Notifications.saving": "Sauvegarde en cours…",
"components.Settings.Notifications.smtpHost": "Hôte SMTP",
"components.Settings.Notifications.smtpPort": "Port SMTP",
"components.Settings.Notifications.validationFromRequired": "Vous devez fournir une adresse courriel d'expéditeur",
"components.Settings.Notifications.validationFromRequired": "Vous devez fournir une adresse e-mail d'expéditeur",
"components.Settings.Notifications.validationSmtpHostRequired": "Vous devez fournir un hôte SSL",
"components.Settings.Notifications.validationSmtpPortRequired": "Vous devez fournir un port SMTP",
"components.Settings.Notifications.validationWebhookUrlRequired": "Vous devez fournir une URL de webhook",
@@ -205,14 +205,14 @@
"components.Settings.runnow": "Lancer maintenant",
"components.Settings.save": "Sauvegarder les changements",
"components.Settings.saving": "Enregistrement en cours…",
"components.Settings.servername": "Nom du serveur (défini automatiquement)",
"components.Settings.servername": "Nom du serveur (automatiquement défini après enregistrement)",
"components.Settings.servernamePlaceholder": "Nom de serveur Plex",
"components.Settings.sonarrSettingsDescription": "Configurez votre connexion Sonarr ci-dessous. Vous pouvez avoir plusieurs, mais seulement deux actives par défaut à tout moment (une pour la HD standard et un pour la 4K). Les administrateurs peuvent remplacer le serveur utilisé pour les nouvelles demandes.",
"components.Settings.sonarrsettings": "Paramètres Sonarr",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Commencer le scan",
"components.Settings.sync": "Synchroniser les bibliothèques Plex",
"components.Settings.syncing": "Synchronisation en cours",
"components.Settings.syncing": "Synchronisation",
"components.Setup.configureplex": "Configurer Plex",
"components.Setup.configureservices": "Configurer les Services",
"components.Setup.continue": "Continuer",
@@ -227,7 +227,7 @@
"components.TvDetails.approve": "Valider",
"components.TvDetails.approverequests": "Valider {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.available": "Disponible",
"components.TvDetails.cancelrequest": "Annuler la Demande",
"components.TvDetails.cancelrequest": "Annuler la demande",
"components.TvDetails.cast": "Casting",
"components.TvDetails.decline": "Refuser",
"components.TvDetails.declinerequests": "Refuser {requestCount} {requestCount, plural, one {Request} other {Requests}}",
@@ -250,17 +250,17 @@
"components.TvDetails.unavailable": "Indisponible",
"components.TvDetails.userrating": "Note utilisateurs",
"components.UserEdit.admin": "Administrateur",
"components.UserEdit.adminDescription": "Accès administrateur complet. Contourner toutes les demandes de permissions.",
"components.UserEdit.adminDescription": "Accès administrateur complet. Contourne toutes les demandes de permissions.",
"components.UserEdit.autoapprove": "Validation automatique",
"components.UserEdit.autoapproveDescription": "Active la validation automatique de toutes les demandes d'ajouts de cet utilisateur.",
"components.UserEdit.avatar": "Avatar",
"components.UserEdit.edituser": "Modifier l'utilisateur",
"components.UserEdit.email": "Courriel",
"components.UserEdit.email": "e-mail",
"components.UserEdit.managerequests": "Gérer les demandes d'ajouts",
"components.UserEdit.managerequestsDescription": "Donne la permission de gérer les demandes d'ajouts sur Overseerr. Ceci inclut la possibilité de valider ou refuser une demande.",
"components.UserEdit.permissions": "Permissions",
"components.UserEdit.permissions": "Autorisations",
"components.UserEdit.request": "Demandes d'ajouts",
"components.UserEdit.requestDescription": "Donne la permission de faire des demandes d'ajouts pour les films et séries.",
"components.UserEdit.requestDescription": "Accorde l'autorisation de demander des films et des séries.",
"components.UserEdit.save": "Sauvegarder",
"components.UserEdit.saving": "Enregistrement en cours…",
"components.UserEdit.settings": "Gérer les paramètres",
@@ -279,7 +279,7 @@
"components.UserList.lastupdated": "Denière mise à jour",
"components.UserList.plexuser": "Utilisateur Plex",
"components.UserList.role": "Rôle",
"components.UserList.totalrequests": "Demandes d'ajouts total",
"components.UserList.totalrequests": "Demandes d'ajout totales",
"components.UserList.user": "Utilisateur",
"components.UserList.userlist": "Liste des utilisateurs",
"components.UserList.username": "Nom d'utilisateur",
@@ -294,7 +294,7 @@
"i18n.movies": "Films",
"i18n.partiallyavailable": "Partiellement disponible",
"i18n.pending": "En attente",
"i18n.processing": "En cours",
"i18n.processing": "Traitement en cours",
"i18n.tvshows": "Séries",
"i18n.unavailable": "Indisponible",
"pages.internalServerError": "{statusCode} Erreur du serveur interne",
@@ -302,5 +302,24 @@
"pages.pageNotFound": "404 Page non trouvée",
"pages.returnHome": "Retourner à l'acceuil",
"pages.serviceUnavailable": "{statusCode} Service indisponible",
"pages.somethingWentWrong": "{statusCode} Il y a eu un problème"
"pages.somethingWentWrong": "{statusCode} Une erreur est survenue",
"components.TvDetails.TvCast.fullseriescast": "Casting complet de la série",
"components.MovieDetails.MovieCast.fullcast": "Casting complet",
"components.Settings.Notifications.emailsettingssaved": "Paramètres de notification par courriel enregistrés !",
"components.Settings.Notifications.emailsettingsfailed": "Les paramètres de notification par courriel n'ont pas pu être enregistrés.",
"components.Settings.Notifications.discordsettingssaved": "Paramètres de notification Discord enregistrés !",
"components.Settings.Notifications.discordsettingsfailed": "Les paramètres de notification Discord n'ont pas pu être enregistrés.",
"components.Settings.validationPortRequired": "Vous devez fournir un port",
"components.Settings.validationHostnameRequired": "Vous devez fournir un nom d'hôte / une IP",
"components.Settings.SonarrModal.validationNameRequired": "Vous devez fournir un nom de serveur",
"components.Settings.SettingsAbout.version": "Version",
"components.Settings.SettingsAbout.totalrequests": "Total des demandes",
"components.Settings.SettingsAbout.totalmedia": "Total des médias",
"components.Settings.SettingsAbout.overseerrinformation": "Informations sur Overseerr",
"components.Settings.SettingsAbout.githubdiscussions": "Discussions GitHub",
"components.Settings.SettingsAbout.gettingsupport": "Obtenir de l'aide",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Cliquez ici pour rejoindre notre serveur Discord.",
"components.Settings.RadarrModal.validationNameRequired": "Vous devez fournir un nom de serveur",
"components.Setup.tip": "Astuce",
"components.Setup.syncingbackground": "La synchronisation s'exécutera en arrière-plan. Vous pouvez continuer le processus de configuration en attendant."
}

View File

@@ -6,7 +6,7 @@
"components.Discover.populartv": "人気のテレビ番組",
"components.Discover.recentlyAdded": "最近追加された動画",
"components.Discover.recentrequests": "最近のリクエスト",
"components.Discover.trending": "最近のトレンド",
"components.Discover.trending": "トレンド",
"components.Discover.upcoming": "今後リリースされる映画",
"components.Discover.upcomingmovies": "今後リリースされる映画",
"components.Layout.LanguagePicker.changelanguage": "言語",
@@ -16,16 +16,16 @@
"components.Layout.Sidebar.settings": "設定",
"components.Layout.Sidebar.users": "ユーザー",
"components.Layout.UserDropdown.signout": "ログアウト",
"components.Layout.alphawarning": "このソフトはアルファ版です。ほとんどの機能は不安定又は故障している可能性があります。バグ発見したとき、OverseerrのGithubまでご連絡ください",
"components.Login.signinplex": "続にはログインしてください",
"components.Layout.alphawarning": "このソフトはアルファ版です。ほとんどの機能は不安定又は故障している可能性があります。バグ発見した場合、OverseerrのGithubまでご連絡ください",
"components.Login.signinplex": "続けるにはログインしてください",
"components.MovieDetails.approve": "承認",
"components.MovieDetails.available": "鑑賞可能",
"components.MovieDetails.budget": "予算",
"components.MovieDetails.cancelrequest": "リクエストを取り消す",
"components.MovieDetails.cast": "キャスト",
"components.MovieDetails.decline": "拒否する",
"components.MovieDetails.manageModalClearMedia": "メディアのデータを消去",
"components.MovieDetails.manageModalClearMediaWarning": "リクエストを含め、メーディアのデータをすべて消去されます。この操作は元に戻すことができません。このアイテムが Plex ライブラリに存在する場合、メディア情報は次の同期で再作成されます。",
"components.MovieDetails.decline": "拒否",
"components.MovieDetails.manageModalClearMedia": "メディア情報を消去",
"components.MovieDetails.manageModalClearMediaWarning": "リクエストを含め、すべての詳細情報が消去されます。この操作は元に戻すことができません。この作品が Plex ライブラリに存在する場合、詳細情報は次の同期で再作成されます。",
"components.MovieDetails.manageModalNoRequests": "リクエストがありません",
"components.MovieDetails.manageModalRequests": "リクエスト",
"components.MovieDetails.manageModalTitle": "映画を管理",
@@ -36,7 +36,7 @@
"components.MovieDetails.recommendations": "オススメの作品",
"components.MovieDetails.recommendationssubtext": "{title}が好きだった人は、こんなのも好きかもしれません...",
"components.MovieDetails.releasedate": "公開日",
"components.MovieDetails.request": "リクエストする",
"components.MovieDetails.request": "リクエストする",
"components.MovieDetails.revenue": "収益",
"components.MovieDetails.runtime": "{minutes}分",
"components.MovieDetails.similar": "関連作品",
@@ -45,28 +45,28 @@
"components.MovieDetails.unavailable": "鑑賞不可",
"components.MovieDetails.userrating": "ユーザー評価",
"components.MovieDetails.viewrequest": "リクエストを閲覧",
"components.PersonDetails.appearsin": "",
"components.PersonDetails.ascharacter": "",
"components.PersonDetails.nobiography": "",
"components.PersonDetails.appearsin": "出演作品",
"components.PersonDetails.ascharacter": "{character}役",
"components.PersonDetails.nobiography": "出演作品の情報はありません。",
"components.PlexLoginButton.loading": "ロード中…",
"components.PlexLoginButton.loggingin": "ログイン中…",
"components.PlexLoginButton.loginwithplex": "Plexでログイン",
"components.RequestBlock.seasons": "シーズン",
"components.RequestCard.all": "",
"components.RequestCard.all": "すべて",
"components.RequestCard.requestedby": "{username}のリクエスト",
"components.RequestCard.seasons": "シーズン",
"components.RequestList.RequestItem.notavailable": "N/A",
"components.RequestList.RequestItem.requestedby": "{username}のリクエスト",
"components.RequestList.RequestItem.seasons": "シーズン",
"components.RequestList.mediaInfo": "メディア情報",
"components.RequestList.mediaInfo": "詳細情報",
"components.RequestList.modifiedBy": "最終更新者",
"components.RequestList.next": "次",
"components.RequestList.previous": "戻る",
"components.RequestList.requestedAt": "リクエスト日時",
"components.RequestList.requests": "リクエスト",
"components.RequestList.showingresults": "<strong>{total}</strong>中<strong>{from}</strong><strong>{to}</strong>件",
"components.RequestList.status": "ステータス",
"components.RequestModal.cancel": "ャンセルリクエスト",
"components.RequestList.status": "状態",
"components.RequestModal.cancel": "ャンセルリクエスト",
"components.RequestModal.cancelling": "取り消し中...",
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
"components.RequestModal.close": "閉じる",
@@ -75,9 +75,9 @@
"components.RequestModal.numberofepisodes": "エピソード数",
"components.RequestModal.pendingrequest": "{title}がリクエスト中",
"components.RequestModal.request": "リクエストする",
"components.RequestModal.requestCancel": "<strong>{title}</strong>のリクエストは取り消された",
"components.RequestModal.requestSuccess": "<strong>{title}</strong>のリクエストは完了した。",
"components.RequestModal.requestadmin": "このリクエスト今すぐ承認ます。",
"components.RequestModal.requestCancel": "<strong>{title}</strong>のリクエストは取り消されました",
"components.RequestModal.requestSuccess": "<strong>{title}</strong>のリクエストは完了しました。",
"components.RequestModal.requestadmin": "このリクエスト今すぐ承認されます。",
"components.RequestModal.requestfrom": "{username}はすでにリクエストを上げています",
"components.RequestModal.requesting": "リクエスト中…",
"components.RequestModal.requestseasons": "{seasonCount} {seasonCount, plural, one {Season} other {Seasons}}をリクエスト",
@@ -128,11 +128,11 @@
"components.Settings.RadarrModal.testing": "テスト中...",
"components.Settings.RadarrModal.toastRadarrTestFailure": "Radarrサーバーの接続は失敗しました",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarrサーバーの接続は成功しました!",
"components.Settings.RadarrModal.validationApiKeyRequired": "APIキーの入力必要です",
"components.Settings.RadarrModal.validationHostnameRequired": "ホスト名/IPの入力必要です",
"components.Settings.RadarrModal.validationPortRequired": "ポートの入力必要です",
"components.Settings.RadarrModal.validationProfileRequired": "プロファイルの選択必要です",
"components.Settings.RadarrModal.validationRootFolderRequired": "ルートフォルダーの選択必要です",
"components.Settings.RadarrModal.validationApiKeyRequired": "APIキーの入力必要です",
"components.Settings.RadarrModal.validationHostnameRequired": "ホスト名/IPの入力必要です",
"components.Settings.RadarrModal.validationPortRequired": "ポートの入力必要です",
"components.Settings.RadarrModal.validationProfileRequired": "プロファイルの選択必要です",
"components.Settings.RadarrModal.validationRootFolderRequired": "ルートフォルダーの選択必要です",
"components.Settings.SonarrModal.add": "サーバーを追加",
"components.Settings.SonarrModal.apiKey": "APIキー",
"components.Settings.SonarrModal.apiKeyPlaceholder": "SonarrのAPIキー",
@@ -157,12 +157,12 @@
"components.Settings.SonarrModal.test": "テストする",
"components.Settings.SonarrModal.testing": "テスト中...",
"components.Settings.SonarrModal.toastRadarrTestFailure": "Sonarrサーバーに接続できませんでした",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarrサーバーの接続成功しました!",
"components.Settings.SonarrModal.validationApiKeyRequired": "APIキーの入力必要です",
"components.Settings.SonarrModal.validationHostnameRequired": "ホスト名/IPの入力必要です",
"components.Settings.SonarrModal.validationPortRequired": "ポートの入力必要です",
"components.Settings.SonarrModal.validationProfileRequired": "プロファイルの選択必要です",
"components.Settings.SonarrModal.validationRootFolderRequired": "ルートフォルダーの選択必要です",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarrサーバーの接続成功しました!",
"components.Settings.SonarrModal.validationApiKeyRequired": "APIキーの入力必要です",
"components.Settings.SonarrModal.validationHostnameRequired": "ホスト名/IPの入力必要です",
"components.Settings.SonarrModal.validationPortRequired": "ポートの入力必要です",
"components.Settings.SonarrModal.validationProfileRequired": "プロファイルの選択必要です",
"components.Settings.SonarrModal.validationRootFolderRequired": "ルートフォルダーの選択必要です",
"components.Settings.activeProfile": "アクティブプロファイル",
"components.Settings.addradarr": "Radarrサーバーを追加",
"components.Settings.address": "アドレス",
@@ -183,7 +183,7 @@
"components.Settings.jobname": "ジョブ名",
"components.Settings.librariesRemaining": "残りのライブラリー: {count}",
"components.Settings.manualscan": "手動ライブラリースキャン",
"components.Settings.manualscanDescription": "通常は6時間に一度しか実行されません。Overseerr は、Plex サーバーの最近追加されたフォルダをより頻繁にチェックします。初めて Plex を設定する場合は、一度だけ手動でライブラリーをスキャンすることをお勧めします。",
"components.Settings.manualscanDescription": "通常は6時間に一度しか実行されません。Overseerr は、Plex サーバーの最近追加されたフォルダをより頻繁にチェックします。初めて Plex を設定する場合は、一度手動でライブラリーをスキャンすることをお勧めします。",
"components.Settings.menuAbout": "Overseerrについて",
"components.Settings.menuGeneralSettings": "一般設定",
"components.Settings.menuJobs": "ジョブ",
@@ -205,14 +205,14 @@
"components.Settings.runnow": "今すぐ実行",
"components.Settings.save": "変更を保存",
"components.Settings.saving": "保存中...",
"components.Settings.servername": "サーバー名 (自動設定)",
"components.Settings.servername": "サーバー名 (保存時に自動設定されます)",
"components.Settings.servernamePlaceholder": "Plexサーバー名",
"components.Settings.sonarrSettingsDescription": "Sonarr接続設定。複数のサーバーを繋ぐことができますが、デフォルトで常にアクティブなのは2つのみ1つは標準HD用、もう1つは4K用。管理者は、新しいリクエストに使用するサーバーを選択することができます。",
"components.Settings.sonarrsettings": "Sonarr設定",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "スキャンを開始",
"components.Settings.sync": "Plexライブラリーの同期",
"components.Settings.syncing": "同期中",
"components.Settings.syncing": "同期中",
"components.Setup.configureplex": "Plexの設定",
"components.Setup.configureservices": "連携サービスの設定",
"components.Setup.continue": "続行",
@@ -294,7 +294,7 @@
"i18n.movies": "映画",
"i18n.partiallyavailable": "一部鑑賞可能",
"i18n.pending": "リクエスト中",
"i18n.processing": "処理中",
"i18n.processing": "処理中",
"i18n.tvshows": "シリーズ",
"i18n.unavailable": "鑑賞不可",
"pages.internalServerError": "{statusCode} - 内部サーバーエラー",
@@ -302,5 +302,22 @@
"pages.pageNotFound": "404 - ページが見つかりません",
"pages.returnHome": "ホームへ戻る",
"pages.serviceUnavailable": "{statusCode} - サービスが利用できません",
"pages.somethingWentWrong": "{statusCode} - 問題が発生しました"
"pages.somethingWentWrong": "{statusCode} - 問題が発生しました",
"components.TvDetails.TvCast.fullseriescast": "フルシリーズキャスト",
"components.Settings.validationPortRequired": "ポートの入力が必要です",
"components.Settings.validationHostnameRequired": "ホスト名/IPの入力が必要です",
"components.Settings.SonarrModal.validationNameRequired": "サーバー名を指定してください",
"components.Settings.SettingsAbout.version": "バージョン",
"components.Settings.SettingsAbout.totalrequests": "総リクエスト数",
"components.Settings.SettingsAbout.totalmedia": "総メディア数",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr情報",
"components.Settings.SettingsAbout.githubdiscussions": "GitHubディスカッション",
"components.Settings.SettingsAbout.gettingsupport": "サポート",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Discordサーバーの参加はこちら。",
"components.Settings.RadarrModal.validationNameRequired": "サーバー名を指定してください",
"components.Settings.Notifications.emailsettingssaved": "メール通知設定が保存されました!",
"components.Settings.Notifications.emailsettingsfailed": "メール通知設定の保存に失敗しました。",
"components.Settings.Notifications.discordsettingsfailed": "ディスコード通知設定の保存に失敗しました。",
"components.Settings.Notifications.discordsettingssaved": "ディスコードの通知設定が保存されました!",
"components.MovieDetails.MovieCast.fullcast": "フルキャスト"
}

View File

@@ -45,14 +45,14 @@
"components.MovieDetails.unavailable": "Utilgjengelig",
"components.MovieDetails.userrating": "Brukervurdering",
"components.MovieDetails.viewrequest": "Vis forespørsel",
"components.PersonDetails.appearsin": "",
"components.PersonDetails.ascharacter": "",
"components.PersonDetails.nobiography": "",
"components.PersonDetails.appearsin": "Finnes i",
"components.PersonDetails.ascharacter": "som {character}",
"components.PersonDetails.nobiography": "Ingen biografi tilgjengelig.",
"components.PlexLoginButton.loading": "Laster inn …",
"components.PlexLoginButton.loggingin": "Logger inn …",
"components.PlexLoginButton.loginwithplex": "Logg inn med Plex",
"components.RequestBlock.seasons": "Sesonger",
"components.RequestCard.all": "",
"components.RequestCard.all": "Alle",
"components.RequestCard.requestedby": "Forespurt av {username}",
"components.RequestCard.seasons": "Sesonger",
"components.RequestList.RequestItem.notavailable": "I/T",

View File

@@ -1,306 +1,337 @@
{
"components.Discover.discovermovies": "",
"components.Discover.discovertv": "",
"components.Discover.nopending": "",
"components.Discover.popularmovies": "",
"components.Discover.populartv": "",
"components.Discover.recentlyAdded": "",
"components.Discover.recentrequests": "",
"components.Discover.trending": "",
"components.Discover.upcoming": "",
"components.Discover.upcomingmovies": "",
"components.Layout.LanguagePicker.changelanguage": "",
"components.Layout.SearchInput.searchPlaceholder": "",
"components.Layout.Sidebar.dashboard": "",
"components.Layout.Sidebar.requests": "",
"components.Layout.Sidebar.settings": "",
"components.Layout.Sidebar.users": "",
"components.Layout.UserDropdown.signout": "",
"components.Layout.alphawarning": "",
"components.Login.signinplex": "",
"components.MovieDetails.approve": "",
"components.MovieDetails.available": "",
"components.MovieDetails.budget": "",
"components.MovieDetails.cancelrequest": "",
"components.MovieDetails.cast": "",
"components.MovieDetails.decline": "",
"components.MovieDetails.manageModalClearMedia": "",
"components.MovieDetails.manageModalClearMediaWarning": "",
"components.MovieDetails.manageModalNoRequests": "",
"components.MovieDetails.manageModalRequests": "",
"components.MovieDetails.manageModalTitle": "",
"components.MovieDetails.originallanguage": "",
"components.MovieDetails.overview": "",
"components.MovieDetails.overviewunavailable": "",
"components.MovieDetails.pending": "",
"components.MovieDetails.recommendations": "",
"components.MovieDetails.recommendationssubtext": "",
"components.MovieDetails.releasedate": "",
"components.MovieDetails.request": "",
"components.MovieDetails.revenue": "",
"components.MovieDetails.runtime": "",
"components.MovieDetails.similar": "",
"components.MovieDetails.similarsubtext": "",
"components.MovieDetails.status": "",
"components.MovieDetails.unavailable": "",
"components.MovieDetails.userrating": "",
"components.MovieDetails.viewrequest": "",
"components.PersonDetails.appearsin": "",
"components.PersonDetails.ascharacter": "",
"components.PersonDetails.nobiography": "",
"components.PlexLoginButton.loading": "",
"components.PlexLoginButton.loggingin": "",
"components.PlexLoginButton.loginwithplex": "",
"components.RequestBlock.seasons": "",
"components.RequestCard.all": "",
"components.RequestCard.requestedby": "",
"components.RequestCard.seasons": "",
"components.RequestList.RequestItem.notavailable": "",
"components.RequestList.RequestItem.requestedby": "",
"components.RequestList.RequestItem.seasons": "",
"components.RequestList.mediaInfo": "",
"components.RequestList.modifiedBy": "",
"components.RequestList.next": "",
"components.RequestList.previous": "",
"components.RequestList.requestedAt": "",
"components.RequestList.requests": "",
"components.RequestList.showingresults": "",
"components.RequestList.status": "",
"components.RequestModal.cancel": "",
"components.RequestModal.cancelling": "",
"components.RequestModal.cancelrequest": "",
"components.RequestModal.close": "",
"components.RequestModal.extras": "",
"components.RequestModal.notrequested": "",
"components.RequestModal.numberofepisodes": "",
"components.RequestModal.pendingrequest": "",
"components.RequestModal.request": "",
"components.RequestModal.requestCancel": "",
"components.RequestModal.requestSuccess": "",
"components.RequestModal.requestadmin": "",
"components.RequestModal.requestfrom": "",
"components.RequestModal.requesting": "",
"components.RequestModal.requestseasons": "",
"components.RequestModal.requesttitle": "",
"components.RequestModal.season": "",
"components.RequestModal.seasonnumber": "",
"components.RequestModal.selectseason": "",
"components.RequestModal.status": "",
"components.Search.searchresults": "",
"components.Settings.Notifications.agentenabled": "",
"components.Settings.Notifications.authPass": "",
"components.Settings.Notifications.authUser": "",
"components.Settings.Notifications.emailsender": "",
"components.Settings.Notifications.enableSsl": "",
"components.Settings.Notifications.save": "",
"components.Settings.Notifications.saving": "",
"components.Settings.Notifications.smtpHost": "",
"components.Settings.Notifications.smtpPort": "",
"components.Settings.Notifications.validationFromRequired": "",
"components.Settings.Notifications.validationSmtpHostRequired": "",
"components.Settings.Notifications.validationSmtpPortRequired": "",
"components.Settings.Notifications.validationWebhookUrlRequired": "",
"components.Settings.Notifications.webhookUrl": "",
"components.Settings.Notifications.webhookUrlPlaceholder": "",
"components.Settings.RadarrModal.add": "",
"components.Settings.RadarrModal.apiKey": "",
"components.Settings.RadarrModal.apiKeyPlaceholder": "",
"components.Settings.RadarrModal.baseUrl": "",
"components.Settings.RadarrModal.baseUrlPlaceholder": "",
"components.Settings.RadarrModal.createradarr": "",
"components.Settings.RadarrModal.defaultserver": "",
"components.Settings.RadarrModal.editradarr": "",
"components.Settings.RadarrModal.hostname": "",
"components.Settings.RadarrModal.minimumAvailability": "",
"components.Settings.RadarrModal.port": "",
"components.Settings.RadarrModal.qualityprofile": "",
"components.Settings.RadarrModal.rootfolder": "",
"components.Settings.RadarrModal.save": "",
"components.Settings.RadarrModal.saving": "",
"components.Settings.RadarrModal.selectMinimumAvailability": "",
"components.Settings.RadarrModal.selectQualityProfile": "",
"components.Settings.RadarrModal.selectRootFolder": "",
"components.Settings.RadarrModal.server4k": "",
"components.Settings.RadarrModal.servername": "",
"components.Settings.RadarrModal.servernamePlaceholder": "",
"components.Settings.RadarrModal.ssl": "",
"components.Settings.RadarrModal.test": "",
"components.Settings.RadarrModal.testing": "",
"components.Settings.RadarrModal.toastRadarrTestFailure": "",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "",
"components.Settings.RadarrModal.validationApiKeyRequired": "",
"components.Settings.RadarrModal.validationHostnameRequired": "",
"components.Settings.RadarrModal.validationPortRequired": "",
"components.Settings.RadarrModal.validationProfileRequired": "",
"components.Settings.RadarrModal.validationRootFolderRequired": "",
"components.Settings.SonarrModal.add": "",
"components.Settings.SonarrModal.apiKey": "",
"components.Settings.SonarrModal.apiKeyPlaceholder": "",
"components.Settings.SonarrModal.baseUrl": "",
"components.Settings.SonarrModal.baseUrlPlaceholder": "",
"components.Settings.SonarrModal.createsonarr": "",
"components.Settings.SonarrModal.defaultserver": "",
"components.Settings.SonarrModal.editsonarr": "",
"components.Settings.SonarrModal.hostname": "",
"components.Settings.SonarrModal.port": "",
"components.Settings.SonarrModal.qualityprofile": "",
"components.Settings.SonarrModal.rootfolder": "",
"components.Settings.SonarrModal.save": "",
"components.Settings.SonarrModal.saving": "",
"components.Settings.SonarrModal.seasonfolders": "",
"components.Settings.SonarrModal.selectQualityProfile": "",
"components.Settings.SonarrModal.selectRootFolder": "",
"components.Settings.SonarrModal.server4k": "",
"components.Settings.SonarrModal.servername": "",
"components.Settings.SonarrModal.servernamePlaceholder": "",
"components.Settings.SonarrModal.ssl": "",
"components.Settings.SonarrModal.test": "",
"components.Settings.SonarrModal.testing": "",
"components.Settings.SonarrModal.toastRadarrTestFailure": "",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "",
"components.Settings.SonarrModal.validationApiKeyRequired": "",
"components.Settings.SonarrModal.validationHostnameRequired": "",
"components.Settings.SonarrModal.validationPortRequired": "",
"components.Settings.SonarrModal.validationProfileRequired": "",
"components.Settings.SonarrModal.validationRootFolderRequired": "",
"components.Settings.activeProfile": "",
"components.Settings.addradarr": "",
"components.Settings.address": "",
"components.Settings.addsonarr": "",
"components.Settings.apikey": "",
"components.Settings.applicationurl": "",
"components.Settings.cancelscan": "",
"components.Settings.copied": "",
"components.Settings.currentlibrary": "",
"components.Settings.default": "",
"components.Settings.default4k": "",
"components.Settings.delete": "",
"components.Settings.deleteserverconfirm": "",
"components.Settings.edit": "",
"components.Settings.generalsettings": "",
"components.Settings.generalsettingsDescription": "",
"components.Settings.hostname": "",
"components.Settings.jobname": "",
"components.Settings.librariesRemaining": "",
"components.Settings.manualscan": "",
"components.Settings.manualscanDescription": "",
"components.Settings.menuAbout": "",
"components.Settings.menuGeneralSettings": "",
"components.Settings.menuJobs": "",
"components.Settings.menuLogs": "",
"components.Settings.menuNotifications": "",
"components.Settings.menuPlexSettings": "",
"components.Settings.menuServices": "",
"components.Settings.nextexecution": "",
"components.Settings.notificationsettings": "",
"components.Settings.notificationsettingsDescription": "",
"components.Settings.notrunning": "",
"components.Settings.plexlibraries": "",
"components.Settings.plexlibrariesDescription": "",
"components.Settings.plexsettings": "",
"components.Settings.plexsettingsDescription": "",
"components.Settings.port": "",
"components.Settings.radarrSettingsDescription": "",
"components.Settings.radarrsettings": "",
"components.Settings.runnow": "",
"components.Settings.save": "",
"components.Settings.saving": "",
"components.Settings.servername": "",
"components.Settings.servernamePlaceholder": "",
"components.Settings.sonarrSettingsDescription": "",
"components.Settings.sonarrsettings": "",
"components.Settings.ssl": "",
"components.Settings.startscan": "",
"components.Settings.sync": "",
"components.Settings.syncing": "",
"components.Setup.configureplex": "",
"components.Setup.configureservices": "",
"components.Setup.continue": "",
"components.Setup.finish": "",
"components.Setup.finishing": "",
"components.Setup.loginwithplex": "",
"components.Setup.signinMessage": "",
"components.Setup.welcome": "",
"components.Slider.noresults": "",
"components.TitleCard.movie": "",
"components.TitleCard.tvshow": "",
"components.TvDetails.approve": "",
"components.TvDetails.approverequests": "",
"components.TvDetails.available": "",
"components.TvDetails.cancelrequest": "",
"components.TvDetails.cast": "",
"components.TvDetails.decline": "",
"components.TvDetails.declinerequests": "",
"components.TvDetails.manageModalClearMedia": "",
"components.TvDetails.manageModalClearMediaWarning": "",
"components.TvDetails.manageModalNoRequests": "",
"components.TvDetails.manageModalRequests": "",
"components.TvDetails.manageModalTitle": "",
"components.TvDetails.originallanguage": "",
"components.TvDetails.overview": "",
"components.TvDetails.overviewunavailable": "",
"components.TvDetails.pending": "",
"components.TvDetails.recommendations": "",
"components.TvDetails.recommendationssubtext": "",
"components.TvDetails.request": "",
"components.TvDetails.requestmore": "",
"components.TvDetails.similar": "",
"components.TvDetails.similarsubtext": "",
"components.TvDetails.status": "",
"components.TvDetails.unavailable": "",
"components.TvDetails.userrating": "",
"components.UserEdit.admin": "",
"components.UserEdit.adminDescription": "",
"components.UserEdit.autoapprove": "",
"components.UserEdit.autoapproveDescription": "",
"components.UserEdit.avatar": "",
"components.UserEdit.edituser": "",
"components.UserEdit.email": "",
"components.UserEdit.managerequests": "",
"components.UserEdit.managerequestsDescription": "",
"components.UserEdit.permissions": "",
"components.UserEdit.request": "",
"components.UserEdit.requestDescription": "",
"components.UserEdit.save": "",
"components.UserEdit.saving": "",
"components.UserEdit.settings": "",
"components.UserEdit.settingsDescription": "",
"components.UserEdit.userfail": "",
"components.UserEdit.username": "",
"components.UserEdit.users": "",
"components.UserEdit.usersDescription": "",
"components.UserEdit.usersaved": "",
"components.UserEdit.vote": "",
"components.UserEdit.voteDescription": "",
"components.UserList.admin": "",
"components.UserList.created": "",
"components.UserList.delete": "",
"components.UserList.edit": "",
"components.UserList.lastupdated": "",
"components.UserList.plexuser": "",
"components.UserList.role": "",
"components.UserList.totalrequests": "",
"components.UserList.user": "",
"components.UserList.userlist": "",
"components.UserList.username": "",
"components.UserList.usertype": "",
"i18n.approve": "",
"i18n.approved": "",
"i18n.available": "",
"i18n.cancel": "",
"i18n.decline": "",
"i18n.declined": "",
"i18n.delete": "",
"i18n.movies": "",
"i18n.partiallyavailable": "",
"i18n.pending": "",
"i18n.processing": "",
"i18n.tvshows": "",
"i18n.unavailable": "",
"pages.internalServerError": "",
"pages.oops": "",
"pages.pageNotFound": "",
"pages.returnHome": "",
"pages.serviceUnavailable": "",
"pages.somethingWentWrong": ""
"components.Discover.discovermovies": "Populaire Films",
"components.Discover.discovertv": "Populaire Series",
"components.Discover.nopending": "Geen Wachtende Verzoeken",
"components.Discover.popularmovies": "Populaire Films",
"components.Discover.populartv": "Populaire Series",
"components.Discover.recentlyAdded": "Recentelijk Toegevoegd",
"components.Discover.recentrequests": "Recente Verzoeken",
"components.Discover.trending": "Trending",
"components.Discover.upcoming": "Verwachte Films",
"components.Discover.upcomingmovies": "Verwachte Films",
"components.Layout.LanguagePicker.changelanguage": "Wijzig Taal",
"components.Layout.SearchInput.searchPlaceholder": "Zoek Films & Series",
"components.Layout.Sidebar.dashboard": "Ontdek",
"components.Layout.Sidebar.requests": "Verzoeken",
"components.Layout.Sidebar.settings": "Instellingen",
"components.Layout.Sidebar.users": "Gebruikers",
"components.Layout.UserDropdown.signout": "Uitloggen",
"components.Layout.alphawarning": "Dit is ALPHA software. Bijna alles is waarschijnlijk kapot of onstabiel. Rapporteer issues bij de Overseerr GitHub!",
"components.Login.signinplex": "Log in om door te gaan",
"components.MovieDetails.approve": "Goedkeuren",
"components.MovieDetails.available": "Beschikbaar",
"components.MovieDetails.budget": "Budget",
"components.MovieDetails.cancelrequest": "Annuleer Verzoek",
"components.MovieDetails.cast": "Acteurs",
"components.MovieDetails.decline": "Weigeren",
"components.MovieDetails.manageModalClearMedia": "Wis Alle Media Data",
"components.MovieDetails.manageModalClearMediaWarning": "Dit wist alle media data inclusief alle verzoeken voor dit item zonder herstelmogelijkheden. Als dit item in je Plex bibliotheek bestaat zal alle media info bij de volgende sync hersteld worden.",
"components.MovieDetails.manageModalNoRequests": "Geen Verzoeken",
"components.MovieDetails.manageModalRequests": "Verzoeken",
"components.MovieDetails.manageModalTitle": "Beheer Film",
"components.MovieDetails.originallanguage": "Originele Taal",
"components.MovieDetails.overview": "Overzicht",
"components.MovieDetails.overviewunavailable": "Overzicht onbeschikbaar",
"components.MovieDetails.pending": "Wachtende",
"components.MovieDetails.recommendations": "Aanbevelingen",
"components.MovieDetails.recommendationssubtext": "Als je {title} leuk vond, vind je dit misschien ook leuk...",
"components.MovieDetails.releasedate": "Releasedatum",
"components.MovieDetails.request": "Verzoek",
"components.MovieDetails.revenue": "Omzet",
"components.MovieDetails.runtime": "{minutes} minuten",
"components.MovieDetails.similar": "Vergelijkbare Titels",
"components.MovieDetails.similarsubtext": "Andere films vergelijkbaar met {title}",
"components.MovieDetails.status": "Status",
"components.MovieDetails.unavailable": "Onbeschikbaar",
"components.MovieDetails.userrating": "Gebruikersbeoordeling",
"components.MovieDetails.viewrequest": "Verzoek Bekijken",
"components.PersonDetails.appearsin": "Verschijnt in",
"components.PersonDetails.ascharacter": "als {character}",
"components.PersonDetails.nobiography": "Geen biografie beschikbaar.",
"components.PlexLoginButton.loading": "Bezig met laden...",
"components.PlexLoginButton.loggingin": "Bezig met inloggen...",
"components.PlexLoginButton.loginwithplex": "Login met Plex",
"components.RequestBlock.seasons": "Seizoenen",
"components.RequestCard.all": "Alle",
"components.RequestCard.requestedby": "Verzocht door {username}",
"components.RequestCard.seasons": "Seizoenen",
"components.RequestList.RequestItem.notavailable": "Nvt",
"components.RequestList.RequestItem.requestedby": "Verzocht door {username}",
"components.RequestList.RequestItem.seasons": "Seizoenen",
"components.RequestList.mediaInfo": "Media Info",
"components.RequestList.modifiedBy": "Laatst Aangepast Door",
"components.RequestList.next": "Volgende",
"components.RequestList.previous": "Vorige",
"components.RequestList.requestedAt": "Aangevraagd Om",
"components.RequestList.requests": "Verzoeken",
"components.RequestList.showingresults": "<strong>{from}</strong> tot <strong>{to}</strong> van de <strong>{total}</strong> resultaten worden weergegeven",
"components.RequestList.status": "Status",
"components.RequestModal.cancel": "Verzoek Annuleren",
"components.RequestModal.cancelling": "Bezig met annuleren...",
"components.RequestModal.cancelrequest": "Hiermee wordt je verzoek verwijderd. Weet je zeker dat je door wilt gaan?",
"components.RequestModal.close": "Sluiten",
"components.RequestModal.extras": "Extra",
"components.RequestModal.notrequested": "Niet Verzocht",
"components.RequestModal.numberofepisodes": "Nr afleveringen",
"components.RequestModal.pendingrequest": "Wachtend verzoek voor {title}",
"components.RequestModal.request": "Verzoek",
"components.RequestModal.requestCancel": "Verzoek voor <strong>{title}</strong> is geannuleerd",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> is verzocht.",
"components.RequestModal.requestadmin": "Je verzoek zal onmiddelijk goedgekeurd worden.",
"components.RequestModal.requestfrom": "Er is een afwachtend verzoek van {user}",
"components.RequestModal.requesting": "Bezig met verzoeken…",
"components.RequestModal.requestseasons": "Verzoek {seasonCount} {seasonCount, plural, één {Season} andere {Seasons}}",
"components.RequestModal.requesttitle": "Verzoek {title}",
"components.RequestModal.season": "Seizoen",
"components.RequestModal.seasonnumber": "Seizoen {number}",
"components.RequestModal.selectseason": "Selecteer seizoen(en)",
"components.RequestModal.status": "Status",
"components.Search.searchresults": "Zoekresultaten",
"components.Settings.Notifications.agentenabled": "Agent Ingeschakeld",
"components.Settings.Notifications.authPass": "Wachtwoord",
"components.Settings.Notifications.authUser": "Gebruikersnaam",
"components.Settings.Notifications.emailsender": "E-mailadres Van Afzender",
"components.Settings.Notifications.enableSsl": "Schakel SSL in",
"components.Settings.Notifications.save": "Wijzigingen Opslaan",
"components.Settings.Notifications.saving": "Bezig met opslaan...",
"components.Settings.Notifications.smtpHost": "SMTP Host",
"components.Settings.Notifications.smtpPort": "SMTP Poort",
"components.Settings.Notifications.validationFromRequired": "Je moet een afzenderadres opgeven",
"components.Settings.Notifications.validationSmtpHostRequired": "Je moet een SMTP host opgeven",
"components.Settings.Notifications.validationSmtpPortRequired": "Je moet een SMTP poort opgeven",
"components.Settings.Notifications.validationWebhookUrlRequired": "Je moet een webhook URL opgeven",
"components.Settings.Notifications.webhookUrl": "Webhook URL",
"components.Settings.Notifications.webhookUrlPlaceholder": "Server instellingen -> Integraties -> Webhooks",
"components.Settings.RadarrModal.add": "Server Toevoegen",
"components.Settings.RadarrModal.apiKey": "API Key",
"components.Settings.RadarrModal.apiKeyPlaceholder": "Je Radarr API Key",
"components.Settings.RadarrModal.baseUrl": "Basis URL",
"components.Settings.RadarrModal.baseUrlPlaceholder": "Voorbeeld: /radarr",
"components.Settings.RadarrModal.createradarr": "Maak een nieuwe Radarr-server aan",
"components.Settings.RadarrModal.defaultserver": "Standaard Server",
"components.Settings.RadarrModal.editradarr": "Wijzig Radarr Server",
"components.Settings.RadarrModal.hostname": "Hostnaam",
"components.Settings.RadarrModal.minimumAvailability": "Minimale Beschikbaarheid",
"components.Settings.RadarrModal.port": "Poort",
"components.Settings.RadarrModal.qualityprofile": "Kwaliteitsprofiel",
"components.Settings.RadarrModal.rootfolder": "Hoofdmap",
"components.Settings.RadarrModal.save": "Wijzigingen Opslaan",
"components.Settings.RadarrModal.saving": "Bezig met opslaan...",
"components.Settings.RadarrModal.selectMinimumAvailability": "Selecteer minimale beschikbaarheid",
"components.Settings.RadarrModal.selectQualityProfile": "Selecteer een kwaliteitsprofiel",
"components.Settings.RadarrModal.selectRootFolder": "Selecteer een Hoofdmap",
"components.Settings.RadarrModal.server4k": "4K Server",
"components.Settings.RadarrModal.servername": "Servernaam",
"components.Settings.RadarrModal.servernamePlaceholder": "Een Radarr Server",
"components.Settings.RadarrModal.ssl": "SSL",
"components.Settings.RadarrModal.test": "Test",
"components.Settings.RadarrModal.testing": "Bezig met testen...",
"components.Settings.RadarrModal.toastRadarrTestFailure": "Kon niet verbinden met de Radarr Server",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Verbonden met de Radarr server!",
"components.Settings.RadarrModal.validationApiKeyRequired": "Je moet een API key opgeven",
"components.Settings.RadarrModal.validationHostnameRequired": "Je moet een hostnaam/IP opgeven",
"components.Settings.RadarrModal.validationPortRequired": "Je moet een poort opgeven",
"components.Settings.RadarrModal.validationProfileRequired": "Je moet een profiel selecteren",
"components.Settings.RadarrModal.validationRootFolderRequired": "Je moet een hoofdmap selecteren",
"components.Settings.SonarrModal.add": "Server Toevoegen",
"components.Settings.SonarrModal.apiKey": "API Key",
"components.Settings.SonarrModal.apiKeyPlaceholder": "Je Sonarr API Key",
"components.Settings.SonarrModal.baseUrl": "Basis URL",
"components.Settings.SonarrModal.baseUrlPlaceholder": "Voorbeeld: /sonarr",
"components.Settings.SonarrModal.createsonarr": "Maak een Nieuwe Sonar Server aan",
"components.Settings.SonarrModal.defaultserver": "Standaard Server",
"components.Settings.SonarrModal.editsonarr": "Wijzig Sonarr Server",
"components.Settings.SonarrModal.hostname": "Hostnaam",
"components.Settings.SonarrModal.port": "Poort",
"components.Settings.SonarrModal.qualityprofile": "Kwaliteitsprofiel",
"components.Settings.SonarrModal.rootfolder": "Basisfolder",
"components.Settings.SonarrModal.save": "Wijzigingen Opslaan",
"components.Settings.SonarrModal.saving": "Bezig met opslaan...",
"components.Settings.SonarrModal.seasonfolders": "Seizoenmappen",
"components.Settings.SonarrModal.selectQualityProfile": "Selecteer een Kwaliteitsprofiel",
"components.Settings.SonarrModal.selectRootFolder": "Selecteer een hoofdfolder",
"components.Settings.SonarrModal.server4k": "4K Server",
"components.Settings.SonarrModal.servername": "Servernaam",
"components.Settings.SonarrModal.servernamePlaceholder": "Een Sonarr Server",
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.test": "Test",
"components.Settings.SonarrModal.testing": "Bezig met testen...",
"components.Settings.SonarrModal.toastRadarrTestFailure": "Kon niet verbinden met de Sonarr Server",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Verbonden met de Sonarr server!",
"components.Settings.SonarrModal.validationApiKeyRequired": "Je moet een API key opgeven",
"components.Settings.SonarrModal.validationHostnameRequired": "Je moet een hostnaam/IP opgeven",
"components.Settings.SonarrModal.validationPortRequired": "Je moet een poort opgeven",
"components.Settings.SonarrModal.validationProfileRequired": "Je moet een profiel selecteren",
"components.Settings.SonarrModal.validationRootFolderRequired": "Je moet een hoofdfolder selecteren",
"components.Settings.activeProfile": "Actief profiel",
"components.Settings.addradarr": "Radarr Server Toevoegen",
"components.Settings.address": "Adres",
"components.Settings.addsonarr": "Voeg een Sonarr Server Toe",
"components.Settings.apikey": "API Key",
"components.Settings.applicationurl": "Applicatie URL",
"components.Settings.cancelscan": "Scan Annuleren",
"components.Settings.copied": "API key gekopieerd naar het klembord",
"components.Settings.currentlibrary": "Huidige Bibliotheek: {name}",
"components.Settings.default": "Standaard",
"components.Settings.default4k": "Standaard 4K",
"components.Settings.delete": "Verwijderen",
"components.Settings.deleteserverconfirm": "Weet je zeker dat je deze server wil verwijderen?",
"components.Settings.edit": "Wijzig",
"components.Settings.generalsettings": "Algemene Instellingen",
"components.Settings.generalsettingsDescription": "Deze instellingen zijn gerelateerd aan de algemene Overseer configuratie.",
"components.Settings.hostname": "Hostnaam/IP",
"components.Settings.jobname": "Taaknaam",
"components.Settings.librariesRemaining": "Resterende Bibliotheken: {count}",
"components.Settings.manualscan": "Handmatige Bibliotheek Scan",
"components.Settings.manualscanDescription": "Normaal gesproken wordt dit eens elke 6 uur gedraaid. Overseerr controleert de recentelijk toegoevoegde items van je Plex Server aggresiever. Als dit je eerst keer is om Plex te configuren raden wij aan om eenmaal een volledige bibliotheek scan handmatig te draaien!",
"components.Settings.menuAbout": "Over",
"components.Settings.menuGeneralSettings": "Algemene Instellingen",
"components.Settings.menuJobs": "Taken",
"components.Settings.menuLogs": "Logboeken",
"components.Settings.menuNotifications": "Notificaties",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Diensten",
"components.Settings.nextexecution": "Volgende Uitvoering",
"components.Settings.notificationsettings": "Notificatie Instellingen",
"components.Settings.notificationsettingsDescription": "Hier kun je kiezen welke soort notificaties er worden verstuurd en door welke type diensten.",
"components.Settings.notrunning": "Draait niet",
"components.Settings.plexlibraries": "Plex Bibliotheken",
"components.Settings.plexlibrariesDescription": "The bibliotheken die Overseerr scanned voor titels. Stel je Plex connectie instellingen in en sla ze op. Klik op de onderstaande knop als er geen connecties staan.",
"components.Settings.plexsettings": "Plex Instellingen",
"components.Settings.plexsettingsDescription": "Configureer de instellingen voor je Plex server. Overseerr gebruikt je Plex server om je bibliotheek met een interval te scannen om te zien welke content beschikbaar is.",
"components.Settings.port": "Poort",
"components.Settings.radarrSettingsDescription": "Stel hier onder je Radarr connectie in. Je kan er meerdere hebben, maar slechts twee actief hebben als standaard (één voor standaard HD, en één voor 4K). Beheerders kunnen bepalen welke server gebruikt wordt voor nieuwe verzoeken.",
"components.Settings.radarrsettings": "Radarr Instellingen",
"components.Settings.runnow": "Nu Starten",
"components.Settings.save": "Wijzigingen Opslaan",
"components.Settings.saving": "Bezig met opslaan...",
"components.Settings.servername": "Servernaam (Automatisch ingesteld na opslaan)",
"components.Settings.servernamePlaceholder": "Plex Servernaam",
"components.Settings.sonarrSettingsDescription": "Stel hier onder je Sonarr connectie in. Je kan er meerdere hebben, maar slechts twee actief hebben als standaard (één voor standaard HD, en één voor 4K). Beheerders kunnen bepalen welke server gebruikt wordt voor nieuwe verzoeken.",
"components.Settings.sonarrsettings": "Sonarr Instellingen",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Scan Starten",
"components.Settings.sync": "Sync Plex Bibliotheken",
"components.Settings.syncing": "Bezig met synchronizeren…",
"components.Setup.configureplex": "Configureer Plex",
"components.Setup.configureservices": "Configureer Diensten",
"components.Setup.continue": "Doorgaan",
"components.Setup.finish": "Installatie Voltooien",
"components.Setup.finishing": "Bezig met voltooien...",
"components.Setup.loginwithplex": "Login met Plex",
"components.Setup.signinMessage": "Ga aan de slag door in te loggen met je Plex account",
"components.Setup.welcome": "Welcome bij Overseerr",
"components.Slider.noresults": "Geen Resultaten",
"components.TitleCard.movie": "Film",
"components.TitleCard.tvshow": "Series",
"components.TvDetails.approve": "Goedkeuren",
"components.TvDetails.approverequests": "Keur {requestCount} {requestCount, plural, één {Request} meerdere {Requests}} goed",
"components.TvDetails.available": "Beschikbaar",
"components.TvDetails.cancelrequest": "Verzoek Annuleren",
"components.TvDetails.cast": "Cast",
"components.TvDetails.decline": "Weigeren",
"components.TvDetails.declinerequests": "Weiger {requestCount} {requestCount, plural, één {Request} meerdere {Requests}}",
"components.TvDetails.manageModalClearMedia": "Wis Alle Media Data",
"components.TvDetails.manageModalClearMediaWarning": "Dit wist alle media data inclusief alle verzoeken voor dit item zonder herstelmogelijkheden. Als dit item in je Plex bibliotheek bestaat zal alle media info bij de volgende sync hersteld worden.",
"components.TvDetails.manageModalNoRequests": "Geen Verzoeken",
"components.TvDetails.manageModalRequests": "Verzoeken",
"components.TvDetails.manageModalTitle": "Beheer Series",
"components.TvDetails.originallanguage": "Originele Taal",
"components.TvDetails.overview": "Overzicht",
"components.TvDetails.overviewunavailable": "Overzicht onbeschikbaar",
"components.TvDetails.pending": "Wachtende",
"components.TvDetails.recommendations": "Aanbevelingen",
"components.TvDetails.recommendationssubtext": "Als je {title} leuk vond, vind je dit misschien ook leuk...",
"components.TvDetails.request": "Verzoek",
"components.TvDetails.requestmore": "Verzoek Meer",
"components.TvDetails.similar": "Soortgelijke Series",
"components.TvDetails.similarsubtext": "Andere series gelijk aan {title}",
"components.TvDetails.status": "Status",
"components.TvDetails.unavailable": "Onbeschikbaar",
"components.TvDetails.userrating": "Gebruikersbeoordeling",
"components.UserEdit.admin": "Beheerder",
"components.UserEdit.adminDescription": "Volledige beheerderstoegang. Omzeilt alle toestemmingscontroles.",
"components.UserEdit.autoapprove": "Automatisch Goedkeuren",
"components.UserEdit.autoapproveDescription": "Geeft automatische goedkeuring voor alle verzoeken van deze gebruiker.",
"components.UserEdit.avatar": "Avatar",
"components.UserEdit.edituser": "Wijzig Gebruiker",
"components.UserEdit.email": "E-mail",
"components.UserEdit.managerequests": "Beheer Verzoeken",
"components.UserEdit.managerequestsDescription": "Geeft toestemming om Overseerr-verzoeken te beheren. Inclusief het goedkeuren en weigeren van verzoeken.",
"components.UserEdit.permissions": "Rechten",
"components.UserEdit.request": "Verzoek",
"components.UserEdit.requestDescription": "Geeft toestemming om films en series aan te vragen.",
"components.UserEdit.save": "Sla op",
"components.UserEdit.saving": "Bezig met opslaan...",
"components.UserEdit.settings": "Instellingen Beheren",
"components.UserEdit.settingsDescription": "Geeft toestemming om alle Overseerr-instellingen te wijzigen. Een gebruiker moet deze toestemming hebben om deze aan anderen te verlenen.",
"components.UserEdit.userfail": "Er ging iets verkeerd bij het opslaan van de gebruiker.",
"components.UserEdit.username": "Gebruikersnaam",
"components.UserEdit.users": "Beheer Gebruikers",
"components.UserEdit.usersDescription": "Geeft toestemming om Overseerr-gebruikers te beheren. Gebruikers met deze toestemming kunnen gebruikers met beheerdersrechten niet wijzigen of deze toestemming geven.",
"components.UserEdit.usersaved": "Gebruiker opgeslagen",
"components.UserEdit.vote": "Stem",
"components.UserEdit.voteDescription": "Geeft toestemming om te stemmen op verzoeken (stemmen is nog niet geïmplementeerd)",
"components.UserList.admin": "Beheerder",
"components.UserList.created": "Gemaakt",
"components.UserList.delete": "Verwijderen",
"components.UserList.edit": "Wijzig",
"components.UserList.lastupdated": "Laatst Aangepast",
"components.UserList.plexuser": "Plex Gebruiker",
"components.UserList.role": "Rol",
"components.UserList.totalrequests": "Totale Verzoeken",
"components.UserList.user": "Gebruiker",
"components.UserList.userlist": "Gebruikerslijst",
"components.UserList.username": "Gebruikersnaam",
"components.UserList.usertype": "Gebruikerstype",
"i18n.approve": "Goedkeuren",
"i18n.approved": "Goedgekeurd",
"i18n.available": "Beschikbaar",
"i18n.cancel": "Annuleer",
"i18n.decline": "Weigeren",
"i18n.declined": "Geweigerd",
"i18n.delete": "Verwijder",
"i18n.movies": "Films",
"i18n.partiallyavailable": "Gedeeltelijk Beschikbaar",
"i18n.pending": "Afwachtende",
"i18n.processing": "Bezig met verwerken…",
"i18n.tvshows": "Series",
"i18n.unavailable": "Onbeschikbaar",
"pages.internalServerError": "{statusCode} Interne Server Error",
"pages.oops": "Oeps",
"pages.pageNotFound": "404 Pagina Niet Gevonden",
"pages.returnHome": "Terug naar de Startpagina",
"pages.serviceUnavailable": "{statusCode} Service Onbeschikbaar",
"pages.somethingWentWrong": "{statusCode} Er is iets verkeerd gegaan",
"components.MovieDetails.MovieCast.fullcast": "Volledige Cast",
"components.TvDetails.TvCast.fullseriescast": "Volledige Cast van de Series",
"components.Settings.Notifications.emailsettingssaved": "Instellingen voor e-mailmeldingen opgeslagen!",
"components.Settings.Notifications.emailsettingsfailed": "Instellingen voor e-mailmeldingen zijn niet opgeslagen.",
"components.Settings.Notifications.discordsettingssaved": "Discord notificatie instellingen zijn opgeslagen!",
"components.Settings.Notifications.discordsettingsfailed": "Discord notificatie instellingen konden niet opgeslagen worden.",
"components.Settings.validationPortRequired": "Je moet een poort opgeven",
"components.Settings.validationHostnameRequired": "Je moet een hostnaam/IP opgeven",
"components.Settings.SonarrModal.validationNameRequired": "Je moet een servernaam opgeven",
"components.Settings.RadarrModal.validationNameRequired": "Je moet een servernaam opgeven",
"components.Settings.SettingsAbout.version": "Versie",
"components.Settings.SettingsAbout.totalrequests": "Totale Verzoeken",
"components.Settings.SettingsAbout.totalmedia": "Totale media",
"components.Settings.SettingsAbout.overseerrinformation": "Overseer Informatie",
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussies",
"components.Setup.tip": "Tip",
"components.Setup.syncingbackground": "Synchroniseren draait in de achtergrond. Je kan ondertussen door met het setup proces.",
"components.Settings.nodefaultdescription": "Tenminste één server moet geselecteerd zijn als standaard voordat verzoeken doorkomen bij je diensten.",
"components.Settings.nodefault": "Geen standaard server geselecteerd!",
"components.Settings.no4kimplemented": "(Standaard 4K servers zijn momenteel niet geïmplementeerd)",
"components.Settings.SonarrModal.testFirstRootFolders": "Test je connectie om hoofdfolders te laden",
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test je connectie om kwaliteitsprofielen te laden",
"components.Settings.SonarrModal.loadingrootfolders": "Bezig met laden van hoofdfolders…",
"components.Settings.SonarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…",
"components.Settings.SettingsAbout.gettingsupport": "Ondersteuning Krijgen",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Klik hier voor onze Discord server.",
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Je moet een minimale bechikbaarheid selecteren",
"components.Settings.RadarrModal.testFirstRootFolders": "Test je connectie om hoofdfolders te laden",
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test je connectie om kwaliteitsprofielen te laden",
"components.Settings.RadarrModal.loadingrootfolders": "Bezig met laden van hoofdfolders…",
"components.Settings.RadarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…"
}

View File

@@ -1,7 +1,7 @@
{
"components.Discover.discovermovies": "Популярные фильмы",
"components.Discover.discovertv": "Популярные серии",
"components.Discover.nopending": "",
"components.Discover.nopending": "Нет отложенных запросов",
"components.Discover.popularmovies": "Популярные фильмы",
"components.Discover.populartv": "Популярные серии",
"components.Discover.recentlyAdded": "Недавно добавленные",

View File

@@ -12,6 +12,7 @@ import { IntlProvider } from 'react-intl';
import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
import Head from 'next/head';
import Toast from '../components/Toast';
import { InteractionProvider } from '../context/InteractionContext';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadLocaleData = (locale: string): Promise<any> => {
@@ -28,6 +29,8 @@ const loadLocaleData = (locale: string): Promise<any> => {
return import('../i18n/locale/ru.json');
case 'nl':
return import('../i18n/locale/nl.json');
case 'es':
return import('../i18n/locale/es.json');
default:
return import('../i18n/locale/en.json');
}
@@ -88,12 +91,14 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
defaultLocale="en"
messages={loadedMessages}
>
<ToastProvider components={{ Toast }}>
<Head>
<title>Overseerr</title>
</Head>
<UserContext initialUser={user}>{component}</UserContext>
</ToastProvider>
<InteractionProvider>
<ToastProvider components={{ Toast }}>
<Head>
<title>Overseerr</title>
</Head>
<UserContext initialUser={user}>{component}</UserContext>
</ToastProvider>
</InteractionProvider>
</IntlProvider>
</LanguageContext.Provider>
</SWRConfig>
@@ -109,7 +114,7 @@ CoreApp.getInitialProps = async (initialProps) => {
if (ctx.res) {
// Check if app is initialized and redirect if necessary
const response = await axios.get<{ initialized: boolean }>(
`http://localhost:${process.env.PORT || 3000}/api/v1/settings/public`
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
);
const initialized = response.data.initialized;
@@ -125,7 +130,7 @@ CoreApp.getInitialProps = async (initialProps) => {
try {
// Attempt to get the user by running a request to the local api
const response = await axios.get<User>(
`http://localhost:${process.env.PORT || 3000}/api/v1/auth/me`,
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }
);
user = response.data;

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { NextPage } from 'next';
import Holiday from '../../components/Discover/Holiday';
const HolidayPage: NextPage = () => {
return <Holiday />;
};
export default HolidayPage;

View File

@@ -0,0 +1,9 @@
import { NextPage } from 'next';
import React from 'react';
import MovieCast from '../../../components/MovieDetails/MovieCast';
const MovieCastPage: NextPage = () => {
return <MovieCast />;
};
export default MovieCastPage;

View File

@@ -17,7 +17,7 @@ MoviePage.getInitialProps = async (ctx) => {
if (ctx.req) {
const cookies = parseCookies(ctx);
const response = await axios.get<MovieDetailsType>(
`http://localhost:${process.env.PORT || 3000}/api/v1/movie/${
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
ctx.query.movieId
}${cookies.locale ? `?language=${cookies.locale}` : ''}`,
{

View File

@@ -0,0 +1,17 @@
import { NextPage } from 'next';
import React from 'react';
import SettingsAbout from '../../components/Settings/SettingsAbout';
import SettingsLayout from '../../components/Settings/SettingsLayout';
import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const SettingsAboutPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS);
return (
<SettingsLayout>
<SettingsAbout />
</SettingsLayout>
);
};
export default SettingsAboutPage;

View File

@@ -0,0 +1,14 @@
import { NextPage } from 'next';
import React from 'react';
import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsLogs from '../../components/Settings/SettingsLogs';
const SettingsLogsPage: NextPage = () => {
return (
<SettingsLayout>
<SettingsLogs />
</SettingsLayout>
);
};
export default SettingsLogsPage;

View File

@@ -0,0 +1,9 @@
import { NextPage } from 'next';
import React from 'react';
import TvCast from '../../../components/TvDetails/TvCast';
const TvCastPage: NextPage = () => {
return <TvCast />;
};
export default TvCastPage;

View File

@@ -17,7 +17,7 @@ TvPage.getInitialProps = async (ctx) => {
if (ctx.req) {
const cookies = parseCookies(ctx);
const response = await axios.get<TvDetailsType>(
`http://localhost:${process.env.PORT || 3000}/api/v1/tv/${
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${
ctx.query.tvId
}${cookies.locale ? `?language=${cookies.locale}` : ''}`,
{

View File

@@ -7,7 +7,7 @@ body {
}
.plex-button {
@apply w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 transition ease-in-out duration-150 text-center;
@apply w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 transition ease-in-out duration-150 text-center disabled:opacity-50;
background-color: #cc7b19;
}
@@ -47,3 +47,7 @@ body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
code {
@apply bg-gray-800 py-1 px-2 rounded-md;
}

View File

@@ -3,7 +3,7 @@ export type Nullable<T> = T | null;
export type Maybe<T> = T | null | undefined;
/**
* Helps type objects with an abitrary number of properties that are
* Helps type objects with an arbitrary number of properties that are
* usually being defined at export.
*
* @param component Main object you want to apply properties to

View File

@@ -1020,27 +1020,13 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@7.12.5":
"@babel/runtime@7.12.5", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740"
integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@@ -7109,9 +7095,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
init-package-json@^1.10.3:
version "1.10.3"
@@ -14197,10 +14183,10 @@ yn@3.1.1:
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yup@^0.32.6:
version "0.32.6"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.6.tgz#7fd924536cdf61ec5df7a4bd0576d0a3954b03f6"
integrity sha512-i5poB/ZWU0zGoCwViLw++NQbUFy+X9xfoSSK31JOLh3RoBVUaYrNlYIyxqK8HGrxTHPIl5BBNu/mJYKNeFVvdA==
yup@^0.32.8:
version "0.32.8"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.8.tgz#16e4a949a86a69505abf99fd0941305ac9adfc39"
integrity sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA==
dependencies:
"@babel/runtime" "^7.10.5"
"@types/lodash" "^4.14.165"