Compare commits

...

43 Commits

Author SHA1 Message Date
semantic-release-bot
63dfe003b0 chore(release): 2.0.0 2024-10-15 17:55:56 +00:00
fallenbagel
a47db19ae7 chore(release): prepare for v1.10 release 2024-10-16 01:53:45 +08:00
Gauthier
92ba26207d feat: refresh monitored downloads before getting queue items (#994)
Currently, we sync with sonarr/radarr with whatever value those return. Radarr/Sonarr syncs the
activity from the download clients every few minutes. This leads to inaccurate estimated download
times, because of the refresh delay with Jellyseerr and the *arrs.

This PR fixes this by making a request to the *arrs to refresh the monitored downloads just before
we get these downloads information.

re #866
2024-10-10 11:37:08 +02:00
Gauthier
96e1d40304 fix(session): set the correct TTL for the cookie store (#992)
The time-to-live (TTL) of cookies stored in the database was incorrect because the connect-typeorm
library takes a TTL in seconds and not milliseconds, making cookies valid for ~82 years instead of
30 days.

fix #991
2024-10-02 20:59:35 +02:00
Thomas Loubiou
a5d22ba5b8 feat: allow request managers to delete data from sonarr/radarr (#644)
* feat: allow requests managers to delete media files

* fix(i18n): add missing translations

* fix(i18n): remove french translation

* refactor: use fetch API
2024-09-30 18:56:25 +02:00
Gauthier
f390da4866 fix(blacklist): add blacklist to mobile menu (#980)
* fix(blacklist): add blacklist to mobile menu

The "Blacklist" menu was only available in the desktop sidebar, not in the mobile menu.

fix #979

* fix: export translations
2024-09-25 21:25:44 +02:00
Joaquin Olivero
edfd80444c refactor: Proxy and cache avatar images (#907)
* refactor: proxy and cache user avatar images

* fix: extract keys

* fix: set avatar image URL

* fix: show the correct avatar in the list of available users in advanced request

* fix(s): set correct src URL for cached image

* fix: remove unexpired unused image when a user changes their avatar

* fix: requested changes

* refactor: use 'mime' package to detmerine file extension

* style: grammar

* refactor: checks if the default avatar is cached to avoid creating duplicates for different users

* fix: fix vulnerability

* fix: fix incomplete URL substring sanitization

* refactor: only cache avatar with http url protocol

* fix: remove log and correctly set the if statement for the cached image component

* fix: avatar images not showing on issues page

* style: formatting

---------

Co-authored-by: JoaquinOlivero <joaquin.olivero@hotmail.com>
2024-09-19 10:38:14 +08:00
Gauthier
2b05ffface chore(issuetemplate): update defaults labels of GitHub issues (#968) 2024-09-17 11:12:00 +05:00
Joaquin Olivero
818aa60aac feat: blacklist items from Discover page (#632)
* feat: blacklist media items

re #490

* feat: blacklist media items

* feat: blacklist media items

* style: formatting

* refactor: close the manage slide-over when the media item is removed from the blacklist

* fix: fix media data in the db when blacklisting an item

* refactor: refactor component to accept show boolean

* refactor: hide watchlist button in the media page when it's blacklisted. Also add a blacklist button

* style: formatting

---------

Co-authored-by: JoaquinOlivero <joaquin.olivero@hotmail.com>
2024-09-16 22:08:12 +02:00
Jonas F
ee7e91c7c9 fix: change SeriesSearch to MissingEpisodeSearch for season requests (#711)
This fix changes the behavior of how Overseerr requests series data from Sonarr. Previously, when adding new seasons to a partially available series, Overseerr would initiate a SeriesSearch, causing Sonarr to search for all monitored seasons of the series, including those already available. This behavior is now corrected by executing a MissingEpisodeSearchCommand for the specific seriesId, which aligns with the intended behavior of only searching for and adding the newly requested seasons that are not already available.

Resolves: https://github.com/Fallenbagel/jellyseerr/issues/710
2024-09-16 22:07:43 +05:00
Aidan Hilt
45ef150e36 feat: add environment variable for API key (#831)
* Added the ability to set the API key with the env var API_KEY

* Adding debug statements

* Updating

* feat: adding env var for API key

* feat: update

* fix(settings/index.ts): remove a print statement that logs the API key to the console

* Update en.json

* docs: added documentation about API_KEY environment variable

* feat: add a check to ensure API key always uses env var if provided

* feat: always check the API_KEY env var at startup

* chore: add back the gitkeeps under ./config, accidentally deleted in prev commit

* chore: revert change made to docker-compose that was accidentally committed
2024-09-16 19:05:44 +02:00
Fallenbagel
54cfeefe74 docs(readme): update the translate badge (#951) 2024-08-28 21:58:04 +05:00
Gauthier
89e0a831ec fix: add an error message to say when an email is already taken (#947)
When the email is modified in the user settings and it is already taken by someone else, a generic
message saying that something wrong happened, without saying that it is because the email is already
taken by another user. This PR adds this error message for the email.
2024-08-27 12:54:56 +02:00
Joaquin Olivero
e57d2654d1 fix: set correct user type when importing from emby (#949)
fix #948

Co-authored-by: JoaquinOlivero <joaquin.olivero@hotmail.com>
2024-08-26 22:14:46 +02:00
Gauthier
a02a0dd176 docs: fix broken anchors (#946) 2024-08-26 17:27:06 +05:00
Gauthier
7423bbbffc fix(setup): page display when homepage is loading (#940)
* fix: remove an unwanted display of the setup page while the homepage is loading

* fix: edit incorrect return type of setup page
2024-08-22 17:46:06 +05:00
Fallenbagel
32343f23a3 chore(migrations): proper rename & clean up of media server type migration (#939) 2024-08-22 14:37:41 +02:00
Fallenbagel
15cb949f1f feat: Jellyfin/Emby server type setup (#685)
* feat: add Media Server Selection to Setup Page

Introduce the ability to select the media server type on the setup page. Users can now choose their
preferred media server (e.g., Plex through the Plex sign-in or Emby/Jellyfin sign-in to select
either Emby or Jellyfin). The selected media server type is then reflected in the application
settings. This enhancement provides users with increased flexibility and customization options
during the initial setup process, eliminating the need to rely on environment variables (which
cannot be set if using platforms like snaps). Existing Emby users, who use the environment variable,
should log out and log back in after updating to set their mediaServerType to Emby.

BREAKING CHANGE: This commit deprecates the JELLYFIN_TYPE variable to identify Emby media server and
instead rely on the mediaServerType that is set in the `settings.json`. Existing environment
variable users can log out and log back in to set the mediaServerType to `3` (Emby).

* feat(api): add severType to the api

BREAKING CHANGE: This adds a serverType to the `/auth/jellyfin` which requires a serverType to be
set (`jellyfin`/`emby`)

* refactor: use enums for serverType and rename selectedservice to serverType

* refactor(auth): jellyfin/emby authentication to set MediaServerType

* fix: issue page formatMessage for 4k media

* refactor: cleaner way of handling serverType change using MediaServerType instead of strings

instead of using strings now it will use MediaServerType enums for serverType

* revert: removed conditional render of the auto-request permission

reverts the conditional render toshow the auto-request permission if the mediaServerType was set to
Plex as this should be handled in a different PR and Cypress tests should be modified
accordingly(currently cypress test would fail if this conditional check is there)

* feat: add server type step to setup

* feat: migrate existing emby setups to use emby mediaServerType

* fix: scan jobs not running when media server type is emby

* fix: emby media server type migration

* refactor: change emby logo to full logo

* style: decrease emby logo size in setup screen

* refactor: use title case for servertype i18n message

* refactor(i18n): fix a typo

* refactor: use enums instead of numbers

* fix: remove old references to JELLYFIN_TYPE environment variable

* fix: go back to the last step when refresh the setup page

* fix: move "scanning in background" tip next to the scanning section

* fix: redirect the setup page when Jellyseerr is already setup

---------

Co-authored-by: Gauthier <mail@gauthierth.fr>
2024-08-21 02:35:47 +05:00
Joaquin Olivero
cfd1bc2535 feat: adds status filter for tv shows (#796)
re #605

Co-authored-by: JoaquinOlivero <joaquin.olivero@hotmail.com>
2024-08-19 23:37:04 +05:00
Gauthier
80f63017ac fix: handle status badge for season packs (#927)
* fix: handle status badge for season packs

When a series is downloaded with a season pack, the status tooltip displays only the name of the
first episode as a title, and displays a list of all episodes as a description, with the same file
being repeated for each episode. This PR fixes this, using the season number as the tooltip title
and showing only the season pack file currently being downloaded.

* fix: add missing i18n translation
2024-08-19 09:02:52 +05:00
jellyseerr-weblate
0c7e652672 refactor(i18n): merge weblate (#934)
* Added translation using Weblate (Slovenian)

* Translated using Weblate (German)
Currently translated at 93.1% (1216 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/de/

* Translated using Weblate (Slovenian)
Currently translated at 4.1% (54 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/sl/

* Translated using Weblate (Dutch)
Currently translated at 99.4% (1299 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/nl/

* Added translation using Weblate (Turkish)

* Translated using Weblate (Turkish)
Currently translated at 7.2% (95 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (German)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/de/

* Translated using Weblate (Romanian)
Currently translated at 33.5% (438 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ro/

* Translated using Weblate (Russian)
Currently translated at 96.6% (1262 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ru/

* Translated using Weblate (Russian)
Currently translated at 97.4% (1273 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ru/

* Translated using Weblate (Russian)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ru/

* Translated using Weblate (Dutch)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/nl/

* Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/zh_Hans/

* Translated using Weblate (Spanish)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/es/

* Translated using Weblate (French)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/fr/

* Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.0% (1254 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pt_BR/

* Translated using Weblate (Hebrew)
Currently translated at 15.2% (199 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/he/

* Translated using Weblate (Polish)
Currently translated at 82.6% (1079 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/

* Translated using Weblate (Polish)
Currently translated at 83.9% (1096 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/

* Translated using Weblate (Spanish)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/es/

* Translated using Weblate (Ukrainian)
Currently translated at 93.7% (1225 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Ukrainian)
Currently translated at 94.3% (1232 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Ukrainian)
Currently translated at 94.3% (1232 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Polish)
Currently translated at 84.1% (1099 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/

* Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Catalan)
Currently translated at 94.1% (1230 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ca/

* Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/

* Translated using Weblate (Hebrew)
Currently translated at 23.0% (301 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/he/

* Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/zh_Hans/

* Translated using Weblate (Hebrew)
Currently translated at 26.4% (346 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/he/

* Translated using Weblate (German)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/de/

* Translated using Weblate (Polish)
Currently translated at 94.4% (1233 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/

* Translated using Weblate (Turkish)
Currently translated at 19.2% (252 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (Slovenian)
Currently translated at 4.2% (56 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/sl/

* Translated using Weblate (Turkish)
Currently translated at 42.7% (558 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (Turkish)
Currently translated at 44.6% (583 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (Turkish)
Currently translated at 75.6% (988 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (Turkish)
Currently translated at 85.0% (1111 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (Turkish)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/

* Translated using Weblate (Swedish)
Currently translated at 99.7% (1303 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/sv/

* style(i18n): ran prettier

* style(i18n): ran prettier

---------

Co-authored-by: Boštjan KOLAR <civywl@users.noreply.jellyseerr.borgcube.de>
Co-authored-by: Alex F <weblate@xathon.de>
Co-authored-by: Bas <910100490+weblate@proton.me>
Co-authored-by: N/A <me@puffin.icu>
Co-authored-by: Ramon Stohr <ramonstohr@gmail.com>
Co-authored-by: Cosmin Mocan <cosmin_mocan@hotmail.com>
Co-authored-by: Aleksandr <AlexZagric@users.noreply.jellyseerr.borgcube.de>
Co-authored-by: Aleksandr <alexzag2004@gmail.com>
Co-authored-by: Bas Muldder <bas.d.mulder@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Co-authored-by: Eduard Perez Mendez <eduardperezmendez@gmail.com>
Co-authored-by: Quack6765 <weblate@po-mail.com>
Co-authored-by: grayair <grayair@proton.me>
Co-authored-by: osh <osh@osh.cc>
Co-authored-by: uqlel <jellyseer.borgcube.de@uqlel.ovh>
Co-authored-by: Flashk <mevengar@gmail.com>
Co-authored-by: michael <michaelvelosk@gmail.com>
Co-authored-by: A a <arnau2106@gmail.com>
Co-authored-by: Albert Einstien <dbig350@gmail.com>
Co-authored-by: C W <the-eggs@163.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Adrian Konopczynski <adrikonop@anomalie.ga>
Co-authored-by: Wiktor Kowalski <a1opnxgtr@mozmail.com>
Co-authored-by: Jakob Števanec <jakolin98.windowslive@gmail.com>
Co-authored-by: Mattias Magnusson <mattish.91@gmail.com>
Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
2024-08-16 23:02:16 +05:00
semantic-release-bot
65def9d20d chore(release): 1.9.2 2024-06-13 09:32:07 +00:00
fallenbagel
a302929966 Merge remote-tracking branch 'origin/develop' 2024-06-13 14:30:15 +05:00
semantic-release-bot
f735d86064 chore(release): 1.9.1 2024-06-12 05:52:01 +00:00
fallenbagel
66c5de2bfa Merge remote-tracking branch 'origin/develop' 2024-06-12 10:49:50 +05:00
fallenbagel
6cf1ac7295 Merge remote-tracking branch 'origin/develop' 2024-06-12 10:38:58 +05:00
semantic-release-bot
25bf4b275a chore(release): 1.9.0 2024-05-29 12:53:56 +00:00
fallenbagel
103f028d99 Merge remote-tracking branch 'origin/develop' 2024-05-29 16:26:32 +05:00
semantic-release-bot
2101d0fff5 chore(release): 1.8.1 2024-04-17 19:08:15 +00:00
fallenbagel
09f50ac80f Merge branch 'develop' 2024-04-18 00:05:45 +05:00
semantic-release-bot
24fde7aec2 chore(release): 1.8.0 2024-04-15 21:49:19 +00:00
fallenbagel
d03bdf0cf9 Merge branch 'develop' 2024-04-16 02:46:59 +05:00
Fallenbagel
12986990ae Merge origin/develop into main (#716)
* fix(i18n): fixed jellyfin jobs

* feat: translations update from Hosted Weblate (#3258)

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

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Korean)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Korean)

Currently translated at 11.2% (139 of 1233 strings)

feat(lang): translated using Weblate (Korean)

Currently translated at 11.3% (139 of 1226 strings)

feat(lang): translated using Weblate (Korean)

Currently translated at 7.8% (96 of 1226 strings)

feat(lang): translated using Weblate (Korean)

Currently translated at 7.4% (91 of 1226 strings)

feat(lang): translated using Weblate (Korean)

Currently translated at 1.7% (21 of 1226 strings)

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

Co-authored-by: Developer J <jshsakura@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: 김상구 (Studio) <spair0039@gmail.com>
Co-authored-by: 최효근 <gyrms7532@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ko/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (Greek)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Greek)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Greek)

Currently translated at 100.0% (1233 of 1233 strings)

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

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

Currently translated at 49.7% (608 of 1222 strings)

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

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

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1228 of 1228 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1226 of 1226 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1224 of 1224 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1223 of 1223 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1222 of 1222 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (1222 of 1222 strings)

Co-authored-by: Angel <adelpozoman@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 33.0% (408 of 1234 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 29.7% (367 of 1234 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 27.9% (345 of 1234 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 27.8% (344 of 1233 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 27.6% (339 of 1226 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 27.4% (337 of 1226 strings)

feat(lang): translated using Weblate (Romanian)

Currently translated at 22.8% (279 of 1223 strings)

Co-authored-by: Bunduc Dragos <bunduc.dragos@gmail.com>
Co-authored-by: DragoPrime <emperordrago@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ro/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1223 of 1223 strings)

feat(lang): translated using Weblate (Russian)

Currently translated at 87.4% (1069 of 1223 strings)

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

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

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 100.0% (1226 of 1226 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 100.0% (1222 of 1222 strings)

feat(lang): translated using Weblate (Danish)

Currently translated at 100.0% (1222 of 1222 strings)

Co-authored-by: Anders Ecklon <aecklon@gmail.com>
Co-authored-by: Emil Nymann <ens@hiper.dk>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/da/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 86.3% (1055 of 1222 strings)

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

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

Currently translated at 99.4% (1226 of 1233 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1226 of 1226 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1224 of 1224 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1223 of 1223 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (1222 of 1222 strings)

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

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

Currently translated at 99.6% (1229 of 1233 strings)

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

Currently translated at 100.0% (1228 of 1228 strings)

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

Currently translated at 100.0% (1226 of 1226 strings)

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

Currently translated at 100.0% (1224 of 1224 strings)

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

Currently translated at 100.0% (1223 of 1223 strings)

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

Currently translated at 100.0% (1222 of 1222 strings)

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

Currently translated at 99.2% (1213 of 1222 strings)

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

Currently translated at 99.1% (1212 of 1222 strings)

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

Currently translated at 99.1% (1212 of 1222 strings)

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

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

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 99.7% (1223 of 1226 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 100.0% (1223 of 1223 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 96.4% (1179 of 1222 strings)

feat(lang): translated using Weblate (Czech)

Currently translated at 89.1% (1090 of 1222 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marek <marek@pavelka.xyz>
Co-authored-by: Smexhy <roman.bartik@icloud.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/cs/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (Arabic)

Currently translated at 100.0% (1233 of 1233 strings)

Co-authored-by: Fhd-pro <juve.11@msn.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ar/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1222 of 1222 strings)

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

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

Currently translated at 99.9% (1233 of 1234 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.5% (1228 of 1234 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.5% (1227 of 1233 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (1226 of 1226 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (1224 of 1224 strings)

feat(lang): translated using Weblate (German)

Currently translated at 95.9% (1172 of 1222 strings)

feat(lang): translated using Weblate (German)

Currently translated at 95.9% (1172 of 1222 strings)

feat(lang): translated using Weblate (German)

Currently translated at 94.7% (1158 of 1222 strings)

Co-authored-by: Ben <ben.david.wallner@gmail.com>
Co-authored-by: Furkan Çakar <cakar.55.furkan@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Juli <snowjuli@protonmail.com>
Co-authored-by: Leo Schultheiss <leoschultheiss@yahoo.de>
Co-authored-by: inkarnation <94744834+inkarnation@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 99.6% (1229 of 1233 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (1228 of 1228 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (1226 of 1226 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 90.0% (1104 of 1226 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 90.0% (1101 of 1222 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Topfield99 <timmiesonne@live.se>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 58.7% (725 of 1233 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 58.6% (719 of 1226 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 51.0% (624 of 1222 strings)

feat(lang): translated using Weblate (Lithuanian)

Currently translated at 43.9% (537 of 1222 strings)

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

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

Currently translated at 100.0% (1226 of 1226 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1224 of 1224 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1224 of 1224 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1223 of 1223 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 99.5% (1217 of 1223 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 99.4% (1216 of 1223 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 98.6% (1207 of 1223 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 97.2% (1189 of 1223 strings)

feat(lang): translated using Weblate (Catalan)

Currently translated at 94.3% (1154 of 1223 strings)

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

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

Currently translated at 89.9% (1103 of 1226 strings)

Co-authored-by: Bruno Ševčenko <bs3vcenk@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 92.2% (1138 of 1233 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 88.5% (1092 of 1233 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 85.8% (1058 of 1233 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 86.0% (1052 of 1223 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 83.2% (1017 of 1222 strings)

Co-authored-by: Francesco <francy.ammirati@hotmail.com>
Co-authored-by: Gian Marco Cinalli <gm.cinalli@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mirco Cau <mircocau@gmail.com>
Co-authored-by: eggermn <egger.mn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1233 of 1233 strings)

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

Currently translated at 100.0% (1226 of 1226 strings)

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

Currently translated at 99.9% (1225 of 1226 strings)

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

Currently translated at 99.9% (1225 of 1226 strings)

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

Currently translated at 99.8% (1224 of 1226 strings)

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

Currently translated at 99.9% (1223 of 1224 strings)

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

Currently translated at 100.0% (1222 of 1222 strings)

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

Currently translated at 98.0% (1198 of 1222 strings)

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

Currently translated at 96.7% (1182 of 1222 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jassy lin <linjiaxinme@gmail.com>
Co-authored-by: anpplex <anpplex@gmail.com>
Co-authored-by: kx <yoboy.rox@gmail.com>
Co-authored-by: lkw123 <lkw20010211@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1234 of 1234 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1233 of 1233 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.9% (1225 of 1226 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1224 of 1224 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1223 of 1223 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1222 of 1222 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1222 of 1222 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.5% (1216 of 1222 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.5% (1216 of 1222 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.5% (1216 of 1222 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (1222 of 1222 strings)

Co-authored-by: Hordo <hordocast@mailo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Rémi Guerrero <remidu34070@hotmail.fr>
Co-authored-by: Sulli <susu.leduc@gmail.com>
Co-authored-by: Symness <simon@frayssines.fr>
Co-authored-by: Valentin <droidente@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 99.7% (1219 of 1222 strings)

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

Currently translated at 89.6% (1095 of 1222 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

---------

Co-authored-by: Developer J <jshsakura@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: 김상구 (Studio) <spair0039@gmail.com>
Co-authored-by: 최효근 <gyrms7532@gmail.com>
Co-authored-by: BeardedWatermelon <periklis.karantonis@gmail.com>
Co-authored-by: Dzonkins <nikoladjordjevic.ns@gmail.com>
Co-authored-by: Angel <adelpozoman@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Bunduc Dragos <bunduc.dragos@gmail.com>
Co-authored-by: DragoPrime <emperordrago@gmail.com>
Co-authored-by: Kirill Zhukov <siper13@gmail.com>
Co-authored-by: Anders Ecklon <aecklon@gmail.com>
Co-authored-by: Emil Nymann <ens@hiper.dk>
Co-authored-by: ZsiGiT <zsigit@gmail.com>
Co-authored-by: Bas <bashankamp+weblate@gmail.com>
Co-authored-by: COTMO <moermantom1@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Mateus <mateusbernardo@protonmail.com>
Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Marek <marek@pavelka.xyz>
Co-authored-by: Smexhy <roman.bartik@icloud.com>
Co-authored-by: Fhd-pro <juve.11@msn.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Ben <ben.david.wallner@gmail.com>
Co-authored-by: Furkan Çakar <cakar.55.furkan@gmail.com>
Co-authored-by: Juli <snowjuli@protonmail.com>
Co-authored-by: Leo Schultheiss <leoschultheiss@yahoo.de>
Co-authored-by: inkarnation <94744834+inkarnation@users.noreply.github.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Topfield99 <timmiesonne@live.se>
Co-authored-by: PovilasID <povilas.sidaravicius@gmail.com>
Co-authored-by: Maite Guix <maite.guix@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Bruno Ševčenko <bs3vcenk@gmail.com>
Co-authored-by: Francesco <francy.ammirati@hotmail.com>
Co-authored-by: Gian Marco Cinalli <gm.cinalli@gmail.com>
Co-authored-by: Mirco Cau <mircocau@gmail.com>
Co-authored-by: eggermn <egger.mn@gmail.com>
Co-authored-by: Jassy lin <linjiaxinme@gmail.com>
Co-authored-by: anpplex <anpplex@gmail.com>
Co-authored-by: kx <yoboy.rox@gmail.com>
Co-authored-by: lkw123 <lkw20010211@gmail.com>
Co-authored-by: Hordo <hordocast@mailo.com>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Rémi Guerrero <remidu34070@hotmail.fr>
Co-authored-by: Sulli <susu.leduc@gmail.com>
Co-authored-by: Symness <simon@frayssines.fr>
Co-authored-by: Valentin <droidente@gmail.com>
Co-authored-by: 주서현 <adan.89lion@gmail.com>

* feat: add Peacock to Network Slider (#3545)

* feat: add tooltips to tautulli avatars (#3601)

* named service inside docker-compose.yml

* Fix permissions on ManageSliderOver

Previously, would cause a 403 error when a non-admin user opened a movie/series page

* feat: add ko language (#3619)

* style: fix prettier errors

* Update de.json

Added a german translation for 
  "components.Discover.RecentlyAddedSlider.recentlyAdded": "Recently Added",

* feat: select default seriesType for anime (#3627)

* feat: select default seriesType for anime

Added flexibility to set default anime series type in service settings. Now you can choose
'standard' for anime if you prefer it, making it easier to use features like searching for season
packs on Sonarr.

fix #3626

* feat: extracted translations

* feat: standard series type selector (#3628)

* feat: added a standard series type selector

* fix: moved series type property to correct interface

* feat(notif): add Pushover sound options (#2403)

Co-authored-by: Danshil Kokil Mungur <me@danshilm.com>

* chore: specify files/directories to exclude from git archives (#2184)

Co-authored-by: Danshil Kokil Mungur <me@danshilm.com>

* feat: update SameSite policy of session cookie to Lax (#3650)

* update session cookie samesite policy to lax

* set cookie samesite policy based on csrf protection setting

* fix: resolved issue with region selector and all regions value (#3652)

* docs: add RemiRigal as a contributor for code (#3653) [skip ci]

* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>

* fix: request watchlist items sequentially to prevent bypassing quota (#3667)

* build: update node to 20.9 (#3668)

* build: do not link python for arm (#3670)

* docs: adds jellyseerr commit links

Adds jellyseerr commit links to Fallenbagel. TODO: add other contributors of jellyseerr into the
list

* update emoji for jellyseerr contributor

* Too many jellyfishes

* build: update docker ubuntu images to 22.04 (#3671)

* build: use node 18 (#3675)

* build: add global node-gyp for arm (#3676)

* build: correct node version in snapcraft (#3678)

* chore(translations): fixed watchlist translation so its generic for all media servers

* revert(jellyfinapi): reverts #450 as it broke library sync support for local accounts using LDAP

Reverted #450 which addressed the issue where the automatic grouping enabled libraries were not
functioning correctly. The previous fix inadvertently caused a bug for Jellyfin LDAP users,
preventing library syncing with a 401 error. Reverting this change temporarily until support for
automatic library grouping can be re-implemented

fix #489

* fix(langcode): fixes the ukranian language code

This changes the ukranian language code from ua to uk to fit to ISO 639-1 format that the tmdb api
uses.

fix #504

* fix(jellyfinlogin): use externalHostname if set for forgetpassword link

Implemented dynamic URL generation for the 'Forgot Password' feature. If jellyfin external hostname
is set, the URL is generated based on it; otherwise, jellyfin hostname is used as the base URL. The
URL includes additional parameters to handle emby support.

fix #199, fix #424, re #212

* ci(build): changes the base of the snap build to fix compatibility issues with GLIBC version

Changes base to core20 in an attempt to fix the error `node: /lib/x86_64-linux-gnu/libc.so.6:
version `GLIBC_2.28' not found (required by node)` during snap builds

* build(snap): changes node-js plugin to npm plugin for Core20

In an attempt to fix version compatibility of `GLIBC_2.28` base was upgraded from core18 to core20.
Node-js plugin was deprecated for core20 and instead npm plugin has to be used. As npm plugin cannot
specify the package manager to use, yarn is now installed globally during the override-build phase.

* build(snap): fix path for the build-environment

* build(snap): use nil package and try to setup node in override-build step

* build(snap): add yarn install before yarn build

* build(snap): add frozen-lockfile and increase network timeout for yarn install

* build(snap): remove `rm .gitbook.yaml` line to fix snap builds

* fix(watchlist): discover local watchlist item display and profile local watchlist slider visibility

Previously when you expand the `Your Watchlist` slider from the discover page to see all your
watchlist items, you only see the first 20 items. This commit fixes that so you can see all your
local watchlist items when you expand that slider. In addition, this commit also fixes the visiblity
of profile watchlist slider for local watchlists

* refactor: cleans up local watchlist logic and fixes translation extractions

* fix: fix the translations for watchlist permissions and userSettings page

* docs: [skip ci] change contributor settings to add both upstream and downstream contributors

This commit changes the contributorrc for allcontributors bot so we can add both overseerr and
jellyseerr contributors to the list

* docs: update README to accomodate both upstream and downstream contributor list seperately

* docs: [skip ci] add in current project allcontributors only

As allcontributors bot does not support having two lists of allcontributors seperately,
unfortunately had to remove upstream contributors from the .all-contributorsrc. However, they will
be added manually by @Fallenbagel to the README.md

* docs: [skip ci] removed contributor block so all-contributors can handle it

* docs: update README.md

* docs: update .all-contributorsrc

* docs: update README.md

* docs: update .all-contributorsrc

* chore: [skip ci] added skipCi to all-contributorsrc temporarily

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* Add more detailed installation instructions

* Update README.md

* ci(build): implement github repository container images

fix #370

* ci(build): hard-coded repository owner name for lowercase naming

* build: revert the hardcoded tag

* ci: github repository container lowercase tag

* update .github folder templates

* docs: update README.md [skip ci]

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

* Adding Jellyfin Setting for Custom "Forgot Password" URL

Adding Jellyfin Setting for Custom "Forgot Password" URL.  Useful in cases where you are using a custom authentication provider such as the LDAP plugin, Authelia, lldap, or any other external auth scheme with its own password reset page.

* Making the new setting optional

* Fixing code formatting, prettier

* fix(watchlist): added missing prop for watchlist item removal button in watchlist page

This fix resolves a Watchlist page bug where the isAddedToWatchlist prop was missing.
Without this prop, the removal button for watchlist items was absent. In this fix, the
isAddedToWatchlist prop is re-added and set to true, allowing users to remove items from
their local watchlist directly on the Watchlist page.

* fix: ensure watchlist updates are immediately reflected

This fix addresses an issue on the Watchlist page where changes to the watchlist were not
immediately reflected. Previously, after removing an item from the watchlist, the update
required a full page reload or revalidating upon focusing the window or tab. With this fix,
the watchlist now correctly mutates and updates in real-time, providing a seamless user
experience.

* fix: correct width issue in datepicker of filterSliderOver

This commit addresses a rendering issue with the date picker component.
The problem was traced back to a misconfiguration in the tailwindcss settings, resulting in an
incorrect width for the popup.

fix #415

* refactor: jellyfin scan jobs moved from server/jobs to server/libs/scanners

* fix: disable seasonfolder option in sonarr for jellyfin/Emby users

This disables seasonfolder option in sonarr for jellyfin/emby users as physical seasonFolders are
necessary as virtualFolders are ignored since #126

fix #575

* refactor: clean out commented code

* docs: reverted two unrelated files to its develop branch state

* fix: fix german translation for "components.Discover.FilterSlideover.tmdbuservotecount"

* docs: update README.md [skip ci]

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

* Link related projects in README.md

* Add more badges and weblate status

* docs: update README.md [skip ci]

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

* update weblate link

* move weblate details to contributing.md

* add translation percentage badge

* update discord badge

* docs: fix weblate link

* feat: added Letterboxd links for the external link blocks for movies

* ci(preview): added arm support for preview tags

* docs: update README.md [skip ci]

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

* docs: update README.md [skip ci]

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

* fix(jellyfin.ts): process virtual seasons if they have non virtual episodes (#639)

All seasons are processed now, but those without any episodes are filtered out again as unavailable.
This fixes in issue where jellyfin reports all seasons as virtual

* feat(job): media availability support for jellyfin/emby (#522)

* feat(job): media availability support for jellyfin/emby

This refactors the media availability job to support jellyfin/emby for media removal automatically.
Needs further testing on 4k items (as I have not yet tested with 4k), however, non-4k items work as
intended.

fix #406, fix #193, fix #516, fix #362, fix #84

* fix(availabilitysync): use the correct 4k jellyfinMediaId

* fix: season mapping for plex

Fixes a bug introduced with this PR where media availability sync job removed the seasons from all
series even when those seasons existed

* refactor: jellyfin authentication and add gravatar for missing avatars of jellyfin users (#664)

* refactor: jellyfin authentication

This refactor standardizes the authentication approach in Jellyfin to mirror the method employed in
Plex authentication for consistency

* feat: use gravatar for jellyfin users' with missing jellyfin avatars

* Fixed a typo (#654)

Just a simple typo fix.

* docs: add trackmastersteve as a contributor for doc (#665)

* docs: update README.md [skip ci]

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

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>

* fix: typos on readme (#655)

* Fix typo

* Apply suggestions

* Apply suggestions

---------

Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com>

* fix(embyauth): remove the accidentally added mediaServerType change code from another PR (#684)

Accidentally added the mediaServerType change code from another feature branch/PR during the auth
logic refactor that broke emby logins.

* fix(jellyfinscanner): conditionally assign the jellyfinMediaId and jellyfinMediaId4k (#686)

Previously `jellyfinMediaId4k` was being assigned even if 4k server was not setup or even if 4k
content were not present. This fixes it by conditionally assigning the jellyfinMediaId and
JellyfinMediaId4k

fix #681

* feat: check if first jellyfin user is admin (#635)

* feat: merge check if first jellyfin user is admin

re #610

* refactor(i18n): extract admin error message into en locale

---------

Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>

* refactor(i18n): change the user-facing identity of the application in i18n (#703)

* fix: nullable type for jellyfinMediaId(4k) (#702)

The jellyfinMediaId(4k) properties were inferred as string | undefined, causing them to be set to
undefined when assigning null. This prevented the media from being saved correctly to the SQLite
database, as it doesn't accept undefined values. This resolves the availabilitySync job issue where
the "play on" button wasn't being removed for all media server types.

fix #668

* fix(jellyfinapi): refactors jellyfin library sync to support automatic grouping and collections (#700)

* fix(jellyfinapi): refactors jellyfin library sync to support automatic grouping and collections

Previously, #450 added support for automatic library grouping. However, some users reported that
they were getting a 401 when using custom authentication such as LDAP. Therefore, that PR was
reverted (#524). This PR adds back the support for automatic library grouping for jellyfin
authentication users using the endpoint `/Library/MediaFolders` and fallsback to User views endpoint
if they're unable to sync the libraries (some, not all LDAP users had issues. Some reported that it
worked despite having custom authentication). Once it falls back to user views endpoint for syncing,
now it will detect if automatic grouping is enabled giving a warning that its not supported when
using some custom authentication methods. This PR also fixed collection syncing by expanding the
boxsets when syncing.

fix #256, fix #489, re #450, #524, fix #515, fix #474, fix #473

* refactor(i18n): adds the suffix "jellyfin" to jellyfin library sync message keys

* refactor(i18n): extract translation keys

* refactor: remove console logs

* refactor: remove more console logs

* refactor: apply review suggestions

* chore: fix prettier failing on .github file

* feat: jellyseerr makeover (#715)

---------

Co-authored-by: Daniel Fendrich <daniel.fendrich@3-s.at>
Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Developer J <jshsakura@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: 김상구 (Studio) <spair0039@gmail.com>
Co-authored-by: 최효근 <gyrms7532@gmail.com>
Co-authored-by: BeardedWatermelon <periklis.karantonis@gmail.com>
Co-authored-by: Dzonkins <nikoladjordjevic.ns@gmail.com>
Co-authored-by: Angel <adelpozoman@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Bunduc Dragos <bunduc.dragos@gmail.com>
Co-authored-by: DragoPrime <emperordrago@gmail.com>
Co-authored-by: Kirill Zhukov <siper13@gmail.com>
Co-authored-by: Anders Ecklon <aecklon@gmail.com>
Co-authored-by: Emil Nymann <ens@hiper.dk>
Co-authored-by: ZsiGiT <zsigit@gmail.com>
Co-authored-by: Bas <bashankamp+weblate@gmail.com>
Co-authored-by: COTMO <moermantom1@gmail.com>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Mateus <mateusbernardo@protonmail.com>
Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Co-authored-by: Tijuco <sendtomy@protonmail.com>
Co-authored-by: Marek <marek@pavelka.xyz>
Co-authored-by: Smexhy <roman.bartik@icloud.com>
Co-authored-by: Fhd-pro <juve.11@msn.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Ben <ben.david.wallner@gmail.com>
Co-authored-by: Furkan Çakar <cakar.55.furkan@gmail.com>
Co-authored-by: Juli <snowjuli@protonmail.com>
Co-authored-by: Leo Schultheiss <leoschultheiss@yahoo.de>
Co-authored-by: inkarnation <94744834+inkarnation@users.noreply.github.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Topfield99 <timmiesonne@live.se>
Co-authored-by: PovilasID <povilas.sidaravicius@gmail.com>
Co-authored-by: Maite Guix <maite.guix@gmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: Bruno Ševčenko <bs3vcenk@gmail.com>
Co-authored-by: Francesco <francy.ammirati@hotmail.com>
Co-authored-by: Gian Marco Cinalli <gm.cinalli@gmail.com>
Co-authored-by: Mirco Cau <mircocau@gmail.com>
Co-authored-by: eggermn <egger.mn@gmail.com>
Co-authored-by: Jassy lin <linjiaxinme@gmail.com>
Co-authored-by: anpplex <anpplex@gmail.com>
Co-authored-by: kx <yoboy.rox@gmail.com>
Co-authored-by: lkw123 <lkw20010211@gmail.com>
Co-authored-by: Hordo <hordocast@mailo.com>
Co-authored-by: Mathieu <math_du_88@yahoo.fr>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Rémi Guerrero <remidu34070@hotmail.fr>
Co-authored-by: Sulli <susu.leduc@gmail.com>
Co-authored-by: Symness <simon@frayssines.fr>
Co-authored-by: Valentin <droidente@gmail.com>
Co-authored-by: 주서현 <adan.89lion@gmail.com>
Co-authored-by: Jean Beauchamp <jean@vwdubb.com>
Co-authored-by: Ryan Cohen <ryan@sct.dev>
Co-authored-by: Eduardo <sirmartin@gmail.com>
Co-authored-by: Rick Luiken <rick-luiken@live.nl>
Co-authored-by: Br33ce <124933490+Br33ce@users.noreply.github.com>
Co-authored-by: Brandon Cohen <brandon@z3hn.dev>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: Danshil Kokil Mungur <me@danshilm.com>
Co-authored-by: RemiRigal <rigal.remi@gmail.com>
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Athfan Khaleel <athphane@gmail.com>
Co-authored-by: Derek Paschal <dtpaschal@gmail.com>
Co-authored-by: mdll23 <m.dallinger@mailbox.org>
Co-authored-by: Janek <github@melonion.me>
Co-authored-by: Danish Humair <me@danishhumair.com>
Co-authored-by: Aleksa Siriški <31509435+aleksasiriski@users.noreply.github.com>
Co-authored-by: InvalidArgumentException <150857901+InvalidArgumentException@users.noreply.github.com>
Co-authored-by: Stephen Harris <trackmastersteve@users.noreply.github.com>
Co-authored-by: Gauvino <68083474+Gauvino@users.noreply.github.com>
2024-04-16 01:21:45 +05:00
semantic-release-bot
325e2ed6d3 chore(release): 1.7.0 2023-09-14 00:44:40 +00:00
Fallenbagel
e7c11da52b Merge pull request #477 from Fallenbagel/develop
Merge develop into main
2023-09-14 05:41:57 +05:00
semantic-release-bot
5712e19804 chore(release): 1.6.0 2023-08-04 20:43:24 +00:00
fallenbagel
4b549763e5 Merge branch 'develop' 2023-08-05 01:22:19 +05:00
semantic-release-bot
24151d27f7 chore(release): 1.5.0 2023-04-20 02:05:25 +00:00
Fallenbagel
f3cc8cba0a Merge pull request #368 from Fallenbagel/develop
Merge 'develop' into main
2023-04-20 07:02:36 +05:00
semantic-release-bot
57e7d68092 chore(release): 1.4.1 2023-01-31 00:20:50 +00:00
Fallenbagel
d3622f7bb3 Merge pull request #316 from Fallenbagel/develop
Merge develop into main
2023-01-31 05:15:41 +05:00
semantic-release-bot
20c821e2eb chore(release): 1.4.0 2023-01-29 20:33:10 +00:00
Fallenbagel
7b82ced5e6 Merge pull request #312 from Fallenbagel/develop
Merge 'origin/develop' into main
2023-01-30 01:31:00 +05:00
105 changed files with 7205 additions and 3519 deletions

View File

@@ -1,6 +1,6 @@
name: 🐛 Bug Report name: 🐛 Bug Report
description: Report a problem description: Report a problem
labels: ['type:bug', 'awaiting-triage'] labels: ['bug', 'awaiting triage']
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -1,6 +1,6 @@
name: ✨ Feature Request name: ✨ Feature Request
description: Suggest an idea description: Suggest an idea
labels: ['type:enhancement', 'awaiting-triage'] labels: ['enhancement', 'awaiting triage']
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -1,3 +1,421 @@
# [2.0.0](https://github.com/fallenbagel/jellyseerr/compare/v1.9.2...v2.0.0) (2024-10-15)
### Bug Fixes
* abort availability sync job if auth token invalid/connection lost ([#845](https://github.com/fallenbagel/jellyseerr/issues/845)) ([bdee340](https://github.com/fallenbagel/jellyseerr/commit/bdee34053080c8975a88ba16a9e8f402e10fe7e1))
* add an error message to say when an email is already taken ([#947](https://github.com/fallenbagel/jellyseerr/issues/947)) ([89e0a83](https://github.com/fallenbagel/jellyseerr/commit/89e0a831ec85a6905f539f59b7523bb1feb90bcf))
* add missing brackets ([#888](https://github.com/fallenbagel/jellyseerr/issues/888)) ([6cea8bb](https://github.com/fallenbagel/jellyseerr/commit/6cea8bba592b8db566b4d8147630385f5c377f1b))
* add missing content-type header ([#887](https://github.com/fallenbagel/jellyseerr/issues/887)) ([2be9c7d](https://github.com/fallenbagel/jellyseerr/commit/2be9c7dcc1f418726a19e99cfdb3933257a03c6f))
* add missing header when creating an issue ([#879](https://github.com/fallenbagel/jellyseerr/issues/879)) ([084e1b2](https://github.com/fallenbagel/jellyseerr/commit/084e1b224e109f0f8279741b9a5ead138396d7f8))
* add missing parameter to delete requests from ExternalAPI ([#904](https://github.com/fallenbagel/jellyseerr/issues/904)) ([36d98a2](https://github.com/fallenbagel/jellyseerr/commit/36d98a2681921a8770027b78878688f2782e8b77)), closes [#903](https://github.com/fallenbagel/jellyseerr/issues/903)
* **api:** fix nextjs error handler ([#882](https://github.com/fallenbagel/jellyseerr/issues/882)) ([0116c13](https://github.com/fallenbagel/jellyseerr/commit/0116c13e0632d1ccec43299fbb10cd71db45bc29))
* **api:** handle non-existent ratings on IMDb ([#822](https://github.com/fallenbagel/jellyseerr/issues/822)) ([74a2d25](https://github.com/fallenbagel/jellyseerr/commit/74a2d25f153b07a0cae5b44adca5fa1fed5a3b9e))
* **api:** save new password when reset password of local account ([#886](https://github.com/fallenbagel/jellyseerr/issues/886)) ([5cc4389](https://github.com/fallenbagel/jellyseerr/commit/5cc43898256b130c2576f34a3d4e7ce6a3940d3e))
* **blacklist:** add blacklist to mobile menu ([#980](https://github.com/fallenbagel/jellyseerr/issues/980)) ([f390da4](https://github.com/fallenbagel/jellyseerr/commit/f390da486625a22951956ba96867de63f73bfc2b)), closes [#979](https://github.com/fallenbagel/jellyseerr/issues/979)
* change SeriesSearch to MissingEpisodeSearch for season requests ([#711](https://github.com/fallenbagel/jellyseerr/issues/711)) ([ee7e91c](https://github.com/fallenbagel/jellyseerr/commit/ee7e91c7c948b17b556a625919eb1252a721bb6e))
* **docker:** add postinstall script ([#839](https://github.com/fallenbagel/jellyseerr/issues/839)) ([f714132](https://github.com/fallenbagel/jellyseerr/commit/f7141329094d88eb0940b1db1f21376142cb8893))
* enhance error messages when Fetch API fails ([#893](https://github.com/fallenbagel/jellyseerr/issues/893)) ([fccfca6](https://github.com/fallenbagel/jellyseerr/commit/fccfca6ed06c8dc599e1ea4b1b3dbac48eb3a7f6))
* handle status badge for season packs ([#927](https://github.com/fallenbagel/jellyseerr/issues/927)) ([80f6301](https://github.com/fallenbagel/jellyseerr/commit/80f63017ac5e9b1720a19c761dbef4dd517f1c2c))
* length of undefined on users warnings ([#875](https://github.com/fallenbagel/jellyseerr/issues/875)) ([c600566](https://github.com/fallenbagel/jellyseerr/commit/c600566ac0045c2314f9013b063007b087ee4327))
* remove DNS caching ([#837](https://github.com/fallenbagel/jellyseerr/issues/837)) ([268c7df](https://github.com/fallenbagel/jellyseerr/commit/268c7df28eea8b911d6a53297f5ce296983067ce))
* remove email requirement for the user, and use the username if no email provided ([#900](https://github.com/fallenbagel/jellyseerr/issues/900)) ([d5f817e](https://github.com/fallenbagel/jellyseerr/commit/d5f817e734131cdacc229361d9498a095af57950))
* remove protocol-relative URLs from next/image ([#889](https://github.com/fallenbagel/jellyseerr/issues/889)) ([c80d9a8](https://github.com/fallenbagel/jellyseerr/commit/c80d9a853a2a3451293a5382ef183c18add0c040))
* resize episode preview image ([#842](https://github.com/fallenbagel/jellyseerr/issues/842)) ([96ba53f](https://github.com/fallenbagel/jellyseerr/commit/96ba53fecc7b9d269f0d974051ab62836b0102bc))
* resize header image in network and studio pages ([#902](https://github.com/fallenbagel/jellyseerr/issues/902)) ([4220855](https://github.com/fallenbagel/jellyseerr/commit/422085523e5dfc132f3c3ca19eaa87117828b7be))
* rewrite request from axios to Fetch ([#920](https://github.com/fallenbagel/jellyseerr/issues/920)) ([9aee888](https://github.com/fallenbagel/jellyseerr/commit/9aee8887d3cca6e018f4be1c8400c22e86bf8dab))
* rewrite the rate limit utility ([#896](https://github.com/fallenbagel/jellyseerr/issues/896)) ([3fc14c9](https://github.com/fallenbagel/jellyseerr/commit/3fc14c9e2262463afec666e7f54e38d0d36cff68))
* **session:** set the correct TTL for the cookie store ([#992](https://github.com/fallenbagel/jellyseerr/issues/992)) ([96e1d40](https://github.com/fallenbagel/jellyseerr/commit/96e1d40304749ce00d2ff7359efc39a1d9724358)), closes [#991](https://github.com/fallenbagel/jellyseerr/issues/991)
* set correct user type when importing from emby ([#949](https://github.com/fallenbagel/jellyseerr/issues/949)) ([e57d265](https://github.com/fallenbagel/jellyseerr/commit/e57d2654d1c634a91649722d3a2bf4d73c4a02ca)), closes [#948](https://github.com/fallenbagel/jellyseerr/issues/948)
* **setup:** page display when homepage is loading ([#940](https://github.com/fallenbagel/jellyseerr/issues/940)) ([7423bbb](https://github.com/fallenbagel/jellyseerr/commit/7423bbbffc5bee2e52e3348254f035dc8527d973))
* **tmdb:** fallback movie/show overview to English when none is available in requested locale ([#928](https://github.com/fallenbagel/jellyseerr/issues/928)) ([12f908d](https://github.com/fallenbagel/jellyseerr/commit/12f908de7f5fbd717a5f151858b6edee3be13ed9)), closes [#925](https://github.com/fallenbagel/jellyseerr/issues/925)
* update the filter removing existing users from Jellyfin import modal ([#924](https://github.com/fallenbagel/jellyseerr/issues/924)) ([61dcd8e](https://github.com/fallenbagel/jellyseerr/commit/61dcd8e487d7886773ccb12501623c17838476e5))
### Code Refactoring
* **jellyfin:** abstract jellyfin hostname, updated ui to reflect it, better validation ([#773](https://github.com/fallenbagel/jellyseerr/issues/773)) ([38ad875](https://github.com/fallenbagel/jellyseerr/commit/38ad875dd7848b4e92ac3ccdd16dbf785f6a5c4d))
### Features
* add environment variable for API key ([#831](https://github.com/fallenbagel/jellyseerr/issues/831)) ([45ef150](https://github.com/fallenbagel/jellyseerr/commit/45ef150e36944d456cc9440574b5ac75f2e4bbc1))
* adds status filter for tv shows ([#796](https://github.com/fallenbagel/jellyseerr/issues/796)) ([cfd1bc2](https://github.com/fallenbagel/jellyseerr/commit/cfd1bc253557d6e19725743b8aa9a2fa33bbe760)), closes [#605](https://github.com/fallenbagel/jellyseerr/issues/605)
* allow request managers to delete data from sonarr/radarr ([#644](https://github.com/fallenbagel/jellyseerr/issues/644)) ([a5d22ba](https://github.com/fallenbagel/jellyseerr/commit/a5d22ba5b83dd0e812b16f06476d993b5d59cb2a))
* blacklist items from Discover page ([#632](https://github.com/fallenbagel/jellyseerr/issues/632)) ([818aa60](https://github.com/fallenbagel/jellyseerr/commit/818aa60aac185da07bfb71b08e0448939b63a736)), closes [#490](https://github.com/fallenbagel/jellyseerr/issues/490)
* Jellyfin/Emby server type setup ([#685](https://github.com/fallenbagel/jellyseerr/issues/685)) ([15cb949](https://github.com/fallenbagel/jellyseerr/commit/15cb949f1f2e617853f90ae7bb8ae5d6622f610e))
* **jellyfinapi:** switch to API tokens instead of auth tokens ([#868](https://github.com/fallenbagel/jellyseerr/issues/868)) ([bd4da6d](https://github.com/fallenbagel/jellyseerr/commit/bd4da6d5fc8cb55c2bc3d9a8336787cbd30814d0))
* Option on item's page to add/remove from watchlist ([#781](https://github.com/fallenbagel/jellyseerr/issues/781)) ([2348f23](https://github.com/fallenbagel/jellyseerr/commit/2348f23f433195d64dee3e6eeede296fca5fdbc9)), closes [#730](https://github.com/fallenbagel/jellyseerr/issues/730)
* refresh monitored downloads before getting queue items ([#994](https://github.com/fallenbagel/jellyseerr/issues/994)) ([92ba262](https://github.com/fallenbagel/jellyseerr/commit/92ba26207dcb1ddd696e0f01931d2609c521ae45)), closes [#866](https://github.com/fallenbagel/jellyseerr/issues/866)
* show quality profile on request ([#847](https://github.com/fallenbagel/jellyseerr/issues/847)) ([6445332](https://github.com/fallenbagel/jellyseerr/commit/64453320d36595e75dcb710dfd43997bf2d2acd5))
* **translation:** added full Hebrew translation ([#871](https://github.com/fallenbagel/jellyseerr/issues/871)) ([c96ca67](https://github.com/fallenbagel/jellyseerr/commit/c96ca6742e0a6d5685319c52f995fe06e439a450))
* update Plex logo ([#884](https://github.com/fallenbagel/jellyseerr/issues/884)) ([3a363ae](https://github.com/fallenbagel/jellyseerr/commit/3a363ae1ffa7f384be6f7d25f8558b1e55a73fb3))
### Reverts
* fix(api): fix nextjs error handler ([#882](https://github.com/fallenbagel/jellyseerr/issues/882)) ([#892](https://github.com/fallenbagel/jellyseerr/issues/892)) ([62dbde4](https://github.com/fallenbagel/jellyseerr/commit/62dbde448c7f7d530de8534bb8538452d0f91276))
### BREAKING CHANGES
* This commit deprecates the JELLYFIN_TYPE variable to identify Emby media server and
instead rely on the mediaServerType that is set in the `settings.json`. Existing environment
variable users can log out and log back in to set the mediaServerType to `3` (Emby).
* feat(api): add severType to the api
* This adds a serverType to the `/auth/jellyfin` which requires a serverType to be
set (`jellyfin`/`emby`)
* refactor: use enums for serverType and rename selectedservice to serverType
* refactor(auth): jellyfin/emby authentication to set MediaServerType
* fix: issue page formatMessage for 4k media
* refactor: cleaner way of handling serverType change using MediaServerType instead of strings
instead of using strings now it will use MediaServerType enums for serverType
* revert: removed conditional render of the auto-request permission
reverts the conditional render toshow the auto-request permission if the mediaServerType was set to
Plex as this should be handled in a different PR and Cypress tests should be modified
accordingly(currently cypress test would fail if this conditional check is there)
* feat: add server type step to setup
* feat: migrate existing emby setups to use emby mediaServerType
* fix: scan jobs not running when media server type is emby
* fix: emby media server type migration
* refactor: change emby logo to full logo
* style: decrease emby logo size in setup screen
* refactor: use title case for servertype i18n message
* refactor(i18n): fix a typo
* refactor: use enums instead of numbers
* fix: remove old references to JELLYFIN_TYPE environment variable
* fix: go back to the last step when refresh the setup page
* fix: move "scanning in background" tip next to the scanning section
* fix: redirect the setup page when Jellyseerr is already setup
* **jellyfin:** Jellyfin settings now does not include a hostname. Instead it abstracted it to ip,
port, useSsl, and urlBase. However, migration of old settings to new settings should work
automatically.
* refactor: remove console logs and use getHostname and ApiErrorCodes
* fix: store req.body jellyfin settings temporarily and store only if valid
This should fix the issue where settings are saved even if the url
was invalid. Now the settings will only be saved if the url is
valid. Sort of like a test connection.
* refactor: clean up commented out code
* refactor(i18n): extract translation keys
* fix(auth): auth failing with jellyfin login is disabled
* fix(settings): jellyfin migrations replacing the rest of the settings
* fix(settings): jellyfin hostname should be carried out if hostname exists
* fix(settings): merging the wrong settings source
* refactor(settings): use migrator for dynamic settings migrations
* refactor(settingsmigrator): settings migration handler and the migrations
* test(cypress): fix cypress tests failing
cypress settings were lacking some of the jobs so when the startJobs() is called when the app
starts, it was failing to schedule the jobs where their cron timings were not specified in the
cypress settings. Therefore, this commit adds those jobs back. In addition, other setting options
were added to keep cypress settings consistent with a normal user.
* chore(prettierignore): ignore cypress/config/settings.cypress.json as it does not need prettier
* chore(prettier): ran formatter on cypress config to fix format check error
format check locally passes on this file. However, it fails during the github actions format check.
Therefore, json language features formatter was run instead of prettier to see if that fixes the
issue.
* test(cypress): add only missing jobs to the cypress settings
* ci: attempt at trying to get formatter to pass on cypress config json file
* refactor: revert the changes brought to try and fix formatter
added back the rest of the cypress settings and removed cypress settings from .prettierignore
* refactor(settings): better erorr logging when jellyfin connection test fails in settings page
## [1.9.2](https://github.com/fallenbagel/jellyseerr/compare/v1.9.1...v1.9.2) (2024-06-13)
### Bug Fixes
* **auth:** improve login resilience with headerless fallback authentication ([#814](https://github.com/fallenbagel/jellyseerr/issues/814)) ([a9741fa](https://github.com/fallenbagel/jellyseerr/commit/a9741fa36d06710aa00d28db3dd2c29f2b0973d3))
* **auth:** validation of ipv6/ipv4 ([#812](https://github.com/fallenbagel/jellyseerr/issues/812)) ([9aeb360](https://github.com/fallenbagel/jellyseerr/commit/9aeb3604e6498c388df1d30dd0b613ba84160fc0)), closes [#795](https://github.com/fallenbagel/jellyseerr/issues/795)
* bypass cache-able lookups when resolving localhost ([#813](https://github.com/fallenbagel/jellyseerr/issues/813)) ([b5a0699](https://github.com/fallenbagel/jellyseerr/commit/b5a069901a9545772deaa9c491f2075261da0189))
## [1.9.1](https://github.com/fallenbagel/jellyseerr/compare/v1.9.0...v1.9.1) (2024-06-12)
### Bug Fixes
* **api:** add DNS caching ([#810](https://github.com/fallenbagel/jellyseerr/issues/810)) ([46ee8a4](https://github.com/fallenbagel/jellyseerr/commit/46ee8a4ca13b026bd929b4027eb001cc74064bb8)), closes [#387](https://github.com/fallenbagel/jellyseerr/issues/387) [#657](https://github.com/fallenbagel/jellyseerr/issues/657) [#728](https://github.com/fallenbagel/jellyseerr/issues/728)
* empty email in user settings ([#807](https://github.com/fallenbagel/jellyseerr/issues/807)) ([20863d4](https://github.com/fallenbagel/jellyseerr/commit/20863d4a8dabe78fb5c52995b5bcb2da557a804e)), closes [#803](https://github.com/fallenbagel/jellyseerr/issues/803)
* **jellyfinscanner:** assign only 4k available badge for a 4k request instead of both badges ([#805](https://github.com/fallenbagel/jellyseerr/issues/805)) ([d31a2c3](https://github.com/fallenbagel/jellyseerr/commit/d31a2c37e639c1126b446277fa5d666d8102fef5))
* remove the settings button of media when useless ([#809](https://github.com/fallenbagel/jellyseerr/issues/809)) ([f52939e](https://github.com/fallenbagel/jellyseerr/commit/f52939e4cdcbee94fc35165f613f6b3e21599e3c))
### Reverts
* Revert "ci: update format check command to ignore .prettierignore files (#787)" (#788) ([4757f1c](https://github.com/fallenbagel/jellyseerr/commit/4757f1c3e599304410a737c11f97db92a2bfcefd)), closes [#787](https://github.com/fallenbagel/jellyseerr/issues/787) [#788](https://github.com/fallenbagel/jellyseerr/issues/788)
# [1.9.0](https://github.com/fallenbagel/jellyseerr/compare/v1.8.1...v1.9.0) (2024-05-29)
### Bug Fixes
* **api:** save user email on the first try ([#760](https://github.com/fallenbagel/jellyseerr/issues/760)) ([0bbcfdc](https://github.com/fallenbagel/jellyseerr/commit/0bbcfdc4f9ff9735f45232a2412ac8444f525de9)), closes [#227](https://github.com/fallenbagel/jellyseerr/issues/227) [#748](https://github.com/fallenbagel/jellyseerr/issues/748)
* **api:** small errors on overseerr-api.yaml ([#721](https://github.com/fallenbagel/jellyseerr/issues/721)) ([0eea109](https://github.com/fallenbagel/jellyseerr/commit/0eea1090dfdba4333646280c84b09b0197fefa74))
* **auth:** case-sensitive logins not updating authtokens ([#778](https://github.com/fallenbagel/jellyseerr/issues/778)) ([2bd125d](https://github.com/fallenbagel/jellyseerr/commit/2bd125d9a55d15a398ceb5f2996105a5e861b6e0))
* **jellyfinapi:** use external api class for jellyfin api requests ([#762](https://github.com/fallenbagel/jellyseerr/issues/762)) ([650c339](https://github.com/fallenbagel/jellyseerr/commit/650c339d74d4fe85ef7f76184901e86f4eeada85)), closes [#728](https://github.com/fallenbagel/jellyseerr/issues/728) [#387](https://github.com/fallenbagel/jellyseerr/issues/387)
* **logging:** handle media server connection refused error/toast ([#748](https://github.com/fallenbagel/jellyseerr/issues/748)) ([f486fb5](https://github.com/fallenbagel/jellyseerr/commit/f486fb5e75f9ea21456952b6a52cb841e30f3556))
* use UTF8 encoding for webhook JSON ([#714](https://github.com/fallenbagel/jellyseerr/issues/714)) ([c0a0b9c](https://github.com/fallenbagel/jellyseerr/commit/c0a0b9c8a8b0c2eeaf3fa9159f10742baa9f6c1f))
### Features
* add Latin American Spanish translation ([#725](https://github.com/fallenbagel/jellyseerr/issues/725)) ([783fda9](https://github.com/fallenbagel/jellyseerr/commit/783fda9621aef8ffd46e5f036136de82ed502ccc)), closes [#677](https://github.com/fallenbagel/jellyseerr/issues/677)
* add merge conflict labeler workflow ([#719](https://github.com/fallenbagel/jellyseerr/issues/719)) ([d9d07c7](https://github.com/fallenbagel/jellyseerr/commit/d9d07c705a24d5c49905066aac45a3c6a2e36a53))
* **auth:** send real information on login ([#470](https://github.com/fallenbagel/jellyseerr/issues/470)) ([d765055](https://github.com/fallenbagel/jellyseerr/commit/d765055da83ee94546399f6348aee14d8427d462))
* **settings:** stores jellyfin/emby server name in the settings ([#763](https://github.com/fallenbagel/jellyseerr/issues/763)) ([7a5e8d6](https://github.com/fallenbagel/jellyseerr/commit/7a5e8d69bf620c8e7bf5f284840b1a5fe757ae5f))
## [1.8.1](https://github.com/fallenbagel/jellyseerr/compare/v1.8.0...v1.8.1) (2024-04-17)
### Reverts
* Revert "fix: disable seasonfolder option in sonarr for jellyfin/Emby users" (#718) ([cd0fa3e](https://github.com/fallenbagel/jellyseerr/commit/cd0fa3e2232dcb522673143f113fc382fb2ff0a3)), closes [#718](https://github.com/fallenbagel/jellyseerr/issues/718)
# [1.8.0](https://github.com/fallenbagel/jellyseerr/compare/v1.7.0...v1.8.0) (2024-04-15)
### Bug Fixes
* correct width issue in datepicker of filterSliderOver ([f564cdd](https://github.com/fallenbagel/jellyseerr/commit/f564cddff4525ccebffbf304672d49c57aefe635)), closes [#415](https://github.com/fallenbagel/jellyseerr/issues/415)
* disable seasonfolder option in sonarr for jellyfin/Emby users ([8ec8f2a](https://github.com/fallenbagel/jellyseerr/commit/8ec8f2ac5730aad3b12dcd8ed95bb553b46b399c)), closes [#126](https://github.com/fallenbagel/jellyseerr/issues/126) [#575](https://github.com/fallenbagel/jellyseerr/issues/575)
* **embyauth:** remove the accidentally added mediaServerType change code from another PR ([#684](https://github.com/fallenbagel/jellyseerr/issues/684)) ([c2e8771](https://github.com/fallenbagel/jellyseerr/commit/c2e87714b4c4aa11bf68dcd82b76979f82990f3c))
* ensure watchlist updates are immediately reflected ([b85d7f3](https://github.com/fallenbagel/jellyseerr/commit/b85d7f37b931735ca2ad955dccb6599bf445fc73))
* fix german translation for "components.Discover.FilterSlideover.tmdbuservotecount" ([e032c02](https://github.com/fallenbagel/jellyseerr/commit/e032c02f5f84dc4b6b470eecb18ba2c376c55f37))
* fix the translations for watchlist permissions and userSettings page ([8c82a61](https://github.com/fallenbagel/jellyseerr/commit/8c82a61450a7525c0e2f1b64e6939da47a7c715d))
* **i18n:** fixed jellyfin jobs ([7eed236](https://github.com/fallenbagel/jellyseerr/commit/7eed23637ddfb10bdcb19698e7ae171f07299502))
* **jellyfin.ts:** process virtual seasons if they have non virtual episodes ([#639](https://github.com/fallenbagel/jellyseerr/issues/639)) ([db84f65](https://github.com/fallenbagel/jellyseerr/commit/db84f6529ab285be26c96daaab065dfabf347417))
* **jellyfinapi:** refactors jellyfin library sync to support automatic grouping and collections ([#700](https://github.com/fallenbagel/jellyseerr/issues/700)) ([3856061](https://github.com/fallenbagel/jellyseerr/commit/3856061fe1ee4d3457996586b4979ad9dd60765a)), closes [#450](https://github.com/fallenbagel/jellyseerr/issues/450) [#524](https://github.com/fallenbagel/jellyseerr/issues/524) [#256](https://github.com/fallenbagel/jellyseerr/issues/256) [#489](https://github.com/fallenbagel/jellyseerr/issues/489) [#450](https://github.com/fallenbagel/jellyseerr/issues/450) [#524](https://github.com/fallenbagel/jellyseerr/issues/524) [#515](https://github.com/fallenbagel/jellyseerr/issues/515) [#474](https://github.com/fallenbagel/jellyseerr/issues/474) [#473](https://github.com/fallenbagel/jellyseerr/issues/473)
* **jellyfinlogin:** use externalHostname if set for forgetpassword link ([405f6bb](https://github.com/fallenbagel/jellyseerr/commit/405f6bbb7ffc390327c99dcef2cbbf9b3bc75f01)), closes [#199](https://github.com/fallenbagel/jellyseerr/issues/199) [#424](https://github.com/fallenbagel/jellyseerr/issues/424) [#212](https://github.com/fallenbagel/jellyseerr/issues/212)
* **jellyfinscanner:** conditionally assign the jellyfinMediaId and jellyfinMediaId4k ([#686](https://github.com/fallenbagel/jellyseerr/issues/686)) ([530be42](https://github.com/fallenbagel/jellyseerr/commit/530be4272cce1b0d74d7f4156b8d794cda6ea03f)), closes [#681](https://github.com/fallenbagel/jellyseerr/issues/681)
* **langcode:** fixes the ukranian language code ([dc67aaa](https://github.com/fallenbagel/jellyseerr/commit/dc67aaaf53eae86ba20c6c2798c92ec40962d85f)), closes [#504](https://github.com/fallenbagel/jellyseerr/issues/504)
* nullable type for jellyfinMediaId(4k) ([#702](https://github.com/fallenbagel/jellyseerr/issues/702)) ([0900a95](https://github.com/fallenbagel/jellyseerr/commit/0900a95532501b6f4d9698de7530a771512924fc)), closes [#668](https://github.com/fallenbagel/jellyseerr/issues/668)
* request watchlist items sequentially to prevent bypassing quota ([#3667](https://github.com/fallenbagel/jellyseerr/issues/3667)) ([b40ba07](https://github.com/fallenbagel/jellyseerr/commit/b40ba07a4de5857b8392f667038eeb0b22aa5d9a))
* resolved issue with region selector and all regions value ([#3652](https://github.com/fallenbagel/jellyseerr/issues/3652)) ([28a2c50](https://github.com/fallenbagel/jellyseerr/commit/28a2c50495d0ce531da7f8c442bd488a54b1e84c))
* typos on readme ([#655](https://github.com/fallenbagel/jellyseerr/issues/655)) ([eee9a02](https://github.com/fallenbagel/jellyseerr/commit/eee9a025d246c72bcd3aca753d9e49c1f8f064ea))
* **watchlist:** added missing prop for watchlist item removal button in watchlist page ([a0ec992](https://github.com/fallenbagel/jellyseerr/commit/a0ec992028093257e9fa043622e236014f02dea3))
* **watchlist:** discover local watchlist item display and profile local watchlist slider visibility ([3cb9494](https://github.com/fallenbagel/jellyseerr/commit/3cb9494e6210151716587d8c4b22e0a21692cf88))
### Features
* add ko language ([#3619](https://github.com/fallenbagel/jellyseerr/issues/3619)) ([9250735](https://github.com/fallenbagel/jellyseerr/commit/92507359b48db08b0066047d6505660b8c8b0b12))
* add Peacock to Network Slider ([#3545](https://github.com/fallenbagel/jellyseerr/issues/3545)) ([0c39057](https://github.com/fallenbagel/jellyseerr/commit/0c39057ca58743697e9dcc3b678440ac3688c65a))
* add tooltips to tautulli avatars ([#3601](https://github.com/fallenbagel/jellyseerr/issues/3601)) ([c484810](https://github.com/fallenbagel/jellyseerr/commit/c484810f965f8d04643c25c6d283dd83f4bd4a23))
* added Letterboxd links for the external link blocks for movies ([981f5e6](https://github.com/fallenbagel/jellyseerr/commit/981f5e679c4c707e119741240a58de8bb07f9d6c))
* check if first jellyfin user is admin ([#635](https://github.com/fallenbagel/jellyseerr/issues/635)) ([010df62](https://github.com/fallenbagel/jellyseerr/commit/010df62776191fe4c195e590df338f8d8523f55b)), closes [#610](https://github.com/fallenbagel/jellyseerr/issues/610)
* jellyseerr makeover ([#715](https://github.com/fallenbagel/jellyseerr/issues/715)) ([0c27132](https://github.com/fallenbagel/jellyseerr/commit/0c2713213c56de342f76300d12ce01fd543d2ce3))
* **job:** media availability support for jellyfin/emby ([#522](https://github.com/fallenbagel/jellyseerr/issues/522)) ([3eb1bb3](https://github.com/fallenbagel/jellyseerr/commit/3eb1bb3d8ff22391acb2e629bbec7b6e4b65ca95)), closes [#406](https://github.com/fallenbagel/jellyseerr/issues/406) [#193](https://github.com/fallenbagel/jellyseerr/issues/193) [#516](https://github.com/fallenbagel/jellyseerr/issues/516) [#362](https://github.com/fallenbagel/jellyseerr/issues/362) [#84](https://github.com/fallenbagel/jellyseerr/issues/84)
* **notif:** add Pushover sound options ([#2403](https://github.com/fallenbagel/jellyseerr/issues/2403)) ([3ea5076](https://github.com/fallenbagel/jellyseerr/commit/3ea5076053359b518b1b4d537e7b61580d9275a3))
* select default seriesType for anime ([#3627](https://github.com/fallenbagel/jellyseerr/issues/3627)) ([f628635](https://github.com/fallenbagel/jellyseerr/commit/f6286359cfd2ed93fc692aa2efda37310e02c11c)), closes [#3626](https://github.com/fallenbagel/jellyseerr/issues/3626)
* standard series type selector ([#3628](https://github.com/fallenbagel/jellyseerr/issues/3628)) ([7bdd25e](https://github.com/fallenbagel/jellyseerr/commit/7bdd25e5a45843a3e530d3fa2b0887664b53eec8))
* translations update from Hosted Weblate ([#3258](https://github.com/fallenbagel/jellyseerr/issues/3258)) ([e62a078](https://github.com/fallenbagel/jellyseerr/commit/e62a078298ced7dec627fb3ff9fc8f99a39d5e1b))
* update SameSite policy of session cookie to Lax ([#3650](https://github.com/fallenbagel/jellyseerr/issues/3650)) ([c84ca43](https://github.com/fallenbagel/jellyseerr/commit/c84ca4307465af4278f3dad5cf9c2b8cbae3fada))
### Reverts
* **jellyfinapi:** reverts [#450](https://github.com/fallenbagel/jellyseerr/issues/450) as it broke library sync support for local accounts using LDAP ([b5acc09](https://github.com/fallenbagel/jellyseerr/commit/b5acc09ba98e2dd9b61e6b78721e4dd9f42a996c)), closes [#489](https://github.com/fallenbagel/jellyseerr/issues/489)
# [1.7.0](https://github.com/fallenbagel/jellyseerr/compare/v1.6.0...v1.7.0) (2023-09-14)
### Bug Fixes
* adjust the plex watchlist sync schedule to have fuzziness ([#3502](https://github.com/fallenbagel/jellyseerr/issues/3502)) ([2c3f533](https://github.com/fallenbagel/jellyseerr/commit/2c3f5330764492e1323afd2d1f25e28ad78a2f2f))
* handle issue causing incorrect media to change to unknown ([#3516](https://github.com/fallenbagel/jellyseerr/issues/3516)) ([83b008c](https://github.com/fallenbagel/jellyseerr/commit/83b008c8391459bd02dc74bcdb0d8caf27207bdf))
* improved handling of edge case that could cause availability sync to fail ([#3497](https://github.com/fallenbagel/jellyseerr/issues/3497)) ([d0836ce](https://github.com/fallenbagel/jellyseerr/commit/d0836ce0efd55fccf2546087a0c4f94f7cb2e82a))
* Include all defaults in payload ([#3538](https://github.com/fallenbagel/jellyseerr/issues/3538)) ([cb63bf2](https://github.com/fallenbagel/jellyseerr/commit/cb63bf217b9e8810a5210b4bf475b2a96583cc84))
* multiple notifications for available media ([048fa96](https://github.com/fallenbagel/jellyseerr/commit/048fa967f2e5b23831ac9917c703934c50ef75f0))
* repeat notifications for available 4k media ([30361f2](https://github.com/fallenbagel/jellyseerr/commit/30361f2ab751d9a882a9120e0f3df28dc42cc2cd))
* resolved issue with create slider causing incorrect form submission ([#3514](https://github.com/fallenbagel/jellyseerr/issues/3514)) ([a761b7d](https://github.com/fallenbagel/jellyseerr/commit/a761b7dd35a5bd61bb4eb0275b75d1e0977e6a2d))
* resolved user access check issue ([#3551](https://github.com/fallenbagel/jellyseerr/issues/3551)) ([2816c66](https://github.com/fallenbagel/jellyseerr/commit/2816c66300bf870d493c0665b0e984d60f707dfd))
* **server/api/jellyfin.ts:** use /Library/VirtualFolders Jellyfin API call to fetch Jellyfin libs ([8685f57](https://github.com/fallenbagel/jellyseerr/commit/8685f5796a99d9700146bae9892319db10508d68)), closes [#256](https://github.com/fallenbagel/jellyseerr/issues/256)
* **statusbadge:** handle missing season/episode number ([#3526](https://github.com/fallenbagel/jellyseerr/issues/3526)) ([01de972](https://github.com/fallenbagel/jellyseerr/commit/01de972a8fe2ea3c18d5b2f426d01b5b14d142d4))
* **tautulli:** only test connection if hostname is defined ([#3573](https://github.com/fallenbagel/jellyseerr/issues/3573)) ([f7b4dfc](https://github.com/fallenbagel/jellyseerr/commit/f7b4dfcac472d08c54779a14fc1ad3c90927df26))
* **ui:** corrected issues icon color ([#3498](https://github.com/fallenbagel/jellyseerr/issues/3498)) ([c1a47bd](https://github.com/fallenbagel/jellyseerr/commit/c1a47bd9de332cb4925974690f5a33448b5cc2e6))
### Features
* **rating:** added IMDB Radarr proxy ([#3496](https://github.com/fallenbagel/jellyseerr/issues/3496)) ([b4191f9](https://github.com/fallenbagel/jellyseerr/commit/b4191f9c65b7ff08764e61d18e7a75bc8d4b3325))
# [1.6.0](https://github.com/fallenbagel/jellyseerr/compare/v1.5.0...v1.6.0) (2023-08-04)
### Bug Fixes
* availability sync file detection ([#3371](https://github.com/fallenbagel/jellyseerr/issues/3371)) ([7522aa3](https://github.com/fallenbagel/jellyseerr/commit/7522aa31743b169c903ebdf9d4d698645d27514c))
* corrected initial fallback data load on details page ([#3395](https://github.com/fallenbagel/jellyseerr/issues/3395)) ([4bd8764](https://github.com/fallenbagel/jellyseerr/commit/4bd87647d0551c20e13589a62690a6f3e5ad8ff7))
* correctly load series fallback modal with sonarr v4 ([#3451](https://github.com/fallenbagel/jellyseerr/issues/3451)) ([e051b1d](https://github.com/fallenbagel/jellyseerr/commit/e051b1dfea9c9320cc9dd420c475ae74cff0d901))
* **deps:** update all non-major dependencies ([#3223](https://github.com/fallenbagel/jellyseerr/issues/3223)) ([f5191ad](https://github.com/fallenbagel/jellyseerr/commit/f5191aded680357522a65bbdcc40d162b8fbf594))
* error deleting users with over 1000 requests ([#3376](https://github.com/fallenbagel/jellyseerr/issues/3376)) ([ac77b03](https://github.com/fallenbagel/jellyseerr/commit/ac77b037d5fb0c54f5edf4b29d04adb57aef388f))
* external url regex is now consistent with internal url ([33ec443](https://github.com/fallenbagel/jellyseerr/commit/33ec4436fb82e1eb1bc97dd650088c27785e9d94))
* externalLinkBlock ([46cd4d0](https://github.com/fallenbagel/jellyseerr/commit/46cd4d01d9a3cf17d79350c5e678202820272299))
* fix regex for internal url to use a more effecient one ([e848386](https://github.com/fallenbagel/jellyseerr/commit/e848386d10f05f157e7a6dde8847ecab50c169ac))
* fixes RT ratings for tv shows ([#3492](https://github.com/fallenbagel/jellyseerr/issues/3492)) ([04fbd00](https://github.com/fallenbagel/jellyseerr/commit/04fbd00d4ac29045592588ef8b664d1916991e37)), closes [#3491](https://github.com/fallenbagel/jellyseerr/issues/3491)
* **genreselector:** fix searching in Genre filter ([#3468](https://github.com/fallenbagel/jellyseerr/issues/3468)) ([d7fa35e](https://github.com/fallenbagel/jellyseerr/commit/d7fa35e066cf371797aaa46ca464aa531ba8fb35))
* handle search results with collections ([#3393](https://github.com/fallenbagel/jellyseerr/issues/3393)) ([70b1540](https://github.com/fallenbagel/jellyseerr/commit/70b1540ae23e83e01013856a9e06ad39e600922d))
* lock body scroll when using webkit ([#3399](https://github.com/fallenbagel/jellyseerr/issues/3399)) ([c27f960](https://github.com/fallenbagel/jellyseerr/commit/c27f96096ac8cc6c387f9d1dde5b263576ac2132))
* **logs:** jellyfin auth error now has the severity warn consistent with local login ([cc041b5](https://github.com/fallenbagel/jellyseerr/commit/cc041b5e0aa2b67573edba5919772b77a5111162)), closes [#224](https://github.com/fallenbagel/jellyseerr/issues/224)
* make a (shallow) copy of radarr/sonarr tags into a request before adding user tags ([#3485](https://github.com/fallenbagel/jellyseerr/issues/3485)) ([48f7666](https://github.com/fallenbagel/jellyseerr/commit/48f76662d5c08156f1da3f47e216c5f02668f64b))
* **ui:** corrected default badge hover opacity ([#3369](https://github.com/fallenbagel/jellyseerr/issues/3369)) ([a4d07f5](https://github.com/fallenbagel/jellyseerr/commit/a4d07f5afab613317d96c9c6e9b47157a5a28986))
* **ui:** corrected mobile menu spacing in collection details ([#3432](https://github.com/fallenbagel/jellyseerr/issues/3432)) ([77a33cb](https://github.com/fallenbagel/jellyseerr/commit/77a33cb74d744bb747b791785799b632af8c7862))
* **ui:** Make play symbol white ([1fe4bb8](https://github.com/fallenbagel/jellyseerr/commit/1fe4bb8a0415a72791ced75a2fba1027287398d5))
* **ui:** Resize Emby icon and add margins ([ad69d67](https://github.com/fallenbagel/jellyseerr/commit/ad69d6715e976630092bfbbb1843886523551014))
* **watchlist:** add validation for creation request ([03316c6](https://github.com/fallenbagel/jellyseerr/commit/03316c642d1ecf89753789af08caf6e3aac80113))
* **watchlist:** fix github code scanning ([c08897b](https://github.com/fallenbagel/jellyseerr/commit/c08897bdc1cff65862c62347572bbbd01b6c36ac))
### Features
* **add watchlist:** adding midding functionality from overserr ([5f1c10d](https://github.com/fallenbagel/jellyseerr/commit/5f1c10d50aaa430bcda96218ef2cc12a0eb926f3))
* adds streaming services custom slider ([#3361](https://github.com/fallenbagel/jellyseerr/issues/3361)) ([2520d8f](https://github.com/fallenbagel/jellyseerr/commit/2520d8f739abfde608f3ef66a9fbe6b7b5c6647a))
* auto tagging requested media with username ([#3338](https://github.com/fallenbagel/jellyseerr/issues/3338)) ([24f268b](https://github.com/fallenbagel/jellyseerr/commit/24f268b6cb67d9a8d8675cd6e09dd83a7f499add))
* **discover:** support filtering by tmdb user vote count on discover page ([#3407](https://github.com/fallenbagel/jellyseerr/issues/3407)) ([aa84977](https://github.com/fallenbagel/jellyseerr/commit/aa849776809dfe891e67ff4db6861ef44df1a774))
* **settings:** add internal url to jellyfin settings form ([0a30cd3](https://github.com/fallenbagel/jellyseerr/commit/0a30cd356d217a39546c016cc8bfa6ff6ad75e3e)), closes [#194](https://github.com/fallenbagel/jellyseerr/issues/194)
* **src/components/externallinkblock/index.tsx:** support Emby icon ([672061c](https://github.com/fallenbagel/jellyseerr/commit/672061cd646c97c9954790c8e50eac88ea2666e9))
* **tooltip:** email tooltip now appears when hovered over info icon ([cd7930e](https://github.com/fallenbagel/jellyseerr/commit/cd7930eef98451a781e5c9dc5ec223600a379f42))
* translations update ([47287c3](https://github.com/fallenbagel/jellyseerr/commit/47287c368885d14bd1a56e3e8318ce22dd0f6ddf)), closes [#381](https://github.com/fallenbagel/jellyseerr/issues/381)
* **watchlist:** add translation for en ([b7e3d28](https://github.com/fallenbagel/jellyseerr/commit/b7e3d285ed35b623062eceb0d99035cafbf075a6))
# [1.5.0](https://github.com/fallenbagel/jellyseerr/compare/v1.4.1...v1.5.0) (2023-04-20)
### Bug Fixes
* add better checks on 4k detection of series ([bc9017f](https://github.com/fallenbagel/jellyseerr/commit/bc9017f54d84ec24c4d74d38e1b4e24219425d41))
* added a refresh interval if download status is in progress ([#3275](https://github.com/fallenbagel/jellyseerr/issues/3275)) ([1e2c6f4](https://github.com/fallenbagel/jellyseerr/commit/1e2c6f46ab66c836f321b5d8e34f1e8124c0b542))
* **build:** increase threshold for amount of data to be fetched when SSR'ing ([#3320](https://github.com/fallenbagel/jellyseerr/issues/3320)) ([d7b83d2](https://github.com/fallenbagel/jellyseerr/commit/d7b83d22cee3d20db564cc0564d42802b02327e3))
* disable availability sync temporarily ([2e5cf22](https://github.com/fallenbagel/jellyseerr/commit/2e5cf226265686012329248e7f729fec324c3deb))
* hide remove button when default service is not configured ([7d4455b](https://github.com/fallenbagel/jellyseerr/commit/7d4455ba6bfd12e2730f7085cbb87df246f01d22))
* **jellyfin scan:** temporary workaround fix for jellyfin scan when display specials within season ([38fb66d](https://github.com/fallenbagel/jellyseerr/commit/38fb66d31e41232c01898d0d362af8338eb7b960)), closes [#215](https://github.com/fallenbagel/jellyseerr/issues/215) [#176](https://github.com/fallenbagel/jellyseerr/issues/176) [#246](https://github.com/fallenbagel/jellyseerr/issues/246)
* lint issues ([bcd2bb7](https://github.com/fallenbagel/jellyseerr/commit/bcd2bb7c96810f5a6932f42468a628d2db1bc771))
* logger was set to info for the wrong logs ([#3354](https://github.com/fallenbagel/jellyseerr/issues/3354)) ([c36a4ba](https://github.com/fallenbagel/jellyseerr/commit/c36a4ba2b8df05873f5dfd0946a9bc3dc4ecfd1d))
* remove unnecessary parenthesis from api key generation ([#3336](https://github.com/fallenbagel/jellyseerr/issues/3336)) ([6bd3f01](https://github.com/fallenbagel/jellyseerr/commit/6bd3f015d65507efca60279007bd2b86ee860643))
* **snapcraft:** use the correct config folder for image cache ([#3302](https://github.com/fallenbagel/jellyseerr/issues/3302)) ([c93467b](https://github.com/fallenbagel/jellyseerr/commit/c93467b3acf2c256324297e7e8f21e9944005dd4))
* **ui:** hide mini status badge if non-4K media status is unknown ([#3346](https://github.com/fallenbagel/jellyseerr/issues/3346)) ([50f06da](https://github.com/fallenbagel/jellyseerr/commit/50f06dabbffc693f0843584a64d1d96e77982820))
* **ui:** hide search bar behind slideover when opened ([#3348](https://github.com/fallenbagel/jellyseerr/issues/3348)) ([b3882de](https://github.com/fallenbagel/jellyseerr/commit/b3882de8930a70adb2f93a27be6370bfa1826587))
* **ui:** prevent title cards from flickering when quickly hovering across them ([#3349](https://github.com/fallenbagel/jellyseerr/issues/3349)) ([eb5502a](https://github.com/fallenbagel/jellyseerr/commit/eb5502a16f86e37a933f6beca0678c2d228e77d5))
* **watchlist:** correctly load more than 20 watchlist items ([#3351](https://github.com/fallenbagel/jellyseerr/issues/3351)) ([af880a6](https://github.com/fallenbagel/jellyseerr/commit/af880a6c839794b34bddcd7e0fe56353aa48ba36))
### Features
* add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr ([2e74584](https://github.com/fallenbagel/jellyseerr/commit/2e7458457e995dd3ec6dd96035fe997646cdd446))
* availability sync rework ([#3219](https://github.com/fallenbagel/jellyseerr/issues/3219)) ([ae38183](https://github.com/fallenbagel/jellyseerr/commit/ae3818304b2f75222d1bd223ece94f829a3b42d0)), closes [#377](https://github.com/fallenbagel/jellyseerr/issues/377)
* full title of download item on hover with tooltip ([#3296](https://github.com/fallenbagel/jellyseerr/issues/3296)) ([33e7691](https://github.com/fallenbagel/jellyseerr/commit/33e7691b94d7d369a0a1410e434850bc51e5572e))
### Performance Improvements
* **imageproxy:** do not set cookies to image proxy so CDNs can cache images ([#3332](https://github.com/fallenbagel/jellyseerr/issues/3332)) ([966639d](https://github.com/fallenbagel/jellyseerr/commit/966639df430d32f6bfebdb16314dc4590d21caf8))
## [1.4.1](https://github.com/fallenbagel/jellyseerr/compare/v1.4.0...v1.4.1) (2023-01-31)
### Bug Fixes
* pass in library type when scanning recently added items ([#3287](https://github.com/fallenbagel/jellyseerr/issues/3287)) ([8942eb8](https://github.com/fallenbagel/jellyseerr/commit/8942eb8b7c4fa1d16aa2e72e8ba7120a653c9aa2))
* **ui:** air date will use UTC for timezone ([#3297](https://github.com/fallenbagel/jellyseerr/issues/3297)) ([3e43586](https://github.com/fallenbagel/jellyseerr/commit/3e43586acc0804c3fff524509caa890a104e132b))
* **ui:** correct range slider styling in chrome ([#3299](https://github.com/fallenbagel/jellyseerr/issues/3299)) ([d954328](https://github.com/fallenbagel/jellyseerr/commit/d9543289111d72245564d25d300a71b0ea3954ba))
* **ui:** show 5 icons when possible on mobile menu ([#3298](https://github.com/fallenbagel/jellyseerr/issues/3298)) ([7040da1](https://github.com/fallenbagel/jellyseerr/commit/7040da1334f6d18e19a494c73caa17f7df552dfe))
* **ui:** style range thumbs correctly for firefox ([#3294](https://github.com/fallenbagel/jellyseerr/issues/3294)) ([9d10e6a](https://github.com/fallenbagel/jellyseerr/commit/9d10e6a88c0996671f1d9d20792e1930dbc82329))
# [1.4.0](https://github.com/fallenbagel/jellyseerr/compare/v1.3.0...v1.4.0) (2023-01-29)
### Bug Fixes
* add bg-opacity to in-progress status badges ([#3190](https://github.com/fallenbagel/jellyseerr/issues/3190)) ([68223f4](https://github.com/fallenbagel/jellyseerr/commit/68223f4b1e98b01825516dcba39cbb2d3df31a70))
* added download status and title to request card/item error components ([#3186](https://github.com/fallenbagel/jellyseerr/issues/3186)) ([3309f77](https://github.com/fallenbagel/jellyseerr/commit/3309f77aa4be1d70b27693531c119a8e26822518))
* arrow icons were misplaced on mobile in slider edit ([#3260](https://github.com/fallenbagel/jellyseerr/issues/3260)) ([d328485](https://github.com/fallenbagel/jellyseerr/commit/d328485161b9cae6a70ef0713b4878207bc6015e))
* **build:** update usage of publish snap action ([#3272](https://github.com/fallenbagel/jellyseerr/issues/3272)) ([51b05cd](https://github.com/fallenbagel/jellyseerr/commit/51b05cd8fbb5d332807d8c00b2ffb7b10c3d0179))
* changed overflow scroll to only if necessary ([#3184](https://github.com/fallenbagel/jellyseerr/issues/3184)) ([27feeea](https://github.com/fallenbagel/jellyseerr/commit/27feeea69121336557deda1f32b65a5daa146f82))
* convert genre/studio to string in create slider ([#3201](https://github.com/fallenbagel/jellyseerr/issues/3201)) ([93afead](https://github.com/fallenbagel/jellyseerr/commit/93afead92e497f2e5bce67a34fffdaa08d20c7f2))
* correct checkbox position (again) for slider edits ([#3227](https://github.com/fallenbagel/jellyseerr/issues/3227)) ([3ba6df1](https://github.com/fallenbagel/jellyseerr/commit/3ba6df1a41c084c4a6a90354338047623abef521))
* correct grid sizing for webkit on streaming services ([#3248](https://github.com/fallenbagel/jellyseerr/issues/3248)) ([6fd11cf](https://github.com/fallenbagel/jellyseerr/commit/6fd11cf4254e1a19310592bec78a6de52bc073a8))
* correct issue detail bottom padding on mobile displays ([#3268](https://github.com/fallenbagel/jellyseerr/issues/3268)) ([3db010b](https://github.com/fallenbagel/jellyseerr/commit/3db010b9eaec62aa08d973a61caf1801471bbf3e))
* correct link to correct keyword results for series ([#3208](https://github.com/fallenbagel/jellyseerr/issues/3208)) ([4e9be7a](https://github.com/fallenbagel/jellyseerr/commit/4e9be7a3f7304ee7be5ee6fd34b1ea8f6c0cf399))
* correct spacing between sliders ([#3225](https://github.com/fallenbagel/jellyseerr/issues/3225)) ([62e2de7](https://github.com/fallenbagel/jellyseerr/commit/62e2de70bf37b72d5f63370b662d4103a642775b))
* correctly check mobile menu permissions ([#3271](https://github.com/fallenbagel/jellyseerr/issues/3271)) ([f4a22dc](https://github.com/fallenbagel/jellyseerr/commit/f4a22dc437404558f301ccfc195cf0a300dd1ff2))
* correctly restore selected streaming service filters ([#3249](https://github.com/fallenbagel/jellyseerr/issues/3249)) ([154f3e7](https://github.com/fallenbagel/jellyseerr/commit/154f3e72efbf0b663358b3029156f54516f01a2f))
* create shared class to add bottom spacing ([#3269](https://github.com/fallenbagel/jellyseerr/issues/3269)) ([5d1c6f7](https://github.com/fallenbagel/jellyseerr/commit/5d1c6f706555613d97ed9e61d8b665543c2f239b))
* **deps:** pin dependency @headlessui/react to 1.7.7 ([#3194](https://github.com/fallenbagel/jellyseerr/issues/3194)) [skip ci] ([c4b16ab](https://github.com/fallenbagel/jellyseerr/commit/c4b16abc62647c74215155942a4230a31a238677))
* **deps:** update dependency @heroicons/react to v2 ([#2970](https://github.com/fallenbagel/jellyseerr/issues/2970)) ([dd48d59](https://github.com/fallenbagel/jellyseerr/commit/dd48d59b20e2d1800ea30912116f4a4f1bb7928f))
* **deps:** update dependency axios to v1 ([#3202](https://github.com/fallenbagel/jellyseerr/issues/3202)) ([421029e](https://github.com/fallenbagel/jellyseerr/commit/421029ebab66c9a6622ba47e56d7f6473524cce4))
* **deps:** update dependency swr to v2 ([#3212](https://github.com/fallenbagel/jellyseerr/issues/3212)) ([7b6db50](https://github.com/fallenbagel/jellyseerr/commit/7b6db50ae55b1fc60d19a5cff62dd46bb989fa51))
* **experimental:** use new RT API (sorta) ([#3179](https://github.com/fallenbagel/jellyseerr/issues/3179)) ([357cab8](https://github.com/fallenbagel/jellyseerr/commit/357cab87ac7752b8e119b51c938b343c661d83c2))
* improve small screen layout for discover editing ([#3221](https://github.com/fallenbagel/jellyseerr/issues/3221)) ([d23b213](https://github.com/fallenbagel/jellyseerr/commit/d23b2132de05f072f7f9daad83d81421d747cf99))
* include new package calendar css in build ([#3235](https://github.com/fallenbagel/jellyseerr/issues/3235)) ([c2a1a20](https://github.com/fallenbagel/jellyseerr/commit/c2a1a20a3bb20039a1936c7fe0ecb9e8311a0aea))
* issues with issues ([#3267](https://github.com/fallenbagel/jellyseerr/issues/3267)) ([fd21971](https://github.com/fallenbagel/jellyseerr/commit/fd219717c01c558814d7a80de6304272b5a7944e))
* multiple genre filtering now works ([#3282](https://github.com/fallenbagel/jellyseerr/issues/3282)) ([5076938](https://github.com/fallenbagel/jellyseerr/commit/507693881b939819413f0959df5ef6b7a357eb5c))
* prevent double encode if we are on /search endpoint ([#3238](https://github.com/fallenbagel/jellyseerr/issues/3238)) ([a343f8a](https://github.com/fallenbagel/jellyseerr/commit/a343f8ad915491a9c81512c7e541a1dac8906025))
* **request:** approve request when retrying request ([#3234](https://github.com/fallenbagel/jellyseerr/issues/3234)) ([b515701](https://github.com/fallenbagel/jellyseerr/commit/b5157010c46cd9083993d5ee0172007b83d631da))
* **request:** mark request as approved if media is already available when retrying failed request ([#3244](https://github.com/fallenbagel/jellyseerr/issues/3244)) ([cb65074](https://github.com/fallenbagel/jellyseerr/commit/cb650745f6a33e69391a633e6d272831f314e098))
* restore border to ghost button and fix discover slider visibility toggle position ([#3226](https://github.com/fallenbagel/jellyseerr/issues/3226)) ([2eebb7f](https://github.com/fallenbagel/jellyseerr/commit/2eebb7fd3941b34fe9472aaf9d28265df8cce311))
* restore status badges on titles on actors page when hide available media enabled ([#3206](https://github.com/fallenbagel/jellyseerr/issues/3206)) ([9d3446d](https://github.com/fallenbagel/jellyseerr/commit/9d3446d370499c3251159393e5c791b01225e05c))
* screen would zoom on mobile if date picker input was selected ([#3241](https://github.com/fallenbagel/jellyseerr/issues/3241)) ([3aefddd](https://github.com/fallenbagel/jellyseerr/commit/3aefddd48834d86150d5f5cceb2d08af3a78847b))
* series displayed an empty season with series list/request modal ([#3147](https://github.com/fallenbagel/jellyseerr/issues/3147)) ([2179637](https://github.com/fallenbagel/jellyseerr/commit/2179637d437999290eaa4152f6f37c71fc3d8ba3))
* tooltip shows properly if not in progress ([#3185](https://github.com/fallenbagel/jellyseerr/issues/3185)) ([6face8c](https://github.com/fallenbagel/jellyseerr/commit/6face8cc4564b978fb98af32659b326d8c5cede8))
* **ui:** series first air date sorting ([#3283](https://github.com/fallenbagel/jellyseerr/issues/3283)) ([374c78c](https://github.com/fallenbagel/jellyseerr/commit/374c78c989cc86bb144a954a91d5d183c4b591c0))
* update StatusBadgeMini to shrink on title cards (and remove ring) ([#3210](https://github.com/fallenbagel/jellyseerr/issues/3210)) ([042a1a9](https://github.com/fallenbagel/jellyseerr/commit/042a1a950fdd4d4a61edf4bc19657f9b7a526da8))
### Features
* add discover customization ([#3182](https://github.com/fallenbagel/jellyseerr/issues/3182)) ([cd35748](https://github.com/fallenbagel/jellyseerr/commit/cd3574851a12517cbfadc109e6412a7a9e44c114))
* add keywords to movie/series detail pages ([#3204](https://github.com/fallenbagel/jellyseerr/issues/3204)) ([e084649](https://github.com/fallenbagel/jellyseerr/commit/e084649878a58c296786141d12dd69a69a27ee85))
* add streaming services filter ([#3247](https://github.com/fallenbagel/jellyseerr/issues/3247)) ([1154156](https://github.com/fallenbagel/jellyseerr/commit/1154156459403494e8daf0c89a3ba356aeea1d97))
* discover inline customization ([#3220](https://github.com/fallenbagel/jellyseerr/issues/3220)) ([8bd10b5](https://github.com/fallenbagel/jellyseerr/commit/8bd10b5bf3d1b8069872b616c7c8596caeb4937e))
* discover overhaul (filters!) ([#3232](https://github.com/fallenbagel/jellyseerr/issues/3232)) ([dd00e48](https://github.com/fallenbagel/jellyseerr/commit/dd00e48f59054b44bef6b32a2c169e59f6175051))
* discover slider edit arrow buttons for reordering ([#3259](https://github.com/fallenbagel/jellyseerr/issues/3259)) ([da00d45](https://github.com/fallenbagel/jellyseerr/commit/da00d454e17e8b00d04f6e26f6dd5153ed6ced81))
* **lang:** translations update from Hosted Weblate ([#3030](https://github.com/fallenbagel/jellyseerr/issues/3030)) ([0d8b390](https://github.com/fallenbagel/jellyseerr/commit/0d8b390b678731e76bd1f0f8a0a4952c11e77f4d))
* new mobile menu ([#3251](https://github.com/fallenbagel/jellyseerr/issues/3251)) ([fcbca17](https://github.com/fallenbagel/jellyseerr/commit/fcbca1722f31f32633a57bc5048f46c9da057d87))
* translations update from Hosted Weblate ([#3218](https://github.com/fallenbagel/jellyseerr/issues/3218)) ([5940ff7](https://github.com/fallenbagel/jellyseerr/commit/5940ff7f5f62eed9ac5aa6f02803418aaa09813a))
* **ui:** add episode number to front of episode name in season details ([#3086](https://github.com/fallenbagel/jellyseerr/issues/3086)) ([a672b32](https://github.com/fallenbagel/jellyseerr/commit/a672b324ec391a20f6f3a1daed82a8d276a52c2c))
* **ui:** request card progress bar ([#3123](https://github.com/fallenbagel/jellyseerr/issues/3123)) ([03853a1](https://github.com/fallenbagel/jellyseerr/commit/03853a1b9155c8a2153c8885022a74619af1bc15))
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02) # [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
### Bug Fixes ### Bug Fixes

View File

@@ -8,7 +8,7 @@
<p align="center"> <p align="center">
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/discord/952656177924300932" alt="Discord"></a> <a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/discord/952656177924300932" alt="Discord"></a>
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a> <a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a> <a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a> <a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- 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-47-orange.svg"/></a> <a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-47-orange.svg"/></a>

View File

@@ -22,7 +22,7 @@ export const VersionMismatchWarning = () => {
<> <>
{!isUpToDate ? ( {!isUpToDate ? (
<Admonition type="warning"> <Admonition type="warning">
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package">override the package derivation</a>. The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package-derivation">override the package derivation</a>.
</Admonition> </Admonition>
) : ( ) : (
<Admonition type="success"> <Admonition type="success">

View File

@@ -12,6 +12,8 @@ This is your Jellyseerr API key, which can be used to integrate Jellyseerr with
If you need to generate a new API key for any reason, simply click the button to the right of the text box. If you need to generate a new API key for any reason, simply click the button to the right of the text box.
If you want to set the API key, rather than letting it be randomly generated, you can use the API_KEY environment variable. Whatever that variable is set to will be your API key.
## Application Title ## Application Title
If you aren't a huge fan of the name "Jellyseerr" and would like to display something different to your users, you can customize the application title! If you aren't a huge fan of the name "Jellyseerr" and would like to display something different to your users, you can customize the application title!

View File

@@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene
### Discover Region & Discover Language ### Discover Region & Discover Language
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-and-discover-language) to suit their own preferences. Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region--discover-language) to suit their own preferences.
### Movie Request Limit & Series Request Limit ### Movie Request Limit & Series Request Limit

View File

@@ -6,15 +6,10 @@ module.exports = {
commitTag: process.env.COMMIT_TAG || 'local', commitTag: process.env.COMMIT_TAG || 'local',
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false', forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
}, },
publicRuntimeConfig: {
// Will be available on both server and client
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
},
images: { images: {
remotePatterns: [ remotePatterns: [
{ hostname: 'gravatar.com' }, { hostname: 'gravatar.com' },
{ hostname: 'image.tmdb.org' }, { hostname: 'image.tmdb.org' },
{ hostname: '*', protocol: 'https' },
], ],
}, },
webpack(config) { webpack(config) {

View File

@@ -38,6 +38,8 @@ tags:
description: Endpoints related to getting service (Radarr/Sonarr) details. description: Endpoints related to getting service (Radarr/Sonarr) details.
- name: watchlist - name: watchlist
description: Collection of media to watch later description: Collection of media to watch later
- name: blacklist
description: Blacklisted media from discovery page.
servers: servers:
- url: '{server}/api/v1' - url: '{server}/api/v1'
variables: variables:
@@ -46,6 +48,19 @@ servers:
components: components:
schemas: schemas:
Blacklist:
type: object
properties:
tmdbId:
type: number
example: 1
title:
type: string
media:
$ref: '#/components/schemas/MediaInfo'
userId:
type: number
example: 1
Watchlist: Watchlist:
type: object type: object
properties: properties:
@@ -2775,6 +2790,15 @@ paths:
imageCount: imageCount:
type: number type: number
example: 123 example: 123
avatar:
type: object
properties:
size:
type: number
example: 123456
imageCount:
type: number
example: 123
apiCaches: apiCaches:
type: array type: array
items: items:
@@ -3586,6 +3610,8 @@ paths:
type: string type: string
email: email:
type: string type: string
serverType:
type: number
required: required:
- username - username
- password - password
@@ -4040,6 +4066,94 @@ paths:
restricted: restricted:
type: boolean type: boolean
example: false example: false
/blacklist:
get:
summary: Returns blacklisted items
description: Returns list of all blacklisted media
tags:
- settings
parameters:
- in: query
name: take
schema:
type: number
nullable: true
example: 25
- in: query
name: skip
schema:
type: number
nullable: true
example: 0
- in: query
name: search
schema:
type: string
nullable: true
example: dune
responses:
'200':
description: Blacklisted items returned
content:
application/json:
schema:
type: object
properties:
pageInfo:
$ref: '#/components/schemas/PageInfo'
results:
type: array
items:
type: object
properties:
user:
$ref: '#/components/schemas/User'
createdAt:
type: string
example: 2024-04-21T01:55:44.000Z
id:
type: number
example: 1
mediaType:
type: string
example: movie
title:
type: string
example: Dune
tmdbId:
type: number
example: 438631
post:
summary: Add media to blacklist
tags:
- blacklist
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Blacklist'
responses:
'201':
description: Item succesfully blacklisted
'412':
description: Item has already been blacklisted
/blacklist/{tmdbId}:
delete:
summary: Remove media from blacklist
tags:
- blacklist
parameters:
- in: path
name: tmdbId
description: tmdbId ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed media item
/watchlist: /watchlist:
post: post:
summary: Add media to watchlist summary: Add media to watchlist
@@ -4862,6 +4976,11 @@ paths:
schema: schema:
type: string type: string
example: 8|9 example: 8|9
- in: query
name: status
schema:
type: string
example: 3|4
responses: responses:
'200': '200':
description: Results description: Results

View File

@@ -1,6 +1,6 @@
{ {
"name": "jellyseerr", "name": "jellyseerr",
"version": "0.1.0", "version": "2.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -62,6 +62,7 @@
"formik": "^2.4.6", "formik": "^2.4.6",
"gravatar-url": "3.1.0", "gravatar-url": "3.1.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"mime": "3",
"next": "^14.2.4", "next": "^14.2.4",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-gyp": "9.3.1", "node-gyp": "9.3.1",
@@ -119,6 +120,7 @@
"@types/express": "4.17.17", "@types/express": "4.17.17",
"@types/express-session": "1.17.6", "@types/express-session": "1.17.6",
"@types/lodash": "4.14.191", "@types/lodash": "4.14.191",
"@types/mime": "3",
"@types/node": "20.14.8", "@types/node": "20.14.8",
"@types/node-schedule": "2.1.0", "@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",

25
pnpm-lock.yaml generated
View File

@@ -98,6 +98,9 @@ importers:
lodash: lodash:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
mime:
specifier: '3'
version: 3.0.0
next: next:
specifier: ^14.2.4 specifier: ^14.2.4
version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -264,6 +267,9 @@ importers:
'@types/lodash': '@types/lodash':
specifier: 4.14.191 specifier: 4.14.191
version: 4.14.191 version: 4.14.191
'@types/mime':
specifier: '3'
version: 3.0.4
'@types/node': '@types/node':
specifier: 20.14.8 specifier: 20.14.8
version: 20.14.8 version: 20.14.8
@@ -2848,6 +2854,9 @@ packages:
'@types/mime@1.3.5': '@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/mime@3.0.4':
resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==}
'@types/minimatch@3.0.5': '@types/minimatch@3.0.5':
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
@@ -10836,7 +10845,7 @@ snapshots:
nopt: 5.0.0 nopt: 5.0.0
npmlog: 5.0.1 npmlog: 5.0.1
rimraf: 3.0.2 rimraf: 3.0.2
semver: 7.6.2 semver: 7.3.8
tar: 6.2.1 tar: 6.2.1
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@@ -10911,13 +10920,13 @@ snapshots:
'@npmcli/fs@1.1.1': '@npmcli/fs@1.1.1':
dependencies: dependencies:
'@gar/promisify': 1.1.3 '@gar/promisify': 1.1.3
semver: 7.6.2 semver: 7.3.8
optional: true optional: true
'@npmcli/fs@2.1.2': '@npmcli/fs@2.1.2':
dependencies: dependencies:
'@gar/promisify': 1.1.3 '@gar/promisify': 1.1.3
semver: 7.6.2 semver: 7.3.8
'@npmcli/move-file@1.1.2': '@npmcli/move-file@1.1.2':
dependencies: dependencies:
@@ -12326,7 +12335,7 @@ snapshots:
read-pkg: 5.2.0 read-pkg: 5.2.0
registry-auth-token: 5.0.2 registry-auth-token: 5.0.2
semantic-release: 19.0.5(encoding@0.1.13) semantic-release: 19.0.5(encoding@0.1.13)
semver: 7.6.2 semver: 7.3.8
tempy: 1.0.1 tempy: 1.0.1
'@semantic-release/release-notes-generator@10.0.3(semantic-release@19.0.5(encoding@0.1.13))': '@semantic-release/release-notes-generator@10.0.3(semantic-release@19.0.5(encoding@0.1.13))':
@@ -12670,6 +12679,8 @@ snapshots:
'@types/mime@1.3.5': {} '@types/mime@1.3.5': {}
'@types/mime@3.0.4': {}
'@types/minimatch@3.0.5': {} '@types/minimatch@3.0.5': {}
'@types/minimist@1.2.5': {} '@types/minimist@1.2.5': {}
@@ -12887,7 +12898,7 @@ snapshots:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
semver: 7.6.2 semver: 7.3.8
tsutils: 3.21.0(typescript@4.9.5) tsutils: 3.21.0(typescript@4.9.5)
optionalDependencies: optionalDependencies:
typescript: 4.9.5 typescript: 4.9.5
@@ -17269,7 +17280,7 @@ snapshots:
nopt: 5.0.0 nopt: 5.0.0
npmlog: 6.0.2 npmlog: 6.0.2
rimraf: 3.0.2 rimraf: 3.0.2
semver: 7.6.2 semver: 7.3.8
tar: 6.2.1 tar: 6.2.1
which: 2.0.2 which: 2.0.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -17348,7 +17359,7 @@ snapshots:
dependencies: dependencies:
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
is-core-module: 2.14.0 is-core-module: 2.14.0
semver: 7.6.2 semver: 7.3.8
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {} normalize-path@3.0.0: {}

View File

@@ -76,7 +76,7 @@ class ExternalAPI {
} }
const data = await this.getDataFromResponse(response); const data = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
} }
@@ -120,7 +120,7 @@ class ExternalAPI {
} }
const resData = await this.getDataFromResponse(response); const resData = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
} }
@@ -164,7 +164,7 @@ class ExternalAPI {
} }
const resData = await this.getDataFromResponse(response); const resData = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
} }

View File

@@ -157,9 +157,13 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => { public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
try { try {
const data = await this.get<QueueResponse<QueueItemAppendT>>(`/queue`, { const data = await this.get<QueueResponse<QueueItemAppendT>>(
includeEpisode: 'true', `/queue`,
}); {
includeEpisode: 'true',
},
0
);
return data.records; return data.records;
} catch (e) { } catch (e) {
@@ -193,15 +197,24 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
} }
}; };
async refreshMonitoredDownloads(): Promise<void> {
await this.runCommand('RefreshMonitoredDownloads', {});
}
protected async runCommand( protected async runCommand(
commandName: string, commandName: string,
options: Record<string, unknown> options: Record<string, unknown>
): Promise<void> { ): Promise<void> {
try { try {
await this.post(`/command`, { await this.post(
name: commandName, `/command`,
...options, {
}); name: commandName,
...options,
},
{},
0
);
} catch (e) { } catch (e) {
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`); throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
} }

View File

@@ -303,10 +303,10 @@ class SonarrAPI extends ServarrBase<{
}); });
try { try {
await this.runCommand('SeriesSearch', { seriesId }); await this.runCommand('MissingEpisodeSearch', { seriesId });
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while executing Sonarr series search.', 'Something went wrong while executing Sonarr missing episode search.',
{ {
label: 'Sonarr API', label: 'Sonarr API',
errorMessage: e.message, errorMessage: e.message,

View File

@@ -95,6 +95,7 @@ interface DiscoverTvOptions {
sortBy?: SortOptions; sortBy?: SortOptions;
watchRegion?: string; watchRegion?: string;
watchProviders?: string; watchProviders?: string;
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
} }
class TheMovieDb extends ExternalAPI { class TheMovieDb extends ExternalAPI {
@@ -523,6 +524,7 @@ class TheMovieDb extends ExternalAPI {
voteCountLte, voteCountLte,
watchProviders, watchProviders,
watchRegion, watchRegion,
withStatus,
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => { }: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
try { try {
const defaultFutureDate = new Date( const defaultFutureDate = new Date(
@@ -570,6 +572,7 @@ class TheMovieDb extends ExternalAPI {
'vote_count.lte': voteCountLte || '', 'vote_count.lte': voteCountLte || '',
with_watch_providers: watchProviders || '', with_watch_providers: watchProviders || '',
watch_region: watchRegion || '', watch_region: watchRegion || '',
with_status: withStatus || '',
}); });
return data; return data;

View File

@@ -2,6 +2,7 @@ export enum ApiErrorCode {
InvalidUrl = 'INVALID_URL', InvalidUrl = 'INVALID_URL',
InvalidCredentials = 'INVALID_CREDENTIALS', InvalidCredentials = 'INVALID_CREDENTIALS',
InvalidAuthToken = 'INVALID_AUTH_TOKEN', InvalidAuthToken = 'INVALID_AUTH_TOKEN',
InvalidEmail = 'INVALID_EMAIL',
NotAdmin = 'NOT_ADMIN', NotAdmin = 'NOT_ADMIN',
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS', SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES', SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',

View File

@@ -16,4 +16,5 @@ export enum MediaStatus {
PROCESSING, PROCESSING,
PARTIALLY_AVAILABLE, PARTIALLY_AVAILABLE,
AVAILABLE, AVAILABLE,
BLACKLISTED,
} }

View File

@@ -4,3 +4,8 @@ export enum MediaServerType {
EMBY, EMBY,
NOT_CONFIGURED, NOT_CONFIGURED,
} }
export enum ServerType {
JELLYFIN = 'Jellyfin',
EMBY = 'Emby',
}

View File

@@ -2,4 +2,5 @@ export enum UserType {
PLEX = 1, PLEX = 1,
LOCAL = 2, LOCAL = 2,
JELLYFIN = 3, JELLYFIN = 3,
EMBY = 4,
} }

View File

@@ -0,0 +1,95 @@
import { MediaStatus, type MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { User } from '@server/entity/User';
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import type { ZodNumber, ZodOptional, ZodString } from 'zod';
@Entity()
@Unique(['tmdbId'])
export class Blacklist implements BlacklistItem {
@PrimaryGeneratedColumn()
public id: number;
@Column({ type: 'varchar' })
public mediaType: MediaType;
@Column({ nullable: true, type: 'varchar' })
title?: string;
@Column()
@Index()
public tmdbId: number;
@ManyToOne(() => User, (user) => user.id, {
eager: true,
})
user: User;
@OneToOne(() => Media, (media) => media.blacklist, {
onDelete: 'CASCADE',
})
@JoinColumn()
public media: Media;
@CreateDateColumn()
public createdAt: Date;
constructor(init?: Partial<Blacklist>) {
Object.assign(this, init);
}
public static async addToBlacklist({
blacklistRequest,
}: {
blacklistRequest: {
mediaType: MediaType;
title?: ZodOptional<ZodString>['_output'];
tmdbId: ZodNumber['_output'];
};
}): Promise<void> {
const blacklist = new this({
...blacklistRequest,
});
const mediaRepository = getRepository(Media);
let media = await mediaRepository.findOne({
where: {
tmdbId: blacklistRequest.tmdbId,
},
});
const blacklistRepository = getRepository(this);
await blacklistRepository.save(blacklist);
if (!media) {
media = new Media({
tmdbId: blacklistRequest.tmdbId,
status: MediaStatus.BLACKLISTED,
status4k: MediaStatus.BLACKLISTED,
mediaType: blacklistRequest.mediaType,
blacklist: blacklist,
});
await mediaRepository.save(media);
} else {
media.blacklist = blacklist;
media.status = MediaStatus.BLACKLISTED;
media.status4k = MediaStatus.BLACKLISTED;
await mediaRepository.save(media);
}
}
}

View File

@@ -3,6 +3,7 @@ import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaStatus, MediaType } from '@server/constants/media'; import { MediaStatus, MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import { Blacklist } from '@server/entity/Blacklist';
import type { User } from '@server/entity/User'; import type { User } from '@server/entity/User';
import { Watchlist } from '@server/entity/Watchlist'; import { Watchlist } from '@server/entity/Watchlist';
import type { DownloadingItem } from '@server/lib/downloadtracker'; import type { DownloadingItem } from '@server/lib/downloadtracker';
@@ -17,6 +18,7 @@ import {
Entity, Entity,
Index, Index,
OneToMany, OneToMany,
OneToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
@@ -66,7 +68,7 @@ class Media {
try { try {
const media = await mediaRepository.findOne({ const media = await mediaRepository.findOne({
where: { tmdbId: id, mediaType }, where: { tmdbId: id, mediaType: mediaType },
relations: { requests: true, issues: true }, relations: { requests: true, issues: true },
}); });
@@ -116,6 +118,11 @@ class Media {
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true }) @OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
public issues: Issue[]; public issues: Issue[];
@OneToOne(() => Blacklist, (blacklist) => blacklist.media, {
eager: true,
})
public blacklist: Blacklist;
@CreateDateColumn() @CreateDateColumn()
public createdAt: Date; public createdAt: Date;
@@ -211,9 +218,10 @@ class Media {
} }
} else { } else {
const pageName = const pageName =
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details'; getSettings().main.mediaServerType == MediaServerType.EMBY
? 'item'
: 'details';
const { serverId, externalHostname } = getSettings().jellyfin; const { serverId, externalHostname } = getSettings().jellyfin;
const jellyfinHost = const jellyfinHost =
externalHostname && externalHostname.length > 0 externalHostname && externalHostname.length > 0
? externalHostname ? externalHostname

View File

@@ -40,6 +40,7 @@ export class RequestPermissionError extends Error {}
export class QuotaRestrictedError extends Error {} export class QuotaRestrictedError extends Error {}
export class DuplicateMediaRequestError extends Error {} export class DuplicateMediaRequestError extends Error {}
export class NoSeasonsAvailableError extends Error {} export class NoSeasonsAvailableError extends Error {}
export class BlacklistedMediaError extends Error {}
type MediaRequestOptions = { type MediaRequestOptions = {
isAutoRequest?: boolean; isAutoRequest?: boolean;
@@ -143,6 +144,16 @@ export class MediaRequest {
mediaType: requestBody.mediaType, mediaType: requestBody.mediaType,
}); });
} else { } else {
if (media.status === MediaStatus.BLACKLISTED) {
logger.warn('Request for media blocked due to being blacklisted', {
tmdbId: tmdbMedia.id,
mediaType: requestBody.mediaType,
label: 'Media Request',
});
throw new BlacklistedMediaError('This media is blacklisted.');
}
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) { if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
media.status = MediaStatus.PENDING; media.status = MediaStatus.PENDING;
} }

View File

@@ -19,6 +19,7 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import clearCookies from '@server/middleware/clearcookies'; import clearCookies from '@server/middleware/clearcookies';
import routes from '@server/routes'; import routes from '@server/routes';
import avatarproxy from '@server/routes/avatarproxy';
import imageproxy from '@server/routes/imageproxy'; import imageproxy from '@server/routes/imageproxy';
import { getAppVersion } from '@server/utils/appVersion'; import { getAppVersion } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag'; import restartFlag from '@server/utils/restartFlag';
@@ -174,7 +175,7 @@ app
}, },
store: new TypeormStore({ store: new TypeormStore({
cleanupLimit: 2, cleanupLimit: 2,
ttl: 1000 * 60 * 60 * 24 * 30, ttl: 60 * 60 * 24 * 30,
}).connect(sessionRespository) as Store, }).connect(sessionRespository) as Store,
}) })
); );
@@ -202,6 +203,7 @@ app
// Do not set cookies so CDNs can cache them // Do not set cookies so CDNs can cache them
server.use('/imageproxy', clearCookies, imageproxy); server.use('/imageproxy', clearCookies, imageproxy);
server.use('/avatarproxy', clearCookies, avatarproxy);
server.get('*', (req, res) => handle(req, res)); server.get('*', (req, res) => handle(req, res));
server.use( server.use(

View File

@@ -0,0 +1,14 @@
import type { User } from '@server/entity/User';
import type { PaginatedResponse } from '@server/interfaces/api/common';
export interface BlacklistItem {
tmdbId: number;
mediaType: 'movie' | 'tv';
title?: string;
createdAt?: Date;
user: User;
}
export interface BlacklistResultsResponse extends PaginatedResponse {
results: BlacklistItem[];
}

View File

@@ -58,7 +58,7 @@ export interface CacheItem {
export interface CacheResponse { export interface CacheResponse {
apiCaches: CacheItem[]; apiCaches: CacheItem[];
imageCache: Record<'tmdb', { size: number; imageCount: number }>; imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>;
} }
export interface StatusResponse { export interface StatusResponse {

View File

@@ -227,6 +227,9 @@ export const startJobs = (): void => {
}); });
// Clean TMDB image cache // Clean TMDB image cache
ImageProxy.clearCache('tmdb'); ImageProxy.clearCache('tmdb');
// Clean users avatar image cache
ImageProxy.clearCache('avatar');
}), }),
}); });

View File

@@ -20,6 +20,7 @@ export interface DownloadingItem {
timeLeft: string; timeLeft: string;
estimatedCompletionTime: Date; estimatedCompletionTime: Date;
title: string; title: string;
downloadId: string;
episode?: EpisodeNumberResult; episode?: EpisodeNumberResult;
} }
@@ -84,6 +85,7 @@ class DownloadTracker {
}); });
try { try {
await radarr.refreshMonitoredDownloads();
const queueItems = await radarr.getQueue(); const queueItems = await radarr.getQueue();
this.radarrServers[server.id] = queueItems.map((item) => ({ this.radarrServers[server.id] = queueItems.map((item) => ({
@@ -95,6 +97,7 @@ class DownloadTracker {
status: item.status, status: item.status,
timeLeft: item.timeleft, timeLeft: item.timeleft,
title: item.title, title: item.title,
downloadId: item.downloadId,
})); }));
if (queueItems.length > 0) { if (queueItems.length > 0) {
@@ -160,6 +163,7 @@ class DownloadTracker {
}); });
try { try {
await sonarr.refreshMonitoredDownloads();
const queueItems = await sonarr.getQueue(); const queueItems = await sonarr.getQueue();
this.sonarrServers[server.id] = queueItems.map((item) => ({ this.sonarrServers[server.id] = queueItems.map((item) => ({
@@ -172,6 +176,7 @@ class DownloadTracker {
timeLeft: item.timeleft, timeLeft: item.timeleft,
title: item.title, title: item.title,
episode: item.episode, episode: item.episode,
downloadId: item.downloadId,
})); }));
if (queueItems.length > 0) { if (queueItems.length > 0) {

View File

@@ -3,6 +3,7 @@ import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit'; import rateLimit from '@server/utils/rateLimit';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { promises } from 'fs'; import { promises } from 'fs';
import mime from 'mime/lite';
import path, { join } from 'path'; import path, { join } from 'path';
type ImageResponse = { type ImageResponse = {
@@ -11,7 +12,7 @@ type ImageResponse = {
curRevalidate: number; curRevalidate: number;
isStale: boolean; isStale: boolean;
etag: string; etag: string;
extension: string; extension: string | null;
cacheKey: string; cacheKey: string;
cacheMiss: boolean; cacheMiss: boolean;
}; };
@@ -27,29 +28,45 @@ class ImageProxy {
let deletedImages = 0; let deletedImages = 0;
const cacheDirectory = path.join(baseCacheDirectory, key); const cacheDirectory = path.join(baseCacheDirectory, key);
const files = await promises.readdir(cacheDirectory); try {
const files = await promises.readdir(cacheDirectory);
for (const file of files) { for (const file of files) {
const filePath = path.join(cacheDirectory, file); const filePath = path.join(cacheDirectory, file);
const stat = await promises.lstat(filePath); const stat = await promises.lstat(filePath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
const imageFiles = await promises.readdir(filePath); const imageFiles = await promises.readdir(filePath);
for (const imageFile of imageFiles) { for (const imageFile of imageFiles) {
const [, expireAtSt] = imageFile.split('.'); const [, expireAtSt] = imageFile.split('.');
const expireAt = Number(expireAtSt); const expireAt = Number(expireAtSt);
const now = Date.now(); const now = Date.now();
if (now > expireAt) { if (now > expireAt) {
await promises.rm(path.join(filePath, imageFile)); await promises.rm(path.join(filePath), {
deletedImages += 1; recursive: true,
});
deletedImages += 1;
}
} }
} }
} }
} catch (e) {
if (e.code === 'ENOENT') {
logger.error('Directory not found', {
label: 'Image Cache',
message: e.message,
});
} else {
logger.error('Failed to read directory', {
label: 'Image Cache',
message: e.message,
});
}
} }
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, { logger.info(`Cleared ${deletedImages} stale image(s) from cache '${key}'`, {
label: 'Image Cache', label: 'Image Cache',
}); });
} }
@@ -69,33 +86,49 @@ class ImageProxy {
} }
private static async getDirectorySize(dir: string): Promise<number> { private static async getDirectorySize(dir: string): Promise<number> {
const files = await promises.readdir(dir, { try {
withFileTypes: true, const files = await promises.readdir(dir, {
}); withFileTypes: true,
});
const paths = files.map(async (file) => { const paths = files.map(async (file) => {
const path = join(dir, file.name); const path = join(dir, file.name);
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path); if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
if (file.isFile()) { if (file.isFile()) {
const { size } = await promises.stat(path); const { size } = await promises.stat(path);
return size; return size;
}
return 0;
});
return (await Promise.all(paths))
.flat(Infinity)
.reduce((i, size) => i + size, 0);
} catch (e) {
if (e.code === 'ENOENT') {
return 0;
} }
}
return 0; return 0;
});
return (await Promise.all(paths))
.flat(Infinity)
.reduce((i, size) => i + size, 0);
} }
private static async getImageCount(dir: string) { private static async getImageCount(dir: string) {
const files = await promises.readdir(dir); try {
const files = await promises.readdir(dir);
return files.length; return files.length;
} catch (e) {
if (e.code === 'ENOENT') {
return 0;
}
}
return 0;
} }
private fetch: typeof fetch; private fetch: typeof fetch;
@@ -147,6 +180,27 @@ class ImageProxy {
return imageResponse; return imageResponse;
} }
public async clearCachedImage(path: string) {
// find cacheKey
const cacheKey = this.getCacheKey(path);
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const files = await promises.readdir(directory);
await promises.rm(directory, { recursive: true });
logger.info(`Cleared ${files[0]} from cache 'avatar'`, {
label: 'Image Cache',
});
} catch (e) {
logger.error('Failed to clear cached image', {
label: 'Image Cache',
message: e.message,
});
}
}
private async get(cacheKey: string): Promise<ImageResponse | null> { private async get(cacheKey: string): Promise<ImageResponse | null> {
try { try {
const directory = join(this.getCacheDirectory(), cacheKey); const directory = join(this.getCacheDirectory(), cacheKey);
@@ -187,16 +241,25 @@ class ImageProxy {
const directory = join(this.getCacheDirectory(), cacheKey); const directory = join(this.getCacheDirectory(), cacheKey);
const href = const href =
this.baseUrl + this.baseUrl +
(this.baseUrl.endsWith('/') ? '' : '/') + (this.baseUrl.length > 0
? this.baseUrl.endsWith('/')
? ''
: '/'
: '') +
(path.startsWith('/') ? path.slice(1) : path); (path.startsWith('/') ? path.slice(1) : path);
const response = await this.fetch(href); const response = await this.fetch(href);
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer); const buffer = Buffer.from(arrayBuffer);
const extension = path.split('.').pop() ?? ''; const extension = mime.getExtension(
const maxAge = Number( response.headers.get('content-type') ?? ''
);
let maxAge = Number(
(response.headers.get('cache-control') ?? '0').split('=')[1] (response.headers.get('cache-control') ?? '0').split('=')[1]
); );
if (!maxAge) maxAge = 86400;
const expireAt = Date.now() + maxAge * 1000; const expireAt = Date.now() + maxAge * 1000;
const etag = (response.headers.get('etag') ?? '').replace(/"/g, ''); const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
@@ -232,7 +295,7 @@ class ImageProxy {
private async writeToCacheDir( private async writeToCacheDir(
dir: string, dir: string,
extension: string, extension: string | null,
maxAge: number, maxAge: number,
expireAt: number, expireAt: number,
buffer: Buffer, buffer: Buffer,

View File

@@ -27,6 +27,8 @@ export enum Permission {
AUTO_REQUEST_TV = 33554432, AUTO_REQUEST_TV = 33554432,
RECENT_VIEW = 67108864, RECENT_VIEW = 67108864,
WATCHLIST_VIEW = 134217728, WATCHLIST_VIEW = 134217728,
MANAGE_BLACKLIST = 268435456,
VIEW_BLACKLIST = 1073741824,
} }
export interface PermissionCheckOptions { export interface PermissionCheckOptions {

View File

@@ -567,7 +567,10 @@ class JellyfinScanner {
public async run(): Promise<void> { public async run(): Promise<void> {
const settings = getSettings(); const settings = getSettings();
if (settings.main.mediaServerType != MediaServerType.JELLYFIN) { if (
settings.main.mediaServerType != MediaServerType.JELLYFIN &&
settings.main.mediaServerType != MediaServerType.EMBY
) {
return; return;
} }

View File

@@ -611,7 +611,11 @@ class Settings {
} }
private generateApiKey(): string { private generateApiKey(): string {
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64'); if (process.env.API_KEY) {
return process.env.API_KEY;
} else {
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
}
} }
private generateVapidKeys(force = false): void { private generateVapidKeys(force = false): void {
@@ -648,6 +652,12 @@ class Settings {
this.data = merge(this.data, parsedJson); this.data = merge(this.data, parsedJson);
if (process.env.API_KEY) {
if (this.main.apiKey != process.env.API_KEY) {
this.main.apiKey = process.env.API_KEY;
}
}
this.save(); this.save();
} }
return this; return this;

View File

@@ -0,0 +1,16 @@
import { MediaServerType } from '@server/constants/server';
import type { AllSettings } from '@server/lib/settings';
const migrateHostname = (settings: any): AllSettings => {
const oldMediaServerType = settings.main.mediaServerType;
if (
oldMediaServerType === MediaServerType.JELLYFIN &&
process.env.JELLYFIN_TYPE === 'emby'
) {
settings.main.mediaServerType = MediaServerType.EMBY;
}
return settings;
};
export default migrateHostname;

View File

@@ -0,0 +1,20 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBlacklist1699901142442 implements MigrationInterface {
name = 'AddBlacklist1699901142442';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')),"userId" integer, "mediaId" integer,CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId"))`
);
await queryRunner.query(
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "blacklist"`);
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
}
}

View File

@@ -1,11 +1,12 @@
import JellyfinAPI from '@server/api/jellyfin'; import JellyfinAPI from '@server/api/jellyfin';
import PlexTvAPI from '@server/api/plextv'; import PlexTvAPI from '@server/api/plextv';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType, ServerType } from '@server/constants/server';
import { UserType } from '@server/constants/user'; import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User'; import { User } from '@server/entity/User';
import { startJobs } from '@server/job/schedule'; import { startJobs } from '@server/job/schedule';
import ImageProxy from '@server/lib/imageproxy';
import { Permission } from '@server/lib/permissions'; import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
@@ -227,15 +228,20 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
urlBase?: string; urlBase?: string;
useSsl?: boolean; useSsl?: boolean;
email?: string; email?: string;
serverType?: number;
}; };
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured //Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured
if ( if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN && settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED settings.main.mediaServerType !== MediaServerType.EMBY &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED &&
settings.jellyfin.ip !== ''
) { ) {
return res.status(500).json({ error: 'Jellyfin login is disabled' }); return res.status(500).json({ error: 'Jellyfin login is disabled' });
} else if (!body.username) { }
if (!body.username) {
return res.status(500).json({ error: 'You must provide an username' }); return res.status(500).json({ error: 'You must provide an username' });
} else if (settings.jellyfin.ip !== '' && body.hostname) { } else if (settings.jellyfin.ip !== '' && body.hostname) {
return res return res
@@ -273,7 +279,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
} }
// First we need to attempt to log the user in to jellyfin // First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId); const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
const jellyfinHost = const jellyfinHost =
externalHostname && externalHostname.length > 0 externalHostname && externalHostname.length > 0
? externalHostname ? externalHostname
@@ -317,22 +324,49 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
); );
// User doesn't exist, and there are no users in the database, we'll create the user // User doesn't exist, and there are no users in the database, we'll create the user
// with admin permission // with admin permissions
settings.main.mediaServerType = MediaServerType.JELLYFIN; switch (body.serverType) {
user = new User({ case MediaServerType.EMBY:
email: body.email || account.User.Name, settings.main.mediaServerType = MediaServerType.EMBY;
jellyfinUsername: account.User.Name, user = new User({
jellyfinUserId: account.User.Id, email: body.email || account.User.Name,
jellyfinDeviceId: deviceId, jellyfinUsername: account.User.Name,
permissions: Permission.ADMIN, jellyfinUserId: account.User.Id,
avatar: account.User.PrimaryImageTag jellyfinDeviceId: deviceId,
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` jellyfinAuthToken: account.AccessToken,
: gravatarUrl(body.email || account.User.Name, { permissions: Permission.ADMIN,
default: 'mm', avatar: account.User.PrimaryImageTag
size: 200, ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
}), : gravatarUrl(body.email || account.User.Name, {
userType: UserType.JELLYFIN, default: 'mm',
}); size: 200,
}),
userType: UserType.EMBY,
});
break;
case MediaServerType.JELLYFIN:
settings.main.mediaServerType = MediaServerType.JELLYFIN;
user = new User({
email: body.email || account.User.Name,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
}),
userType: UserType.JELLYFIN,
});
break;
default:
throw new Error('select_server_type');
}
// Create an API key on Jellyfin from this admin user // Create an API key on Jellyfin from this admin user
const jellyfinClient = new JellyfinAPI( const jellyfinClient = new JellyfinAPI(
@@ -361,12 +395,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
logger.info( logger.info(
`Found matching ${ `Found matching ${
settings.main.mediaServerType === MediaServerType.JELLYFIN settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin' ? ServerType.JELLYFIN
: 'Emby' : ServerType.EMBY
} user; updating user with ${ } user; updating user with ${
settings.main.mediaServerType === MediaServerType.JELLYFIN settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin' ? ServerType.JELLYFIN
: 'Emby' : ServerType.EMBY
}`, }`,
{ {
label: 'API', label: 'API',
@@ -376,12 +410,24 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
); );
// Update the users avatar with their jellyfin profile pic (incase it changed) // Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) { if (account.User.PrimaryImageTag) {
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; const avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
if (avatar !== user.avatar) {
const avatarProxy = new ImageProxy('avatar', '');
avatarProxy.clearCachedImage(user.avatar);
}
user.avatar = avatar;
} else { } else {
user.avatar = gravatarUrl(user.email || account.User.Name, { const avatar = gravatarUrl(user.email || account.User.Name, {
default: 'mm', default: 'mm',
size: 200, size: 200,
}); });
if (avatar !== user.avatar) {
const avatarProxy = new ImageProxy('avatar', '');
avatarProxy.clearCachedImage(user.avatar);
}
user.avatar = avatar;
} }
user.jellyfinUsername = account.User.Name; user.jellyfinUsername = account.User.Name;
@@ -389,12 +435,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
user.username = ''; user.username = '';
} }
// TODO: If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY
// if (process.env.JELLYFIN_TYPE === 'emby') {
// settings.main.mediaServerType = MediaServerType.EMBY;
// settings.save();
// }
await userRepository.save(user); await userRepository.save(user);
} else if (!settings.main.newPlexLogin) { } else if (!settings.main.newPlexLogin) {
logger.warn( logger.warn(
@@ -432,8 +472,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
default: 'mm', default: 'mm',
size: 200, size: 200,
}), }),
userType: UserType.JELLYFIN, userType:
settings.main.mediaServerType === MediaServerType.JELLYFIN
? UserType.JELLYFIN
: UserType.EMBY,
}); });
//initialize Jellyfin/Emby users with local login //initialize Jellyfin/Emby users with local login
const passedExplicitPassword = body.password && body.password.length > 0; const passedExplicitPassword = body.password && body.password.length > 0;
if (passedExplicitPassword) { if (passedExplicitPassword) {

View File

@@ -0,0 +1,32 @@
import ImageProxy from '@server/lib/imageproxy';
import logger from '@server/logger';
import { Router } from 'express';
const router = Router();
const avatarImageProxy = new ImageProxy('avatar', '');
// Proxy avatar images
router.get('/*', async (req, res) => {
const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
try {
const imageData = await avatarImageProxy.getImage(imagePath);
res.writeHead(200, {
'Content-Type': `image/${imageData.meta.extension}`,
'Content-Length': imageData.imageBuffer.length,
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
'OS-Cache-Key': imageData.meta.cacheKey,
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
});
res.end(imageData.imageBuffer);
} catch (e) {
logger.error('Failed to proxy avatar image', {
imagePath,
errorMessage: e.message,
});
}
});
export default router;

148
server/routes/blacklist.ts Normal file
View File

@@ -0,0 +1,148 @@
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { Blacklist } from '@server/entity/Blacklist';
import Media from '@server/entity/Media';
import { NotFoundError } from '@server/entity/Watchlist';
import type { BlacklistResultsResponse } from '@server/interfaces/api/blacklistInterfaces';
import { Permission } from '@server/lib/permissions';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { QueryFailedError } from 'typeorm';
import { z } from 'zod';
const blacklistRoutes = Router();
export const blacklistAdd = z.object({
tmdbId: z.coerce.number(),
mediaType: z.nativeEnum(MediaType),
title: z.coerce.string().optional(),
user: z.coerce.number(),
});
blacklistRoutes.get(
'/',
isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
type: 'or',
}),
rateLimit({ windowMs: 60 * 1000, max: 50 }),
async (req, res, next) => {
const pageSize = req.query.take ? Number(req.query.take) : 25;
const skip = req.query.skip ? Number(req.query.skip) : 0;
const search = (req.query.search as string) ?? '';
try {
let query = getRepository(Blacklist)
.createQueryBuilder('blacklist')
.leftJoinAndSelect('blacklist.user', 'user');
if (search.length > 0) {
query = query.where('blacklist.title like :title', {
title: `%${search}%`,
});
}
const [blacklistedItems, itemsCount] = await query
.orderBy('blacklist.createdAt', 'DESC')
.take(pageSize)
.skip(skip)
.getManyAndCount();
return res.status(200).json({
pageInfo: {
pages: Math.ceil(itemsCount / pageSize),
pageSize,
results: itemsCount,
page: Math.ceil(skip / pageSize) + 1,
},
results: blacklistedItems,
} as BlacklistResultsResponse);
} catch (error) {
logger.error('Something went wrong while retrieving blacklisted items', {
label: 'Blacklist',
errorMessage: error.message,
});
return next({
status: 500,
message: 'Unable to retrieve blacklisted items.',
});
}
}
);
blacklistRoutes.post(
'/',
isAuthenticated([Permission.MANAGE_BLACKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const values = blacklistAdd.parse(req.body);
await Blacklist.addToBlacklist({
blacklistRequest: values,
});
return res.status(201).send();
} catch (error) {
if (!(error instanceof Error)) {
return;
}
if (error instanceof QueryFailedError) {
switch (error.driverError.errno) {
case 19:
return next({ status: 412, message: 'Item already blacklisted' });
default:
logger.warn('Something wrong with data blacklist', {
tmdbId: req.body.tmdbId,
mediaType: req.body.mediaType,
label: 'Blacklist',
});
return next({ status: 409, message: 'Something wrong' });
}
}
return next({ status: 500, message: error.message });
}
}
);
blacklistRoutes.delete(
'/:id',
isAuthenticated([Permission.MANAGE_BLACKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const blacklisteRepository = getRepository(Blacklist);
const blacklistItem = await blacklisteRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
});
await blacklisteRepository.remove(blacklistItem);
const mediaRepository = getRepository(Media);
const mediaItem = await mediaRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
});
await mediaRepository.remove(mediaItem);
return res.status(204).send();
} catch (e) {
if (e instanceof NotFoundError) {
return next({
status: 401,
message: e.message,
});
}
return next({ status: 500, message: e.message });
}
}
);
export default blacklistRoutes;

View File

@@ -71,6 +71,7 @@ const QueryFilterOptions = z.object({
network: z.coerce.string().optional(), network: z.coerce.string().optional(),
watchProviders: z.coerce.string().optional(), watchProviders: z.coerce.string().optional(),
watchRegion: z.coerce.string().optional(), watchRegion: z.coerce.string().optional(),
status: z.coerce.string().optional(),
}); });
export type FilterOptions = z.infer<typeof QueryFilterOptions>; export type FilterOptions = z.infer<typeof QueryFilterOptions>;
@@ -385,6 +386,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
voteCountLte: query.voteCountLte, voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders, watchProviders: query.watchProviders,
watchRegion: query.watchRegion, watchRegion: query.watchRegion,
withStatus: query.status,
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(

View File

@@ -23,6 +23,7 @@ import restartFlag from '@server/utils/restartFlag';
import { isPerson } from '@server/utils/typeHelpers'; import { isPerson } from '@server/utils/typeHelpers';
import { Router } from 'express'; import { Router } from 'express';
import authRoutes from './auth'; import authRoutes from './auth';
import blacklistRoutes from './blacklist';
import collectionRoutes from './collection'; import collectionRoutes from './collection';
import discoverRoutes, { createTmdbWithRegionLanguage } from './discover'; import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
import issueRoutes from './issue'; import issueRoutes from './issue';
@@ -144,6 +145,7 @@ router.use('/search', isAuthenticated(), searchRoutes);
router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/discover', isAuthenticated(), discoverRoutes);
router.use('/request', isAuthenticated(), requestRoutes); router.use('/request', isAuthenticated(), requestRoutes);
router.use('/watchlist', isAuthenticated(), watchlistRoutes); router.use('/watchlist', isAuthenticated(), watchlistRoutes);
router.use('/blacklist', isAuthenticated(), blacklistRoutes);
router.use('/movie', isAuthenticated(), movieRoutes); router.use('/movie', isAuthenticated(), movieRoutes);
router.use('/tv', isAuthenticated(), tvRoutes); router.use('/tv', isAuthenticated(), tvRoutes);
router.use('/media', isAuthenticated(), mediaRoutes); router.use('/media', isAuthenticated(), mediaRoutes);

View File

@@ -8,6 +8,7 @@ import {
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import { import {
BlacklistedMediaError,
DuplicateMediaRequestError, DuplicateMediaRequestError,
MediaRequest, MediaRequest,
NoSeasonsAvailableError, NoSeasonsAvailableError,
@@ -243,6 +244,8 @@ requestRoutes.post<never, MediaRequest, MediaRequestBody>(
return next({ status: 409, message: error.message }); return next({ status: 409, message: error.message });
case NoSeasonsAvailableError: case NoSeasonsAvailableError:
return next({ status: 202, message: error.message }); return next({ status: 202, message: error.message });
case BlacklistedMediaError:
return next({ status: 403, message: error.message });
default: default:
return next({ status: 500, message: error.message }); return next({ status: 500, message: error.message });
} }

View File

@@ -746,11 +746,13 @@ settingsRoutes.get('/cache', async (_req, res) => {
})); }));
const tmdbImageCache = await ImageProxy.getImageStats('tmdb'); const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
const avatarImageCache = await ImageProxy.getImageStats('avatar');
return res.status(200).json({ return res.status(200).json({
apiCaches, apiCaches,
imageCache: { imageCache: {
tmdb: tmdbImageCache, tmdb: tmdbImageCache,
avatar: avatarImageCache,
}, },
}); });
}); });

View File

@@ -2,6 +2,7 @@ import JellyfinAPI from '@server/api/jellyfin';
import PlexTvAPI from '@server/api/plextv'; import PlexTvAPI from '@server/api/plextv';
import TautulliAPI from '@server/api/tautulli'; import TautulliAPI from '@server/api/tautulli';
import { MediaType } from '@server/constants/media'; import { MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import { UserType } from '@server/constants/user'; import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
@@ -550,7 +551,10 @@ router.post(
default: 'mm', default: 'mm',
size: 200, size: 200,
}), }),
userType: UserType.JELLYFIN, userType:
settings.main.mediaServerType === MediaServerType.JELLYFIN
? UserType.JELLYFIN
: UserType.EMBY,
}); });
await userRepository.save(newUser); await userRepository.save(newUser);

View File

@@ -1,3 +1,4 @@
import { ApiErrorCode } from '@server/constants/error';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User'; import { User } from '@server/entity/User';
import { UserSettings } from '@server/entity/UserSettings'; import { UserSettings } from '@server/entity/UserSettings';
@@ -9,6 +10,7 @@ import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth'; import { isAuthenticated } from '@server/middleware/auth';
import { ApiError } from '@server/types/error';
import { Router } from 'express'; import { Router } from 'express';
import { canMakePermissionsChange } from '.'; import { canMakePermissionsChange } from '.';
@@ -98,10 +100,18 @@ userSettingsRoutes.post<
} }
user.username = req.body.username; user.username = req.body.username;
const oldEmail = user.email;
if (user.jellyfinUsername) { if (user.jellyfinUsername) {
user.email = req.body.email || user.jellyfinUsername || user.email; user.email = req.body.email || user.jellyfinUsername || user.email;
} }
const existingUser = await userRepository.findOne({
where: { email: user.email },
});
if (oldEmail !== user.email && existingUser) {
throw new ApiError(400, ApiErrorCode.InvalidEmail);
}
// Update quota values only if the user has the correct permissions // Update quota values only if the user has the correct permissions
if ( if (
!user.hasPermission(Permission.MANAGE_USERS) && !user.hasPermission(Permission.MANAGE_USERS) &&
@@ -145,7 +155,14 @@ userSettingsRoutes.post<
email: savedUser.email, email: savedUser.email,
}); });
} catch (e) { } catch (e) {
next({ status: 500, message: e.message }); if (e.errorCode) {
return next({
status: e.statusCode,
message: e.errorCode,
});
} else {
return next({ status: 500, message: e.message });
}
} }
}); });

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg2"
viewBox="0 0 712.60077 712.5481"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
id="rect249"
width="712.60077"
height="712.5481"
x="-0.00071160076"
y="2.0223413e-11" />
<rect
style="fill:#ffffff"
id="rect289"
width="230.18982"
height="229.82355"
x="241.20476"
y="241.36227" />
<g
id="layer1"
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
<path
id="path3427"
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
style="fill:#52b54b;fill-opacity:1;stroke:none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,46 +1,131 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
<svg y="0px" width="100%" viewBox="0 0 617 188" enable-background="new 0 0 617 188" xml:space="preserve">
version="1.1" <path fill="#52B54B" opacity="1.000000" stroke="none" d="
id="svg2" M89.583336,1.000000
viewBox="0 0 712.60077 712.5481" C90.189529,1.685005 90.166168,2.574803 90.599510,3.025271
xmlns="http://www.w3.org/2000/svg" C103.718315,16.662701 116.882103,30.256845 129.948212,43.764053
xmlns:svg="http://www.w3.org/2000/svg" C130.577850,43.523941 130.916519,43.491173 131.111343,43.306595
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" C138.657471,36.157455 138.655273,36.156090 146.005478,43.505203
xmlns:cc="http://creativecommons.org/ns#" C159.538589,57.036308 173.016449,70.623535 186.654617,84.047913
xmlns:dc="http://purl.org/dc/elements/1.1/"> C189.264145,86.616562 189.414017,88.253456 186.716782,90.895164
<defs C174.709808,102.655037 162.893280,114.609337 151.008514,126.493958
id="defs4" /> C146.073502,131.428925 146.076691,131.427155 151.017944,136.523712
<metadata C151.698944,137.226120 152.340485,137.966812 153.259171,138.973434
id="metadata7"> C151.947098,140.380035 150.766312,141.712204 149.516266,142.975861
<rdf:RDF> C134.544815,158.110641 119.563087,173.235260 104.792023,188.681274
<cc:Work C103.611107,189.000000 102.222221,189.000000 100.624634,188.681274
rdf:about=""> C86.361732,174.796494 72.307518,161.230438 57.702755,147.132965
<dc:format>image/svg+xml</dc:format> C56.157101,149.136856 54.135899,151.757263 51.994804,154.533112
<dc:type C35.932781,138.457108 20.569420,123.048477 5.141897,107.704361
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> C3.997114,106.565773 2.391420,105.890610 1.000000,105.000000
</cc:Work> C1.000000,103.611107 1.000000,102.222221 1.318741,100.624641
</rdf:RDF> C15.203506,86.361694 28.769531,72.307434 42.867004,57.702602
</metadata> C40.863205,56.156994 38.242813,54.135792 35.425343,51.962570
<rect C51.518696,35.908516 66.939468,20.557360 82.295547,5.141749
style="opacity:0;fill:#ffffff;stroke-width:4.12543" C83.434830,3.998048 84.109390,2.391417 85.000000,0.999999
id="rect249" C86.388893,1.000000 87.777779,1.000000 89.583336,1.000000
width="712.60077" M73.196465,79.500702
height="712.5481" C73.196465,96.254150 73.196465,113.007599 73.196465,130.872055
x="-0.00071160076" C94.273178,118.764557 114.417175,107.192863 135.221664,95.241745
y="2.0223413e-11" /> C114.247169,83.251732 94.091187,71.729622 73.196594,59.785294
<rect C73.196594,66.631348 73.196594,72.566254 73.196465,79.500702
style="fill:#ffffff" z" />
id="rect289" <path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
width="230.18982" M618.000000,60.571537
height="229.82355" C617.004395,62.042580 615.613281,62.912964 615.073181,64.153824
x="241.20476" C608.143372,80.073746 601.328613,96.043816 594.498169,112.006920
y="241.36227" /> C586.973572,129.592300 579.343018,147.133865 571.999390,164.794601
<g C568.632385,172.892075 568.893372,173.002594 560.133972,172.999832
id="layer1" C555.470825,172.998367 550.807617,172.994385 546.144592,172.969360
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)"> C545.841980,172.967712 545.540466,172.775543 544.836609,172.534256
<path C548.592896,163.531219 551.714905,154.222061 556.286133,145.689255
id="path3427" C559.733765,139.253830 559.138794,134.062668 556.454224,127.695969
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z" C546.360352,103.757523 536.803345,79.592712 526.837830,55.000847
style="fill:#52b54b;fill-opacity:1;stroke:none" /> C534.817078,55.000847 542.437622,54.725182 550.003540,55.244331
</g> C551.436218,55.342628 553.169678,58.412052 553.885010,60.423309
C558.720520,74.018005 563.307556,87.700912 568.003784,101.345413
C569.107483,104.551987 570.321045,107.720764 571.976196,112.255157
C573.889587,107.365631 575.415283,103.375916 577.007935,99.413109
C582.693298,85.266724 588.344238,71.105591 594.218018,57.037624
C594.650513,56.001743 596.734497,55.132927 598.079773,55.089733
C604.401855,54.886726 610.734131,54.999401 617.531372,54.999699
C618.000000,56.714359 618.000000,58.428715 618.000000,60.571537
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M430.000122,99.002235
C430.000122,112.477097 430.000122,125.452438 430.000122,138.713440
C423.048126,138.713440 416.308685,138.713440 408.999878,138.713440
C408.999878,129.350739 409.120758,119.916939 408.962219,110.487823
C408.832153,102.753624 409.088898,94.909142 407.866791,87.324188
C406.440887,78.474220 401.302399,74.201607 394.304291,74.000290
C387.617249,73.807938 380.317963,79.297188 378.047363,86.438652
C377.420715,88.409592 377.055725,90.550858 377.044647,92.616508
C376.962494,107.913475 377.000122,123.211082 377.000122,138.753479
C369.630646,138.753479 362.559692,138.753479 354.999878,138.753479
C354.999878,123.256836 355.044769,107.816956 354.977661,92.377571
C354.951050,86.251518 352.748199,80.799278 347.911346,77.066116
C339.239685,70.373154 327.811401,74.635170 324.084412,84.471092
C322.793915,87.876816 322.147491,91.713402 322.090881,95.366882
C321.868958,109.685005 322.000122,124.008591 322.000122,138.665009
C314.823853,138.665009 307.760773,138.665009 300.346558,138.665009
C300.346558,111.006645 300.346558,83.281189 300.346558,55.001301
C306.163818,55.001301 312.104645,54.855133 318.024780,55.139343
C319.060455,55.189068 320.450378,56.891682 320.882477,58.112110
C321.380768,59.519447 320.998291,61.238617 320.998291,64.136040
C328.715179,54.407440 338.407898,52.804527 348.408875,54.206123
C356.403381,55.326527 361.770447,57.638248 366.682190,66.544373
C372.325470,62.972542 377.601440,58.269657 383.771973,56.014080
C396.273407,51.444298 408.602570,53.673611 419.067657,61.818150
C426.629364,67.703125 429.037811,76.770744 429.932556,86.011482
C430.332214,90.138710 430.000122,94.336792 430.000122,99.002235
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M462.000427,35.006332
C462.000427,44.815434 462.000427,54.126144 462.000427,64.132019
C468.844696,58.319965 476.100769,54.654530 484.669739,53.656227
C496.686127,52.256294 507.565582,54.979622 516.927185,62.503853
C534.236755,76.416115 535.360107,106.231667 523.651062,123.341644
C516.745056,133.433182 506.539673,139.485458 493.555267,140.111023
C483.836304,140.579254 474.670624,139.889420 466.610413,133.799713
C465.039795,132.613068 463.390686,131.530289 461.957214,130.525391
C461.633789,132.375305 461.105469,135.397171 460.522095,138.733841
C454.446686,138.733841 448.017822,138.733841 441.292542,138.733841
C441.292542,99.722672 441.292542,60.652122 441.292542,21.290209
C447.943787,21.290209 454.684204,21.290209 462.000427,21.290209
C462.000427,25.636984 462.000427,30.072460 462.000427,35.006332
M480.890228,119.974937
C485.426086,119.681152 490.365997,120.444260 494.421356,118.893707
C506.182587,114.396866 510.858643,104.919495 509.036591,92.234833
C507.422546,80.997993 496.539307,71.772278 483.551605,73.864754
C469.724976,76.092384 464.376770,85.538391 463.152863,96.752327
C462.120667,106.209480 469.961761,116.189537 480.890228,119.974937
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M234.797928,54.654831
C244.856339,52.605957 254.504562,52.040043 264.239868,54.923946
C279.600891,59.474377 286.402191,68.163963 289.768585,81.937614
C291.530579,89.146889 290.954620,96.927589 291.469940,105.005005
C269.550385,105.005005 248.375092,105.005005 227.094437,105.005005
C229.577957,116.288628 239.741562,120.764336 248.594757,121.034813
C256.790771,121.285217 264.390472,119.882645 271.081848,114.731178
C271.774902,114.197632 273.962708,114.659111 274.786041,115.402222
C278.726318,118.958458 282.435333,122.770882 286.509888,126.770363
C281.309174,132.968170 274.787445,135.946014 267.542938,138.175064
C253.746231,142.420120 240.209259,142.317459 227.237503,135.935410
C212.712891,128.789368 205.730453,116.523628 204.973831,100.473404
C204.537735,91.222557 205.503754,82.283119 210.008469,74.017265
C215.396210,64.131088 223.372589,57.511646 234.797928,54.654831
M266.971497,78.708908
C259.384399,70.789909 249.920425,70.480316 240.489548,73.410858
C234.405487,75.301414 229.437546,79.631561 227.800247,86.722244
C242.152313,86.722244 256.002747,86.722244 270.947815,86.722244
C269.410950,83.870155 268.228943,81.676651 266.971497,78.708908
z" />
<path fill="#FCFEFC" opacity="1.000000" stroke="none" d="
M73.196533,79.000931
C73.196594,72.566254 73.196594,66.631348 73.196594,59.785294
C94.091187,71.729622 114.247169,83.251732 135.221664,95.241745
C114.417175,107.192863 94.273178,118.764557 73.196465,130.872055
C73.196465,113.007599 73.196465,96.254150 73.196533,79.000931
z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,417 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import Header from '@app/components/Common/Header';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import useDebouncedState from '@app/hooks/useDebouncedState';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import {
ChevronLeftIcon,
ChevronRightIcon,
MagnifyingGlassIcon,
TrashIcon,
} from '@heroicons/react/24/solid';
import type {
BlacklistItem,
BlacklistResultsResponse,
} from '@server/interfaces/api/blacklistInterfaces';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
const messages = defineMessages('components.Blacklist', {
blacklistsettings: 'Blacklist Settings',
blacklistSettingsDescription: 'Manage blacklisted media.',
mediaName: 'Name',
mediaType: 'Type',
mediaTmdbId: 'tmdb Id',
blacklistdate: 'date',
blacklistedby: '{date} by {user}',
blacklistNotFoundError: '<strong>{title}</strong> is not blacklisted.',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined;
};
const Blacklist = () => {
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
const [searchFilter, debouncedSearchFilter, setSearchFilter] =
useDebouncedState('');
const router = useRouter();
const intl = useIntl();
const page = router.query.page ? Number(router.query.page) : 1;
const pageIndex = page - 1;
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
const {
data,
error,
mutate: revalidate,
} = useSWR<BlacklistResultsResponse>(
`/api/v1/blacklist/?take=${currentPageSize}
&skip=${pageIndex * currentPageSize}
${debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''}`,
{
refreshInterval: 0,
revalidateOnFocus: false,
}
);
// check if there's no data and no errors in the table
// so as to show a spinner inside the table and not refresh the whole component
if (!data && error) {
return <Error statusCode={500} />;
}
const searchItem = (e: ChangeEvent<HTMLInputElement>) => {
// Remove the "page" query param from the URL
// so that the "skip" query param on line 62 is empty
// and the search returns results without skipping items
if (router.query.page) router.replace(router.basePath);
setSearchFilter(e.target.value as string);
};
const hasNextPage = data && data.pageInfo.pages > pageIndex + 1;
const hasPrevPage = pageIndex > 0;
return (
<>
<PageTitle title={[intl.formatMessage(globalMessages.blacklist)]} />
<Header>{intl.formatMessage(globalMessages.blacklist)}</Header>
<div className="mt-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 md:flex-grow-0">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
<MagnifyingGlassIcon className="h-6 w-6" />
</span>
<input
type="text"
className="rounded-r-only"
value={searchFilter}
onChange={(e) => searchItem(e)}
/>
</div>
</div>
{!data ? (
<LoadingSpinner />
) : data.results.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center py-24 text-white">
<span className="text-2xl text-gray-400">
{intl.formatMessage(globalMessages.noresults)}
</span>
</div>
) : (
data.results.map((item: BlacklistItem) => {
return (
<div className="py-2" key={`request-list-${item.tmdbId}`}>
<BlacklistedItem item={item} revalidateList={revalidate} />
</div>
);
})
)}
<div className="actions">
<nav
className="mb-3 flex flex-col items-center space-y-3 sm:flex-row sm:space-y-0"
aria-label="Pagination"
>
<div className="hidden lg:flex lg:flex-1">
<p className="text-sm">
{data &&
(data?.results.length ?? 0) > 0 &&
intl.formatMessage(globalMessages.showingresults, {
from: pageIndex * currentPageSize + 1,
to:
data.results.length < currentPageSize
? pageIndex * currentPageSize + data.results.length
: (pageIndex + 1) * currentPageSize,
total: data.pageInfo.results,
strong: (msg: React.ReactNode) => (
<span className="font-medium">{msg}</span>
),
})}
</p>
</div>
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
<span className="-mt-3 items-center truncate text-sm sm:mt-0">
{intl.formatMessage(globalMessages.resultsperpage, {
pageSize: (
<select
id="pageSize"
name="pageSize"
onChange={(e) => {
setCurrentPageSize(Number(e.target.value));
router
.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
})
.then(() => window.scrollTo(0, 0));
}}
value={currentPageSize}
className="short inline"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
),
})}
</span>
</div>
<div className="flex flex-auto justify-center space-x-2 sm:flex-1 sm:justify-end">
<Button
disabled={!hasPrevPage}
onClick={() => updateQueryParams('page', (page - 1).toString())}
>
<ChevronLeftIcon />
<span>{intl.formatMessage(globalMessages.previous)}</span>
</Button>
<Button
disabled={!hasNextPage}
onClick={() => updateQueryParams('page', (page + 1).toString())}
>
<span>{intl.formatMessage(globalMessages.next)}</span>
<ChevronRightIcon />
</Button>
</div>
</nav>
</div>
</>
);
};
export default Blacklist;
interface BlacklistedItemProps {
item: BlacklistItem;
revalidateList: () => void;
}
const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const { addToast } = useToasts();
const { ref, inView } = useInView({
triggerOnce: true,
});
const intl = useIntl();
const { hasPermission } = useUser();
const url =
item.mediaType === 'movie'
? `/api/v1/movie/${item.tmdbId}`
: `/api/v1/tv/${item.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? url : null
);
if (!title && !error) {
return (
<div
className="h-64 w-full animate-pulse rounded-xl bg-gray-800 xl:h-28"
ref={ref}
/>
);
}
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);
const res = await fetch('/api/v1/blacklist/' + tmdbId, {
method: 'DELETE',
});
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
revalidateList();
setIsUpdating(false);
};
return (
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
{title && title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
fill
/>
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
}}
/>
</div>
)}
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
<Link
href={
item.mediaType === 'movie'
? `/movie/${item.tmdbId}`
: `/tv/${item.tmdbId}`
}
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
<CachedImage
src={
title?.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"
style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
width={600}
height={900}
/>
</Link>
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
{title &&
(isMovie(title)
? title.releaseDate
: title.firstAirDate
)?.slice(0, 4)}
</div>
<Link
href={
item.mediaType === 'movie'
? `/movie/${item.tmdbId}`
: `/tv/${item.tmdbId}`
}
>
<span className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
{title && (isMovie(title) ? title.title : title.name)}
</span>
</Link>
</div>
</div>
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
<div className="card-field">
<span className="card-field-name">Status</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)}
</Badge>
</div>
{item.createdAt && (
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(globalMessages.blacklisted)}
</span>
<span className="flex truncate text-sm text-gray-300">
{intl.formatMessage(messages.blacklistedby, {
date: (
<FormattedRelativeTime
value={Math.floor(
(new Date(item.createdAt).getTime() - Date.now()) / 1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
),
user: (
<Link href={`/users/${item.user.id}`}>
<span className="group flex items-center truncate">
<CachedImage
src={item.user.avatar}
alt=""
className="avatar-sm ml-1.5"
width={20}
height={20}
style={{ objectFit: 'cover' }}
/>
<span className="ml-1 truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{item.user.displayName}
</span>
</span>
</Link>
),
})}
</span>
</div>
)}
<div className="card-field">
{item.mediaType === 'movie' ? (
<div className="pointer-events-none z-40 self-start rounded-full border border-blue-500 bg-blue-600 bg-opacity-80 shadow-md">
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
{intl.formatMessage(globalMessages.movie)}
</div>
</div>
) : (
<div className="pointer-events-none z-40 self-start rounded-full border border-purple-600 bg-purple-600 bg-opacity-80 shadow-md">
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
{intl.formatMessage(globalMessages.tvshow)}
</div>
</div>
)}
</div>
</div>
</div>
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
{hasPermission(Permission.MANAGE_BLACKLIST) && (
<ConfirmButton
onClick={() =>
removeFromBlacklist(
item.tmdbId,
title && (isMovie(title) ? title.title : title.name)
)
}
confirmText={intl.formatMessage(
isUpdating ? globalMessages.deleting : globalMessages.areyousure
)}
className={`w-full ${
isUpdating ? 'pointer-events-none opacity-50' : ''
}`}
>
<TrashIcon />
<span>
{intl.formatMessage(globalMessages.removefromBlacklist)}
</span>
</ConfirmButton>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,129 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import Tooltip from '@app/components/Common/Tooltip';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid';
import type { Blacklist } from '@server/entity/Blacklist';
import Link from 'next/link';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages('component.BlacklistBlock', {
blacklistedby: 'Blacklisted By',
blacklistdate: 'Blacklisted date',
});
interface BlacklistBlockProps {
blacklistItem: Blacklist;
onUpdate?: () => void;
onDelete?: () => void;
}
const BlacklistBlock = ({
blacklistItem,
onUpdate,
onDelete,
}: BlacklistBlockProps) => {
const { user } = useUser();
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const { addToast } = useToasts();
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);
const res = await fetch('/api/v1/blacklist/' + tmdbId, {
method: 'DELETE',
});
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
onUpdate && onUpdate();
onDelete && onDelete();
setIsUpdating(false);
};
return (
<div className="px-4 py-3 text-gray-300">
<div className="flex items-center justify-between">
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
<div className="white mb-1 flex flex-nowrap">
<Tooltip content={intl.formatMessage(messages.blacklistedby)}>
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
</Tooltip>
<span className="w-40 truncate md:w-auto">
<Link
href={
blacklistItem.user.id === user?.id
? '/profile'
: `/users/${blacklistItem.user.id}`
}
>
<span className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
{blacklistItem.user.displayName}
</span>
</Link>
</span>
</div>
</div>
<div className="ml-2 flex flex-shrink-0 flex-wrap">
<Tooltip
content={intl.formatMessage(globalMessages.removefromBlacklist)}
>
<Button
buttonType="danger"
onClick={() =>
removeFromBlacklist(blacklistItem.tmdbId, blacklistItem.title)
}
disabled={isUpdating}
>
<TrashIcon className="icon-sm" />
</Button>
</Tooltip>
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<div className="mr-6 flex items-center text-sm leading-5">
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)}
</Badge>
</div>
</div>
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
<Tooltip content={intl.formatMessage(messages.blacklistdate)}>
<CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<span>
{intl.formatDate(blacklistItem.createdAt, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
</div>
</div>
</div>
);
};
export default BlacklistBlock;

View File

@@ -0,0 +1,79 @@
import Modal from '@app/components/Common/Modal';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import { useIntl } from 'react-intl';
import useSWR from 'swr';
interface BlacklistModalProps {
tmdbId: number;
type: 'movie' | 'tv' | 'collection';
show: boolean;
onComplete?: () => void;
onCancel?: () => void;
isUpdating?: boolean;
}
const messages = defineMessages('component.BlacklistModal', {
blacklisting: 'Blacklisting',
});
const isMovie = (
movie: MovieDetails | TvDetails | undefined
): movie is MovieDetails => {
if (!movie) return false;
return (movie as MovieDetails).title !== undefined;
};
const BlacklistModal = ({
tmdbId,
type,
show,
onComplete,
onCancel,
isUpdating,
}: BlacklistModalProps) => {
const intl = useIntl();
const { data, error } = useSWR<TvDetails | MovieDetails>(
`/api/v1/${type}/${tmdbId}`
);
return (
<Transition
as="div"
enter="transition-opacity duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={show}
>
<Modal
loading={!data && !error}
backgroundClickable
title={`${intl.formatMessage(globalMessages.blacklist)} ${
isMovie(data)
? intl.formatMessage(globalMessages.movie)
: intl.formatMessage(globalMessages.tvshow)
}`}
subTitle={`${isMovie(data) ? data.title : data?.name}`}
onCancel={onCancel}
onOk={onComplete}
okText={
isUpdating
? intl.formatMessage(messages.blacklisting)
: intl.formatMessage(globalMessages.blacklist)
}
okButtonType="danger"
okDisabled={isUpdating}
backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`}
/>
</Transition>
);
};
export default BlacklistModal;

View File

@@ -183,6 +183,11 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
); );
} }
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
{ type: 'or' }
);
return ( return (
<div <div
className="media-page" className="media-page"
@@ -335,20 +340,26 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
sliderKey="collection-movies" sliderKey="collection-movies"
isLoading={false} isLoading={false}
isEmpty={data.parts.length === 0} isEmpty={data.parts.length === 0}
items={data.parts.map((title) => ( items={data.parts
<TitleCard .filter((title) => {
key={`collection-movie-${title.id}`} if (!blacklistVisibility)
id={title.id} return title.mediaInfo?.status !== MediaStatus.BLACKLISTED;
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0} return title;
image={title.posterPath} })
status={title.mediaInfo?.status} .map((title) => (
summary={title.overview} <TitleCard
title={title.title} key={`collection-movie-${title.id}`}
userScore={title.voteAverage} id={title.id}
year={title.releaseDate} isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
mediaType={title.mediaType} image={title.posterPath}
/> status={title.mediaInfo?.status}
))} summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/> />
<div className="extra-bottom-space relative" /> <div className="extra-bottom-space relative" />
</div> </div>

View File

@@ -16,8 +16,11 @@ const CachedImage = ({ src, ...props }: ImageProps) => {
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) { if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
const parsedUrl = new URL(imageUrl); const parsedUrl = new URL(imageUrl);
if (parsedUrl.host === 'image.tmdb.org' && currentSettings.cacheImages) { if (parsedUrl.host === 'image.tmdb.org') {
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy'); if (currentSettings.cacheImages)
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
} else if (parsedUrl.host !== 'gravatar.com') {
imageUrl = '/avatarproxy/' + imageUrl;
} }
} }

View File

@@ -1,8 +1,10 @@
import PersonCard from '@app/components/PersonCard'; import PersonCard from '@app/components/PersonCard';
import TitleCard from '@app/components/TitleCard'; import TitleCard from '@app/components/TitleCard';
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
import { Permission, useUser } from '@app/hooks/useUser';
import useVerticalScroll from '@app/hooks/useVerticalScroll'; import useVerticalScroll from '@app/hooks/useVerticalScroll';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import { MediaStatus } from '@server/constants/media';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import type { import type {
CollectionResult, CollectionResult,
@@ -32,7 +34,14 @@ const ListView = ({
mutateParent, mutateParent,
}: ListViewProps) => { }: ListViewProps) => {
const intl = useIntl(); const intl = useIntl();
const { hasPermission } = useUser();
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd); useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
{ type: 'or' }
);
return ( return (
<> <>
{isEmpty && ( {isEmpty && (
@@ -55,76 +64,89 @@ const ListView = ({
</li> </li>
); );
})} })}
{items?.map((title, index) => { {items
let titleCard: React.ReactNode; ?.filter((title) => {
if (!blacklistVisibility)
return (
(title as TvResult | MovieResult).mediaInfo?.status !==
MediaStatus.BLACKLISTED
);
return title;
})
.map((title, index) => {
let titleCard: React.ReactNode;
switch (title.mediaType) { switch (title.mediaType) {
case 'movie': case 'movie':
titleCard = ( titleCard = (
<TitleCard <TitleCard
key={title.id} key={title.id}
id={title.id} id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0} isAddedToWatchlist={
image={title.posterPath} title.mediaInfo?.watchlists?.length ?? 0
status={title.mediaInfo?.status} }
summary={title.overview} image={title.posterPath}
title={title.title} status={title.mediaInfo?.status}
userScore={title.voteAverage} summary={title.overview}
year={title.releaseDate} title={title.title}
mediaType={title.mediaType} userScore={title.voteAverage}
inProgress={ year={title.releaseDate}
(title.mediaInfo?.downloadStatus ?? []).length > 0 mediaType={title.mediaType}
} inProgress={
canExpand (title.mediaInfo?.downloadStatus ?? []).length > 0
/> }
); canExpand
break; />
case 'tv': );
titleCard = ( break;
<TitleCard case 'tv':
key={title.id} titleCard = (
id={title.id} <TitleCard
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0} key={title.id}
image={title.posterPath} id={title.id}
status={title.mediaInfo?.status} isAddedToWatchlist={
summary={title.overview} title.mediaInfo?.watchlists?.length ?? 0
title={title.name} }
userScore={title.voteAverage} image={title.posterPath}
year={title.firstAirDate} status={title.mediaInfo?.status}
mediaType={title.mediaType} summary={title.overview}
inProgress={ title={title.name}
(title.mediaInfo?.downloadStatus ?? []).length > 0 userScore={title.voteAverage}
} year={title.firstAirDate}
canExpand mediaType={title.mediaType}
/> inProgress={
); (title.mediaInfo?.downloadStatus ?? []).length > 0
break; }
case 'collection': canExpand
titleCard = ( />
<TitleCard );
id={title.id} break;
image={title.posterPath} case 'collection':
summary={title.overview} titleCard = (
title={title.title} <TitleCard
mediaType={title.mediaType} id={title.id}
canExpand image={title.posterPath}
/> summary={title.overview}
); title={title.title}
break; mediaType={title.mediaType}
case 'person': canExpand
titleCard = ( />
<PersonCard );
personId={title.id} break;
name={title.name} case 'person':
profilePath={title.profilePath} titleCard = (
canExpand <PersonCard
/> personId={title.id}
); name={title.name}
break; profilePath={title.profilePath}
} canExpand
/>
);
break;
}
return <li key={`${title.id}-${index}`}>{titleCard}</li>; return <li key={`${title.id}-${index}`}>{titleCard}</li>;
})} })}
{isLoading && {isLoading &&
!isReachingEnd && !isReachingEnd &&
[...Array(20)].map((_item, i) => ( [...Array(20)].map((_item, i) => (

View File

@@ -1,6 +1,11 @@
import Spinner from '@app/assets/spinner.svg'; import Spinner from '@app/assets/spinner.svg';
import { CheckCircleIcon } from '@heroicons/react/20/solid'; import { CheckCircleIcon } from '@heroicons/react/20/solid';
import { BellIcon, ClockIcon, MinusSmallIcon } from '@heroicons/react/24/solid'; import {
BellIcon,
ClockIcon,
EyeSlashIcon,
MinusSmallIcon,
} from '@heroicons/react/24/solid';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
interface StatusBadgeMiniProps { interface StatusBadgeMiniProps {
@@ -44,6 +49,10 @@ const StatusBadgeMini = ({
); );
indicatorIcon = <BellIcon />; indicatorIcon = <BellIcon />;
break; break;
case MediaStatus.BLACKLISTED:
badgeStyle.push('bg-red-500 border-white-400 ring-white-400 text-white');
indicatorIcon = <EyeSlashIcon />;
break;
case MediaStatus.PARTIALLY_AVAILABLE: case MediaStatus.PARTIALLY_AVAILABLE:
badgeStyle.push( badgeStyle.push(
'bg-green-500 border-green-400 ring-green-400 text-green-100' 'bg-green-500 border-green-400 ring-green-400 text-green-100'

View File

@@ -8,6 +8,7 @@ import {
CompanySelector, CompanySelector,
GenreSelector, GenreSelector,
KeywordSelector, KeywordSelector,
StatusSelector,
WatchProviderSelector, WatchProviderSelector,
} from '@app/components/Selector'; } from '@app/components/Selector';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
@@ -40,6 +41,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
runtime: 'Runtime', runtime: 'Runtime',
streamingservices: 'Streaming Services', streamingservices: 'Streaming Services',
voteCount: 'Number of votes between {minValue} and {maxValue}', voteCount: 'Number of votes between {minValue} and {maxValue}',
status: 'Status',
}); });
type FilterSlideoverProps = { type FilterSlideoverProps = {
@@ -150,6 +152,23 @@ const FilterSlideover = ({
updateQueryParams('genre', value?.map((v) => v.value).join(',')); updateQueryParams('genre', value?.map((v) => v.value).join(','));
}} }}
/> />
{type === 'tv' && (
<>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.status)}
</span>
<StatusSelector
defaultValue={currentFilters.status}
isMulti
onChange={(value) => {
updateQueryParams(
'status',
value?.map((v) => v.value).join('|')
);
}}
/>
</>
)}
<span className="text-lg font-semibold"> <span className="text-lg font-semibold">
{intl.formatMessage(messages.keywords)} {intl.formatMessage(messages.keywords)}
</span> </span>

View File

@@ -108,6 +108,7 @@ export const QueryFilterOptions = z.object({
voteCountGte: z.string().optional(), voteCountGte: z.string().optional(),
watchRegion: z.string().optional(), watchRegion: z.string().optional(),
watchProviders: z.string().optional(), watchProviders: z.string().optional(),
status: z.string().optional(),
}); });
export type FilterOptions = z.infer<typeof QueryFilterOptions>; export type FilterOptions = z.infer<typeof QueryFilterOptions>;
@@ -147,6 +148,10 @@ export const prepareFilterValues = (
filterValues.genre = values.genre; filterValues.genre = values.genre;
} }
if (values.status) {
filterValues.status = values.status;
}
if (values.keywords) { if (values.keywords) {
filterValues.keywords = values.keywords; filterValues.keywords = values.keywords;
} }

View File

@@ -11,7 +11,6 @@ import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { MediaType } from '@server/constants/media'; import { MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
interface ExternalLinkBlockProps { interface ExternalLinkBlockProps {
mediaType: 'movie' | 'tv'; mediaType: 'movie' | 'tv';
@@ -31,7 +30,6 @@ const ExternalLinkBlock = ({
mediaUrl, mediaUrl,
}: ExternalLinkBlockProps) => { }: ExternalLinkBlockProps) => {
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { locale } = useLocale(); const { locale } = useLocale();
return ( return (
@@ -45,7 +43,8 @@ const ExternalLinkBlock = ({
> >
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? ( {settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
<PlexLogo /> <PlexLogo />
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( ) : settings.currentSettings.mediaServerType ===
MediaServerType.EMBY ? (
<EmbyLogo /> <EmbyLogo />
) : ( ) : (
<JellyfinLogo /> <JellyfinLogo />

View File

@@ -1,4 +1,5 @@
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Modal from '@app/components/Common/Modal'; import Modal from '@app/components/Common/Modal';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
@@ -6,7 +7,6 @@ import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import type { default as IssueCommentType } from '@server/entity/IssueComment'; import type { default as IssueCommentType } from '@server/entity/IssueComment';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
@@ -88,8 +88,8 @@ const IssueComment = ({
</Modal> </Modal>
</Transition> </Transition>
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}> <Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
<Image <CachedImage
src={comment.user.avatar} src={`${comment.user.avatar}`}
alt="" alt=""
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105" className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
width={40} width={40}

View File

@@ -28,8 +28,6 @@ import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
@@ -108,7 +106,6 @@ const IssueDetails = () => {
(opt) => opt.issueType === issueData?.issueType (opt) => opt.issueType === issueData?.issueType
); );
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) { if (!data && !error) {
return <LoadingSpinner />; return <LoadingSpinner />;
@@ -289,10 +286,10 @@ const IssueDetails = () => {
} }
className="group ml-1 inline-flex h-full items-center xl:ml-1.5" className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
> >
<Image <CachedImage
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6" src={`${issueData.createdBy.avatar}`}
src={issueData.createdBy.avatar}
alt="" alt=""
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
width={20} width={20}
height={20} height={20}
/> />
@@ -390,7 +387,8 @@ const IssueDetails = () => {
> >
<PlayIcon /> <PlayIcon />
<span> <span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.playonplex, { ? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
@@ -437,7 +435,8 @@ const IssueDetails = () => {
> >
<PlayIcon /> <PlayIcon />
<span> <span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.play4konplex, { ? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
@@ -662,7 +661,8 @@ const IssueDetails = () => {
> >
<PlayIcon /> <PlayIcon />
<span> <span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.playonplex, { ? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
@@ -708,7 +708,8 @@ const IssueDetails = () => {
> >
<PlayIcon /> <PlayIcon />
<span> <span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.play4konplex, { ? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })

View File

@@ -11,7 +11,6 @@ import { MediaType } from '@server/constants/media';
import type Issue from '@server/entity/Issue'; import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
@@ -226,8 +225,8 @@ const IssueItem = ({ issue }: IssueItemProps) => {
href={`/users/${issue.createdBy.id}`} href={`/users/${issue.createdBy.id}`}
className="group flex items-center truncate" className="group flex items-center truncate"
> >
<Image <CachedImage
src={issue.createdBy.avatar} src={'/avatarproxy/' + issue.createdBy.avatar}
alt="" alt=""
className="avatar-sm ml-1.5 object-cover" className="avatar-sm ml-1.5 object-cover"
width={20} width={20}

View File

@@ -7,6 +7,7 @@ import {
CogIcon, CogIcon,
EllipsisHorizontalIcon, EllipsisHorizontalIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
EyeSlashIcon,
FilmIcon, FilmIcon,
SparklesIcon, SparklesIcon,
TvIcon, TvIcon,
@@ -16,6 +17,7 @@ import {
ClockIcon as FilledClockIcon, ClockIcon as FilledClockIcon,
CogIcon as FilledCogIcon, CogIcon as FilledCogIcon,
ExclamationTriangleIcon as FilledExclamationTriangleIcon, ExclamationTriangleIcon as FilledExclamationTriangleIcon,
EyeSlashIcon as FilledEyeSlashIcon,
FilmIcon as FilledFilmIcon, FilmIcon as FilledFilmIcon,
SparklesIcon as FilledSparklesIcon, SparklesIcon as FilledSparklesIcon,
TvIcon as FilledTvIcon, TvIcon as FilledTvIcon,
@@ -84,6 +86,18 @@ const MobileMenu = () => {
svgIconSelected: <FilledClockIcon className="h-6 w-6" />, svgIconSelected: <FilledClockIcon className="h-6 w-6" />,
activeRegExp: /^\/requests/, activeRegExp: /^\/requests/,
}, },
{
href: '/blacklist',
content: intl.formatMessage(menuMessages.blacklist),
svgIcon: <EyeSlashIcon className="h-6 w-6" />,
svgIconSelected: <FilledEyeSlashIcon className="h-6 w-6" />,
activeRegExp: /^\/blacklist/,
requiredPermission: [
Permission.MANAGE_BLACKLIST,
Permission.VIEW_BLACKLIST,
],
permissionType: 'or',
},
{ {
href: '/issues', href: '/issues',
content: intl.formatMessage(menuMessages.issues), content: intl.formatMessage(menuMessages.issues),

View File

@@ -8,6 +8,7 @@ import {
ClockIcon, ClockIcon,
CogIcon, CogIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
EyeSlashIcon,
FilmIcon, FilmIcon,
SparklesIcon, SparklesIcon,
TvIcon, TvIcon,
@@ -25,6 +26,7 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', {
browsemovies: 'Movies', browsemovies: 'Movies',
browsetv: 'Series', browsetv: 'Series',
requests: 'Requests', requests: 'Requests',
blacklist: 'Blacklist',
issues: 'Issues', issues: 'Issues',
users: 'Users', users: 'Users',
settings: 'Settings', settings: 'Settings',
@@ -71,6 +73,17 @@ const SidebarLinks: SidebarLinkProps[] = [
svgIcon: <ClockIcon className="mr-3 h-6 w-6" />, svgIcon: <ClockIcon className="mr-3 h-6 w-6" />,
activeRegExp: /^\/requests/, activeRegExp: /^\/requests/,
}, },
{
href: '/blacklist',
messagesKey: 'blacklist',
svgIcon: <EyeSlashIcon className="mr-3 h-6 w-6" />,
activeRegExp: /^\/blacklist/,
requiredPermission: [
Permission.MANAGE_BLACKLIST,
Permission.VIEW_BLACKLIST,
],
permissionType: 'or',
},
{ {
href: '/issues', href: '/issues',
messagesKey: 'issues', messagesKey: 'issues',

View File

@@ -1,3 +1,4 @@
import CachedImage from '@app/components/Common/CachedImage';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay'; import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
@@ -7,7 +8,6 @@ import {
ClockIcon, ClockIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid'; import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import Image from 'next/image';
import type { LinkProps } from 'next/link'; import type { LinkProps } from 'next/link';
import Link from 'next/link'; import Link from 'next/link';
import { forwardRef, Fragment } from 'react'; import { forwardRef, Fragment } from 'react';
@@ -56,9 +56,9 @@ const UserDropdown = () => {
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500" className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
data-testid="user-menu" data-testid="user-menu"
> >
<Image <CachedImage
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10" className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar || ''} src={user ? user.avatar : ''}
alt="" alt=""
width={40} width={40}
height={40} height={40}
@@ -79,9 +79,9 @@ const UserDropdown = () => {
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur"> <div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
<div className="flex flex-col space-y-4 px-4 py-4"> <div className="flex flex-col space-y-4 px-4 py-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Image <CachedImage
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10" className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar || ''} src={user ? user.avatar : ''}
alt="" alt=""
width={40} width={40}
height={40} height={40}

View File

@@ -4,9 +4,9 @@ import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { InformationCircleIcon } from '@heroicons/react/24/solid'; import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType, ServerType } from '@server/constants/server';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config'; import { FormattedMessage, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -26,6 +26,7 @@ const messages = defineMessages('components.Login', {
validationemailformat: 'Valid email required', validationemailformat: 'Valid email required',
validationusernamerequired: 'Username required', validationusernamerequired: 'Username required',
validationpasswordrequired: 'Password required', validationpasswordrequired: 'Password required',
validationservertyperequired: 'Please select a server type',
validationHostnameRequired: 'You must provide a valid hostname or IP address', validationHostnameRequired: 'You must provide a valid hostname or IP address',
validationPortRequired: 'You must provide a valid port number', validationPortRequired: 'You must provide a valid port number',
validationUrlTrailingSlash: 'URL must not end in a trailing slash', validationUrlTrailingSlash: 'URL must not end in a trailing slash',
@@ -40,42 +41,51 @@ const messages = defineMessages('components.Login', {
initialsigningin: 'Connecting…', initialsigningin: 'Connecting…',
initialsignin: 'Connect', initialsignin: 'Connect',
forgotpassword: 'Forgot Password?', forgotpassword: 'Forgot Password?',
servertype: 'Server Type',
back: 'Go back',
}); });
interface JellyfinLoginProps { interface JellyfinLoginProps {
revalidate: () => void; revalidate: () => void;
initial?: boolean; initial?: boolean;
serverType?: MediaServerType;
onCancel?: () => void;
} }
const JellyfinLogin: React.FC<JellyfinLoginProps> = ({ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
revalidate, revalidate,
initial, initial,
serverType,
onCancel,
}) => { }) => {
const toasts = useToasts(); const toasts = useToasts();
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const mediaServerFormatValues = {
mediaServerName:
serverType === MediaServerType.JELLYFIN
? ServerType.JELLYFIN
: serverType === MediaServerType.EMBY
? ServerType.EMBY
: 'Media Server',
};
if (initial) { if (initial) {
const LoginSchema = Yup.object().shape({ const LoginSchema = Yup.object().shape({
hostname: Yup.string().required( hostname: Yup.string().required(
intl.formatMessage(messages.validationhostrequired, { intl.formatMessage(
mediaServerName: messages.validationhostrequired,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', mediaServerFormatValues
}) )
), ),
port: Yup.number().required( port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired) intl.formatMessage(messages.validationPortRequired)
), ),
urlBase: Yup.string() urlBase: Yup.string().matches(
.matches( /^(.*[^/])$/,
/^(\/[^/].*[^/]$)/, intl.formatMessage(messages.validationUrlBaseTrailingSlash)
intl.formatMessage(messages.validationUrlBaseLeadingSlash) ),
)
.matches(
/^(.*[^/])$/,
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
),
email: Yup.string() email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat)) .email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)), .required(intl.formatMessage(messages.validationemailrequired)),
@@ -85,11 +95,6 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
password: Yup.string(), password: Yup.string(),
}); });
const mediaServerFormatValues = {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
};
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
@@ -104,6 +109,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validationSchema={LoginSchema} validationSchema={LoginSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
// Check if serverType is either 'Jellyfin' or 'Emby'
// if (serverType !== 'Jellyfin' && serverType !== 'Emby') {
// throw new Error('Invalid serverType'); // You can customize the error message
// }
const res = await fetch('/api/v1/auth/jellyfin', { const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -117,6 +127,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
useSsl: values.useSsl, useSsl: values.useSsl,
urlBase: values.urlBase, urlBase: values.urlBase,
email: values.email, email: values.email,
serverType: serverType,
}), }),
}); });
if (!res.ok) throw new Error(res.statusText, { cause: res }); if (!res.ok) throw new Error(res.statusText, { cause: res });
@@ -312,7 +323,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
</div> </div>
</div> </div>
<div className="mt-8 border-t border-gray-700 pt-5"> <div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-end"> <div className="flex flex-row-reverse justify-between">
<span className="inline-flex rounded-md shadow-sm"> <span className="inline-flex rounded-md shadow-sm">
<Button <Button
buttonType="primary" buttonType="primary"
@@ -324,6 +335,13 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
: intl.formatMessage(messages.signin)} : intl.formatMessage(messages.signin)}
</Button> </Button>
</span> </span>
{onCancel && (
<span className="inline-flex rounded-md shadow-sm">
<Button buttonType="default" onClick={() => onCancel()}>
<FormattedMessage {...messages.back} />
</Button>
</span>
)}
</div> </div>
</div> </div>
</Form> </Form>
@@ -429,7 +447,8 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
jellyfinForgotPasswordUrl jellyfinForgotPasswordUrl
? `${jellyfinForgotPasswordUrl}` ? `${jellyfinForgotPasswordUrl}`
: `${baseUrl}/web/index.html#!/${ : `${baseUrl}/web/index.html#!/${
process.env.JELLYFIN_TYPE === 'emby' settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'startup/' ? 'startup/'
: '' : ''
}forgotpassword.html` }forgotpassword.html`

View File

@@ -10,7 +10,6 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid'; import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { useRouter } from 'next/dist/client/router'; import { useRouter } from 'next/dist/client/router';
import Image from 'next/image'; import Image from 'next/image';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -34,7 +33,6 @@ const Login = () => {
const { user, revalidate } = useUser(); const { user, revalidate } = useUser();
const router = useRouter(); const router = useRouter();
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth // Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to sign in. If we get a success message, we will // We take the token and attempt to sign in. If we get a success message, we will
@@ -88,6 +86,15 @@ const Login = () => {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return ( return (
<div className="relative flex min-h-screen flex-col bg-gray-900 py-14"> <div className="relative flex min-h-screen flex-col bg-gray-900 py-14">
<PageTitle title={intl.formatMessage(messages.signin)} /> <PageTitle title={intl.formatMessage(messages.signin)} />
@@ -154,12 +161,10 @@ const Login = () => {
{settings.currentSettings.mediaServerType == {settings.currentSettings.mediaServerType ==
MediaServerType.PLEX MediaServerType.PLEX
? intl.formatMessage(messages.signinwithplex) ? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(messages.signinwithjellyfin, { : intl.formatMessage(
mediaServerName: messages.signinwithjellyfin,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' mediaServerFormatValues
? 'Emby' )}
: 'Jellyfin',
})}
</button> </button>
<AccordionContent isOpen={openIndexes.includes(0)}> <AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8"> <div className="px-10 py-8">

View File

@@ -1,4 +1,6 @@
import BlacklistBlock from '@app/components/BlacklistBlock';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton'; import ConfirmButton from '@app/components/Common/ConfirmButton';
import SlideOver from '@app/components/Common/SlideOver'; import SlideOver from '@app/components/Common/SlideOver';
import Tooltip from '@app/components/Common/Tooltip'; import Tooltip from '@app/components/Common/Tooltip';
@@ -26,8 +28,6 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -95,7 +95,6 @@ const ManageSlideOver = ({
const { user: currentUser, hasPermission } = useUser(); const { user: currentUser, hasPermission } = useUser();
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { data: watchData } = useSWR<MediaWatchDataResponse>( const { data: watchData } = useSWR<MediaWatchDataResponse>(
settings.currentSettings.mediaServerType === MediaServerType.PLEX && settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
data.mediaInfo && data.mediaInfo &&
@@ -286,6 +285,20 @@ const ManageSlideOver = ({
</div> </div>
</div> </div>
)} )}
{data.mediaInfo?.status === MediaStatus.BLACKLISTED && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(globalMessages.blacklist)}
</h3>
<div className="overflow-hidden rounded-md border border-gray-700 shadow">
<BlacklistBlock
blacklistItem={data.mediaInfo.blacklist}
onUpdate={() => revalidate()}
onDelete={() => onClose()}
/>
</div>
</div>
)}
{hasPermission(Permission.ADMIN) && {hasPermission(Permission.ADMIN) &&
(data.mediaInfo?.serviceUrl || (data.mediaInfo?.serviceUrl ||
data.mediaInfo?.tautulliUrl || data.mediaInfo?.tautulliUrl ||
@@ -355,7 +368,7 @@ const ManageSlideOver = ({
key={`watch-user-${user.id}`} key={`watch-user-${user.id}`}
content={user.displayName} content={user.displayName}
> >
<Image <CachedImage
src={user.avatar} src={user.avatar}
alt={user.displayName} alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105" className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
@@ -516,7 +529,7 @@ const ManageSlideOver = ({
key={`watch-user-${user.id}`} key={`watch-user-${user.id}`}
content={user.displayName} content={user.displayName}
> >
<Image <CachedImage
src={user.avatar} src={user.avatar}
alt={user.displayName} alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105" className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
@@ -605,32 +618,17 @@ const ManageSlideOver = ({
</div> </div>
</div> </div>
)} )}
{hasPermission(Permission.ADMIN) && data?.mediaInfo && ( {hasPermission(Permission.ADMIN) &&
<div> data?.mediaInfo &&
<h3 className="mb-2 text-xl font-bold"> data.mediaInfo.status !== MediaStatus.BLACKLISTED && (
{intl.formatMessage(messages.manageModalAdvanced)} <div>
</h3> <h3 className="mb-2 text-xl font-bold">
<div className="space-y-2"> {intl.formatMessage(messages.manageModalAdvanced)}
{data?.mediaInfo.status !== MediaStatus.AVAILABLE && ( </h3>
<Button <div className="space-y-2">
onClick={() => markAvailable()} {data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
className="w-full"
buttonType="success"
>
<CheckCircleIcon />
<span>
{intl.formatMessage(
mediaType === 'movie'
? messages.markavailable
: messages.markallseasonsavailable
)}
</span>
</Button>
)}
{data?.mediaInfo.status4k !== MediaStatus.AVAILABLE &&
settings.currentSettings.series4kEnabled && (
<Button <Button
onClick={() => markAvailable(true)} onClick={() => markAvailable()}
className="w-full" className="w-full"
buttonType="success" buttonType="success"
> >
@@ -638,41 +636,59 @@ const ManageSlideOver = ({
<span> <span>
{intl.formatMessage( {intl.formatMessage(
mediaType === 'movie' mediaType === 'movie'
? messages.mark4kavailable ? messages.markavailable
: messages.markallseasons4kavailable : messages.markallseasonsavailable
)} )}
</span> </span>
</Button> </Button>
)} )}
<div> {data?.mediaInfo.status4k !== MediaStatus.AVAILABLE &&
<ConfirmButton settings.currentSettings.series4kEnabled && (
onClick={() => deleteMedia()} <Button
confirmText={intl.formatMessage(globalMessages.areyousure)} onClick={() => markAvailable(true)}
className="w-full" className="w-full"
> buttonType="success"
<DocumentMinusIcon /> >
<span> <CheckCircleIcon />
{intl.formatMessage(messages.manageModalClearMedia)} <span>
</span> {intl.formatMessage(
</ConfirmButton> mediaType === 'movie'
<div className="mt-2 text-xs text-gray-400"> ? messages.mark4kavailable
{intl.formatMessage(messages.manageModalClearMediaWarning, { : messages.markallseasons4kavailable
mediaType: intl.formatMessage( )}
mediaType === 'movie' ? messages.movie : messages.tvshow </span>
), </Button>
mediaServerName: )}
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' <div>
? 'Emby' <ConfirmButton
: settings.currentSettings.mediaServerType === onClick={() => deleteMedia()}
MediaServerType.PLEX confirmText={intl.formatMessage(globalMessages.areyousure)}
? 'Plex' className="w-full"
: 'Jellyfin', >
})} <DocumentMinusIcon />
<span>
{intl.formatMessage(messages.manageModalClearMedia)}
</span>
</ConfirmButton>
<div className="mt-2 text-xs text-gray-400">
{intl.formatMessage(messages.manageModalClearMediaWarning, {
mediaType: intl.formatMessage(
mediaType === 'movie' ? messages.movie : messages.tvshow
),
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> )}
)}
</div> </div>
</SlideOver> </SlideOver>
); );

View File

@@ -3,8 +3,10 @@ import PersonCard from '@app/components/PersonCard';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import TitleCard from '@app/components/TitleCard'; import TitleCard from '@app/components/TitleCard';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline'; import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
import { Permission } from '@server/lib/permissions';
import type { import type {
MovieResult, MovieResult,
PersonResult, PersonResult,
@@ -41,6 +43,7 @@ const MediaSlider = ({
onNewTitles, onNewTitles,
}: MediaSliderProps) => { }: MediaSliderProps) => {
const settings = useSettings(); const settings = useSettings();
const { hasPermission } = useUser();
const { data, error, setSize, size } = useSWRInfinite<MixedResult>( const { data, error, setSize, size } = useSWRInfinite<MixedResult>(
(pageIndex: number, previousPageData: MixedResult | null) => { (pageIndex: number, previousPageData: MixedResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
@@ -90,50 +93,65 @@ const MediaSlider = ({
return null; return null;
} }
const finalTitles = titles.slice(0, 20).map((title) => { const blacklistVisibility = hasPermission(
switch (title.mediaType) { [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
case 'movie': { type: 'or' }
);
const finalTitles = titles
.slice(0, 20)
.filter((title) => {
if (!blacklistVisibility)
return ( return (
<TitleCard (title as TvResult | MovieResult).mediaInfo?.status !==
key={title.id} MediaStatus.BLACKLISTED
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
/>
); );
case 'tv': return title;
return ( })
<TitleCard .map((title) => {
key={title.id} switch (title.mediaType) {
id={title.id} case 'movie':
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0} return (
image={title.posterPath} <TitleCard
status={title.mediaInfo?.status} key={title.id}
summary={title.overview} id={title.id}
title={title.name} isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
userScore={title.voteAverage} image={title.posterPath}
year={title.firstAirDate} status={title.mediaInfo?.status}
mediaType={title.mediaType} summary={title.overview}
inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0} title={title.title}
/> userScore={title.voteAverage}
); year={title.releaseDate}
case 'person': mediaType={title.mediaType}
return ( inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
<PersonCard />
personId={title.id} );
name={title.name} case 'tv':
profilePath={title.profilePath} return (
/> <TitleCard
); key={title.id}
} id={title.id}
}); isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
/>
);
case 'person':
return (
<PersonCard
personId={title.id}
name={title.name}
profilePath={title.profilePath}
/>
);
}
});
if (linkUrl && titles.length > 20) { if (linkUrl && titles.length > 20) {
finalTitles.push( finalTitles.push(

View File

@@ -5,6 +5,7 @@ import RTRotten from '@app/assets/rt_rotten.svg';
import ImdbLogo from '@app/assets/services/imdb.svg'; import ImdbLogo from '@app/assets/services/imdb.svg';
import Spinner from '@app/assets/spinner.svg'; import Spinner from '@app/assets/spinner.svg';
import TmdbLogo from '@app/assets/tmdb_logo.svg'; import TmdbLogo from '@app/assets/tmdb_logo.svg';
import BlacklistModal from '@app/components/BlacklistModal';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import LoadingSpinner from '@app/components/Common/LoadingSpinner';
@@ -35,6 +36,7 @@ import {
CloudIcon, CloudIcon,
CogIcon, CogIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
EyeSlashIcon,
FilmIcon, FilmIcon,
PlayIcon, PlayIcon,
TicketIcon, TicketIcon,
@@ -53,10 +55,9 @@ import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import { countries } from 'country-flag-icons'; import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import getConfig from 'next/config';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -126,7 +127,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>( const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!movie?.onUserWatchlist !movie?.onUserWatchlist
); );
const { publicRuntimeConfig } = getConfig(); const [isBlacklistUpdating, setIsBlacklistUpdating] =
useState<boolean>(false);
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const { addToast } = useToasts(); const { addToast } = useToasts();
const { const {
@@ -157,6 +160,11 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false); setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]); }, [router.query.manage]);
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
[]
);
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: data?.mediaInfo?.mediaUrl, mediaUrl: data?.mediaInfo?.mediaUrl,
mediaUrl4k: data?.mediaInfo?.mediaUrl4k, mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
@@ -279,7 +287,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
?.flatrate ?? []; ?.flatrate ?? [];
function getAvalaibleMediaServerName() { function getAvalaibleMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
} }
@@ -291,8 +299,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
} }
function getAvalaible4kMediaServerName() { function getAvalaible4kMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
} }
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
@@ -376,6 +384,60 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
} }
}; };
const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true);
const res = await fetch('/api/v1/blacklist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: movie?.id,
mediaType: 'movie',
title: movie?.title,
user: user?.id,
}),
});
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
revalidate();
} else if (res.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsBlacklistUpdating(false);
closeBlacklistModal();
};
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
type: 'or',
});
return ( return (
<div <div
className="media-page" className="media-page"
@@ -421,6 +483,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
revalidate={() => revalidate()} revalidate={() => revalidate()}
show={showManager} show={showManager}
/> />
<BlacklistModal
tmdbId={data.id}
type="movie"
show={showBlacklistModal}
onCancel={closeBlacklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isBlacklistUpdating}
/>
<div className="media-header"> <div className="media-header">
<div className="media-poster"> <div className="media-poster">
<CachedImage <CachedImage
@@ -497,40 +567,61 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
</span> </span>
</div> </div>
<div className="media-actions"> <div className="media-actions">
<> {showHideButton &&
{toggleWatchlist ? ( data?.mediaInfo?.status !== MediaStatus.PROCESSING &&
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}> data?.mediaInfo?.status !== MediaStatus.AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PENDING &&
data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<Tooltip
content={intl.formatMessage(globalMessages.addToBlacklist)}
>
<Button <Button
buttonType={'ghost'} buttonType={'ghost'}
className="z-40 mr-2" className="z-40 mr-2"
buttonSize={'md'} buttonSize={'md'}
onClick={onClickWatchlistBtn} onClick={() => setShowBlacklistModal(true)}
> >
{isUpdating ? ( <EyeSlashIcon className={'h-3'} />
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
</> {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<>
{toggleWatchlist ? (
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<PlayButton links={mediaLinks} /> <PlayButton links={mediaLinks} />
<RequestButton <RequestButton
mediaType="movie" mediaType="movie"

View File

@@ -78,6 +78,13 @@ export const messages = defineMessages('components.PermissionEdit', {
viewwatchlists: 'View {mediaServerName} Watchlists', viewwatchlists: 'View {mediaServerName} Watchlists',
viewwatchlistsDescription: viewwatchlistsDescription:
"Grant permission to view other users' {mediaServerName} Watchlists.", "Grant permission to view other users' {mediaServerName} Watchlists.",
manageblacklist: 'Manage Blacklist',
manageblacklistDescription: 'Grant permission to manage blacklisted media.',
blacklistedItems: 'Blacklist media.',
blacklistedItemsDescription: 'Grant permission to blacklist media.',
viewblacklistedItems: 'View blacklisted media.',
viewblacklistedItemsDescription:
'Grant permission to view blacklisted media.',
}); });
interface PermissionEditProps { interface PermissionEditProps {
@@ -332,6 +339,22 @@ export const PermissionEdit = ({
}, },
], ],
}, },
{
id: 'manageblacklist',
name: intl.formatMessage(messages.manageblacklist),
description: intl.formatMessage(messages.manageblacklistDescription),
permission: Permission.MANAGE_BLACKLIST,
children: [
{
id: 'viewblacklisteditems',
name: intl.formatMessage(messages.viewblacklistedItems),
description: intl.formatMessage(
messages.viewblacklistedItemsDescription
),
permission: Permission.VIEW_BLACKLIST,
},
],
},
]; ];
return ( return (

View File

@@ -300,6 +300,7 @@ const RequestButton = ({
}) && }) &&
media && media &&
media.status !== MediaStatus.AVAILABLE && media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.BLACKLISTED &&
!isShowComplete !isShowComplete
) { ) {
buttons.push({ buttons.push({
@@ -345,6 +346,7 @@ const RequestButton = ({
}) && }) &&
media && media &&
media.status4k !== MediaStatus.AVAILABLE && media.status4k !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.BLACKLISTED &&
!is4kShowComplete && !is4kShowComplete &&
settings.currentSettings.series4kEnabled settings.currentSettings.series4kEnabled
) { ) {

View File

@@ -22,7 +22,6 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
@@ -116,7 +115,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
className="group flex items-center" className="group flex items-center"
> >
<span className="avatar-sm"> <span className="avatar-sm">
<Image <CachedImage
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"
@@ -390,7 +389,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
className="group flex items-center" className="group flex items-center"
> >
<span className="avatar-sm"> <span className="avatar-sm">
<Image <CachedImage
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"

View File

@@ -21,7 +21,6 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
@@ -43,6 +42,7 @@ const messages = defineMessages('components.RequestList.RequestItem', {
tmdbid: 'TMDB ID', tmdbid: 'TMDB ID',
tvdbid: 'TheTVDB ID', tvdbid: 'TheTVDB ID',
unknowntitle: 'Unknown Title', unknowntitle: 'Unknown Title',
removearr: 'Remove from {arr}',
profileName: 'Profile', profileName: 'Profile',
}); });
@@ -190,7 +190,7 @@ const RequestItemError = ({
className="group flex items-center truncate" className="group flex items-center truncate"
> >
<span className="avatar-sm ml-1.5"> <span className="avatar-sm ml-1.5">
<Image <CachedImage
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"
@@ -249,7 +249,7 @@ const RequestItemError = ({
className="group flex items-center truncate" className="group flex items-center truncate"
> >
<span className="avatar-sm ml-1.5"> <span className="avatar-sm ml-1.5">
<Image <CachedImage
src={requestData.modifiedBy.avatar} src={requestData.modifiedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"
@@ -342,6 +342,18 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
revalidateList(); revalidateList();
}; };
const deleteMediaFile = async () => {
if (request.media) {
await fetch(`/api/v1/media/${request.media.id}/file`, {
method: 'DELETE',
});
await fetch(`/api/v1/media/${request.media.id}`, {
method: 'DELETE',
});
revalidateList();
}
};
const retryRequest = async () => { const retryRequest = async () => {
setRetrying(true); setRetrying(true);
@@ -557,7 +569,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
className="group flex items-center truncate" className="group flex items-center truncate"
> >
<span className="avatar-sm ml-1.5"> <span className="avatar-sm ml-1.5">
<Image <CachedImage
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"
@@ -616,7 +628,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
className="group flex items-center truncate" className="group flex items-center truncate"
> >
<span className="avatar-sm ml-1.5"> <span className="avatar-sm ml-1.5">
<Image <CachedImage
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"
@@ -667,14 +679,28 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
)} )}
{requestData.status !== MediaRequestStatus.PENDING && {requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (
<ConfirmButton <>
onClick={() => deleteRequest()} <ConfirmButton
confirmText={intl.formatMessage(globalMessages.areyousure)} onClick={() => deleteRequest()}
className="w-full" confirmText={intl.formatMessage(globalMessages.areyousure)}
> className="w-full"
<TrashIcon /> >
<span>{intl.formatMessage(messages.deleterequest)}</span> <TrashIcon />
</ConfirmButton> <span>{intl.formatMessage(messages.deleterequest)}</span>
</ConfirmButton>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr, {
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
</>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (

View File

@@ -1,4 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import CachedImage from '@app/components/Common/CachedImage';
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner'; import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
import type { User } from '@app/hooks/useUser'; import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
@@ -14,7 +15,6 @@ import type {
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions'; import { hasPermission } from '@server/lib/permissions';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import Select from 'react-select'; import Select from 'react-select';
@@ -561,7 +561,7 @@ const AdvancedRequester = ({
<span className="inline-block w-full rounded-md shadow-sm"> <span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"> <Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
<span className="flex items-center"> <span className="flex items-center">
<Image <CachedImage
src={selectedUser.avatar} src={selectedUser.avatar}
alt="" alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover" className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
@@ -613,7 +613,7 @@ const AdvancedRequester = ({
selected ? 'font-semibold' : 'font-normal' selected ? 'font-semibold' : 'font-normal'
} flex items-center`} } flex items-center`}
> >
<Image <CachedImage
src={user.avatar} src={user.avatar}
alt="" alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover" className="h-6 w-6 flex-shrink-0 rounded-full object-cover"

View File

@@ -66,7 +66,9 @@ const CollectionRequestModal = ({
(quota?.movie.remaining ?? 0) - selectedParts.length; (quota?.movie.remaining ?? 0) - selectedParts.length;
const getAllParts = (): number[] => { const getAllParts = (): number[] => {
return (data?.parts ?? []).map((part) => part.id); return (data?.parts ?? [])
.filter((part) => part.mediaInfo?.status !== MediaStatus.BLACKLISTED)
.map((part) => part.id);
}; };
const getAllRequestedParts = (): number[] => { const getAllRequestedParts = (): number[] => {
@@ -248,6 +250,11 @@ const CollectionRequestModal = ({
{ type: 'or' } { type: 'or' }
); );
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
{ type: 'or' }
);
return ( return (
<Modal <Modal
loading={(!data && !error) || !quota} loading={(!data && !error) || !quota}
@@ -344,122 +351,156 @@ const CollectionRequestModal = ({
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-gray-700">
{data?.parts.map((part) => { {data?.parts
const partRequest = getPartRequest(part.id); .filter((part) => {
const partMedia = if (!blacklistVisibility)
part.mediaInfo && return (
part.mediaInfo[is4k ? 'status4k' : 'status'] !== part.mediaInfo?.status !== MediaStatus.BLACKLISTED
MediaStatus.UNKNOWN );
? part.mediaInfo return part;
: undefined; })
.map((part) => {
const partRequest = getPartRequest(part.id);
const partMedia =
part.mediaInfo &&
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
MediaStatus.UNKNOWN
? part.mediaInfo
: undefined;
return ( return (
<tr key={`part-${part.id}`}> <tr key={`part-${part.id}`}>
<td className="whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100"> <td
<span className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
role="checkbox" partMedia?.status === MediaStatus.BLACKLISTED &&
tabIndex={0} 'pointer-events-none opacity-50'
aria-checked={
!!partMedia || isSelectedPart(part.id)
}
onClick={() => togglePart(part.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === 'Space') {
togglePart(part.id);
}
}}
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
!!partMedia ||
partRequest ||
(quota?.movie.limit &&
currentlyRemaining <= 0 &&
!isSelectedPart(part.id))
? 'opacity-50'
: ''
}`} }`}
> >
<span <span
aria-hidden="true" role="checkbox"
className={`${ tabIndex={0}
!!partMedia || aria-checked={
partRequest || (!!partMedia &&
partMedia.status !==
MediaStatus.BLACKLISTED) ||
isSelectedPart(part.id) isSelectedPart(part.id)
? 'bg-indigo-500'
: 'bg-gray-700'
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
></span>
<span
aria-hidden="true"
className={`${
!!partMedia ||
partRequest ||
isSelectedPart(part.id)
? 'translate-x-5'
: 'translate-x-0'
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
></span>
</span>
</td>
<td className="flex items-center px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
<div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
<CachedImage
src={
part.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
: '/images/overseerr_poster_not_found.png'
} }
alt="" onClick={() => togglePart(part.id)}
sizes="100vw" onKeyDown={(e) => {
style={{ if (e.key === 'Enter' || e.key === 'Space') {
width: '100%', togglePart(part.id);
height: 'auto', }
objectFit: 'cover',
}} }}
width={600} className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
height={900} (!!partMedia &&
/> partMedia.status !==
</div> MediaStatus.BLACKLISTED) ||
<div className="flex flex-col justify-center pl-2"> partRequest ||
<div className="text-xs font-medium"> (quota?.movie.limit &&
{part.releaseDate?.slice(0, 4)} currentlyRemaining <= 0 &&
!isSelectedPart(part.id))
? 'opacity-50'
: ''
}`}
>
<span
aria-hidden="true"
className={`${
(!!partMedia &&
partMedia.status !==
MediaStatus.BLACKLISTED) ||
partRequest ||
isSelectedPart(part.id)
? 'bg-indigo-500'
: 'bg-gray-700'
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
></span>
<span
aria-hidden="true"
className={`${
(!!partMedia &&
partMedia.status !==
MediaStatus.BLACKLISTED) ||
partRequest ||
isSelectedPart(part.id)
? 'translate-x-5'
: 'translate-x-0'
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
></span>
</span>
</td>
<td
className={`flex items-center px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 ${
partMedia?.status === MediaStatus.BLACKLISTED &&
'pointer-events-none opacity-50'
}`}
>
<div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
<CachedImage
src={
part.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"
style={{
width: '100%',
height: 'auto',
objectFit: 'cover',
}}
width={600}
height={900}
/>
</div> </div>
<div className="text-base font-bold"> <div className="flex flex-col justify-center pl-2">
{part.title} <div className="text-xs font-medium">
{part.releaseDate?.slice(0, 4)}
</div>
<div className="text-base font-bold">
{part.title}
</div>
</div> </div>
</div> </td>
</td> <td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
<td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6"> {!partMedia && !partRequest && (
{!partMedia && !partRequest && ( <Badge>
<Badge> {intl.formatMessage(
{intl.formatMessage(globalMessages.notrequested)} globalMessages.notrequested
</Badge> )}
)}
{!partMedia &&
partRequest?.status ===
MediaRequestStatus.PENDING && (
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge> </Badge>
)} )}
{((!partMedia && {!partMedia &&
partRequest?.status === partRequest?.status ===
MediaRequestStatus.APPROVED) || MediaRequestStatus.PENDING && (
partMedia?.[is4k ? 'status4k' : 'status'] === <Badge badgeType="warning">
MediaStatus.PROCESSING) && ( {intl.formatMessage(globalMessages.pending)}
<Badge badgeType="primary"> </Badge>
{intl.formatMessage(globalMessages.requested)} )}
</Badge> {((!partMedia &&
)} partRequest?.status ===
{partMedia?.[is4k ? 'status4k' : 'status'] === MediaRequestStatus.APPROVED) ||
MediaStatus.AVAILABLE && ( partMedia?.[is4k ? 'status4k' : 'status'] ===
<Badge badgeType="success"> MediaStatus.PROCESSING) && (
{intl.formatMessage(globalMessages.available)} <Badge badgeType="primary">
</Badge> {intl.formatMessage(globalMessages.requested)}
)} </Badge>
</td> )}
</tr> {partMedia?.[is4k ? 'status4k' : 'status'] ===
); MediaStatus.AVAILABLE && (
})} <Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{partMedia?.status === MediaStatus.BLACKLISTED && (
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)}
</Badge>
)}
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -33,6 +33,13 @@ const messages = defineMessages('components.Selector', {
nooptions: 'No results.', nooptions: 'No results.',
showmore: 'Show More', showmore: 'Show More',
showless: 'Show Less', showless: 'Show Less',
searchStatus: 'Select status...',
returningSeries: 'Returning Series',
planned: 'Planned',
inProduction: 'In Production',
ended: 'Ended',
canceled: 'Canceled',
pilot: 'Pilot',
}); });
type SingleVal = { type SingleVal = {
@@ -204,6 +211,75 @@ export const GenreSelector = ({
); );
}; };
export const StatusSelector = ({
isMulti,
defaultValue,
onChange,
}: BaseSelectorMultiProps | BaseSelectorSingleProps) => {
const intl = useIntl();
const [defaultDataValue, setDefaultDataValue] = useState<
{ label: string; value: number }[] | null
>(null);
const options = useMemo(
() => [
{ name: intl.formatMessage(messages.returningSeries), id: 0 },
{ name: intl.formatMessage(messages.planned), id: 1 },
{ name: intl.formatMessage(messages.inProduction), id: 2 },
{ name: intl.formatMessage(messages.ended), id: 3 },
{ name: intl.formatMessage(messages.canceled), id: 4 },
{ name: intl.formatMessage(messages.pilot), id: 5 },
],
[intl]
);
useEffect(() => {
const loadDefaultStatus = async (): Promise<void> => {
if (!defaultValue) {
return;
}
const statuses = defaultValue.split('|');
const statusData = options
.filter((opt) => statuses.find((s) => Number(s) === opt.id))
.map((o) => ({
label: o.name,
value: o.id,
}));
setDefaultDataValue(statusData);
};
loadDefaultStatus();
}, [defaultValue, options]);
const loadStatusOptions = async () => {
return options
.map((result) => ({
label: result.name,
value: result.id,
}))
.filter(({ label }) => label.toLowerCase());
};
return (
<AsyncSelect
key={`status-select-${defaultDataValue}`}
className="react-select-container"
classNamePrefix="react-select"
defaultValue={isMulti ? defaultDataValue : defaultDataValue?.[0]}
defaultOptions
isMulti={isMulti}
loadOptions={loadStatusOptions}
placeholder={intl.formatMessage(messages.searchStatus)}
onChange={(value) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange(value as any);
}}
/>
);
};
export const KeywordSelector = ({ export const KeywordSelector = ({
isMulti, isMulti,
defaultValue, defaultValue,

View File

@@ -3,13 +3,14 @@ import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import SensitiveInput from '@app/components/Common/SensitiveInput'; import SensitiveInput from '@app/components/Common/SensitiveInput';
import LibraryItem from '@app/components/Settings/LibraryItem'; import LibraryItem from '@app/components/Settings/LibraryItem';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import type { JellyfinSettings } from '@server/lib/settings'; import type { JellyfinSettings } from '@server/lib/settings';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import getConfig from 'next/config';
import { useState } from 'react'; import { useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -61,6 +62,9 @@ const messages = defineMessages('components.Settings', {
validationUrlTrailingSlash: 'URL must not end in a trailing slash', validationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationUrlBaseLeadingSlash: 'URL base must have a leading slash', validationUrlBaseLeadingSlash: 'URL base must have a leading slash',
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash', validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
tip: 'Tip',
scanbackground:
'Scanning will run in the background. You can continue the setup process in the meantime.',
}); });
interface Library { interface Library {
@@ -78,13 +82,13 @@ interface SyncStatus {
} }
interface SettingsJellyfinProps { interface SettingsJellyfinProps {
showAdvancedSettings?: boolean; isSetupSettings?: boolean;
onComplete?: () => void; onComplete?: () => void;
} }
const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
onComplete, onComplete,
showAdvancedSettings, isSetupSettings,
}) => { }) => {
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const toasts = useToasts(); const toasts = useToasts();
@@ -102,7 +106,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
); );
const intl = useIntl(); const intl = useIntl();
const { addToast } = useToasts(); const { addToast } = useToasts();
const { publicRuntimeConfig } = getConfig(); const settings = useSettings();
const JellyfinSettingsSchema = Yup.object().shape({ const JellyfinSettingsSchema = Yup.object().shape({
hostname: Yup.string() hostname: Yup.string()
@@ -284,26 +288,29 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
return <LoadingSpinner />; return <LoadingSpinner />;
} }
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return ( return (
<> <>
<div className="mb-6"> <div className="mb-6">
<h3 className="heading"> <h3 className="heading">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {intl.formatMessage(
? intl.formatMessage(messages.jellyfinlibraries, { messages.jellyfinlibraries,
mediaServerName: 'Emby', mediaServerFormatValues
}) )}
: intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Jellyfin',
})}
</h3> </h3>
<p className="description"> <p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {intl.formatMessage(
? intl.formatMessage(messages.jellyfinlibrariesDescription, { messages.jellyfinlibrariesDescription,
mediaServerName: 'Emby', mediaServerFormatValues
}) )}
: intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Jellyfin',
})}
</p> </p>
</div> </div>
<div className="section"> <div className="section">
@@ -340,13 +347,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<FormattedMessage {...messages.manualscanJellyfin} /> <FormattedMessage {...messages.manualscanJellyfin} />
</h3> </h3>
<p className="description"> <p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {intl.formatMessage(
? intl.formatMessage(messages.manualscanDescriptionJellyfin, { messages.manualscanDescriptionJellyfin,
mediaServerName: 'Emby', mediaServerFormatValues
}) )}
: intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Jellyfin',
})}
</p> </p>
</div> </div>
<div className="section"> <div className="section">
@@ -446,24 +450,26 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
</div> </div>
</div> </div>
</div> </div>
{isSetupSettings && (
<div className="text-sm text-gray-500">
<span className="mr-2">
<Badge>{intl.formatMessage(messages.tip)}</Badge>
</span>
{intl.formatMessage(messages.scanbackground)}
</div>
)}
<div className="mt-10 mb-6"> <div className="mt-10 mb-6">
<h3 className="heading"> <h3 className="heading">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {intl.formatMessage(
? intl.formatMessage(messages.jellyfinSettings, { messages.jellyfinSettings,
mediaServerName: 'Emby', mediaServerFormatValues
}) )}
: intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Jellyfin',
})}
</h3> </h3>
<p className="description"> <p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {intl.formatMessage(
? intl.formatMessage(messages.jellyfinSettingsDescription, { messages.jellyfinSettingsDescription,
mediaServerName: 'Emby', mediaServerFormatValues
}) )}
: intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Jellyfin',
})}
</p> </p>
</div> </div>
<Formik <Formik
@@ -497,12 +503,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
if (!res.ok) throw new Error(res.statusText, { cause: res }); if (!res.ok) throw new Error(res.statusText, { cause: res });
addToast( addToast(
intl.formatMessage(messages.jellyfinSettingsSuccess, { intl.formatMessage(
mediaServerName: messages.jellyfinSettingsSuccess,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' mediaServerFormatValues
? 'Emby' ),
: 'Jellyfin',
}),
{ {
autoDismiss: true, autoDismiss: true,
appearance: 'success', appearance: 'success',
@@ -518,12 +522,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
} }
if (errorData?.message === ApiErrorCode.InvalidUrl) { if (errorData?.message === ApiErrorCode.InvalidUrl) {
addToast( addToast(
intl.formatMessage(messages.invalidurlerror, { intl.formatMessage(
mediaServerName: messages.invalidurlerror,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' mediaServerFormatValues
? 'Emby' ),
: 'Jellyfin',
}),
{ {
autoDismiss: true, autoDismiss: true,
appearance: 'error', appearance: 'error',
@@ -531,12 +533,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
); );
} else { } else {
addToast( addToast(
intl.formatMessage(messages.jellyfinSettingsFailure, { intl.formatMessage(
mediaServerName: messages.jellyfinSettingsFailure,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' mediaServerFormatValues
? 'Emby' ),
: 'Jellyfin',
}),
{ {
autoDismiss: true, autoDismiss: true,
appearance: 'error', appearance: 'error',
@@ -559,7 +559,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
}) => { }) => {
return ( return (
<form className="section" onSubmit={handleSubmit}> <form className="section" onSubmit={handleSubmit}>
{showAdvancedSettings && ( {!isSetupSettings && (
<> <>
<div className="form-row"> <div className="form-row">
<label htmlFor="hostname" className="text-label"> <label htmlFor="hostname" className="text-label">
@@ -643,7 +643,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
)} )}
</div> </div>
</div> </div>
{showAdvancedSettings && ( {!isSetupSettings && (
<> <>
<div className="form-row"> <div className="form-row">
<label htmlFor="urlBase" className="text-label"> <label htmlFor="urlBase" className="text-label">
@@ -710,7 +710,9 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
)} )}
</div> </div>
</div> </div>
<div className="actions"> <div
className={`actions ${isSetupSettings ? 'mt-0 border-0' : ''}`}
>
<div className="flex justify-end"> <div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm"> <span className="ml-3 inline-flex rounded-md shadow-sm">
<Button <Button

View File

@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import Table from '@app/components/Common/Table'; import Table from '@app/components/Common/Table';
import useLocale from '@app/hooks/useLocale'; import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { formatBytes } from '@app/utils/numberHelpers'; import { formatBytes } from '@app/utils/numberHelpers';
@@ -57,8 +58,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
'plex-recently-added-scan': 'Plex Recently Added Scan', 'plex-recently-added-scan': 'Plex Recently Added Scan',
'plex-full-scan': 'Plex Full Library Scan', 'plex-full-scan': 'Plex Full Library Scan',
'plex-watchlist-sync': 'Plex Watchlist Sync', 'plex-watchlist-sync': 'Plex Watchlist Sync',
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
'jellyfin-full-scan': 'Jellyfin Full Library Scan', 'jellyfin-full-scan': 'Jellyfin Full Library Scan',
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
'availability-sync': 'Media Availability Sync', 'availability-sync': 'Media Availability Sync',
'radarr-scan': 'Radarr Scan', 'radarr-scan': 'Radarr Scan',
'sonarr-scan': 'Sonarr Scan', 'sonarr-scan': 'Sonarr Scan',
@@ -81,6 +82,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
'When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.', 'When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.',
imagecachecount: 'Images Cached', imagecachecount: 'Images Cached',
imagecachesize: 'Total Cache Size', imagecachesize: 'Total Cache Size',
usersavatars: "Users' Avatars",
} }
); );
@@ -167,6 +169,20 @@ const SettingsJobs = () => {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const settings = useSettings(); const settings = useSettings();
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
messages['jellyfin-recently-added-scan'] = {
id: 'jellyfin-recently-added-scan',
defaultMessage: 'Emby Recently Added Scan',
};
}
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
messages['jellyfin-full-scan'] = {
id: 'jellyfin-full-scan',
defaultMessage: 'Emby Full Library Scan',
};
}
if (!data && !error) { if (!data && !error) {
return <LoadingSpinner />; return <LoadingSpinner />;
} }
@@ -558,6 +574,19 @@ const SettingsJobs = () => {
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)} {formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
</Table.TD> </Table.TD>
</tr> </tr>
<tr>
<Table.TD>
{intl.formatMessage(messages.usersavatars)} (avatar)
</Table.TD>
<Table.TD>
{intl.formatNumber(
cacheData?.imageCache.avatar.imageCount ?? 0
)}
</Table.TD>
<Table.TD>
{formatBytes(cacheData?.imageCache.avatar.size ?? 0)}
</Table.TD>
</tr>
</Table.TBody> </Table.TBody>
</Table> </Table>
</div> </div>

View File

@@ -5,7 +5,6 @@ import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages('components.Settings', { const messages = defineMessages('components.Settings', {
@@ -26,7 +25,6 @@ type SettingsLayoutProps = {
const SettingsLayout = ({ children }: SettingsLayoutProps) => { const SettingsLayout = ({ children }: SettingsLayoutProps) => {
const intl = useIntl(); const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const settings = useSettings(); const settings = useSettings();
const settingsRoutes: SettingsRoute[] = [ const settingsRoutes: SettingsRoute[] = [
{ {
@@ -89,7 +87,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
function getAvailableMediaServerName() { function getAvailableMediaServerName() {
return intl.formatMessage(messages.menuJellyfinSettings, { return intl.formatMessage(messages.menuJellyfinSettings, {
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin', settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
}); });
} }
}; };

View File

@@ -10,7 +10,6 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { MainSettings } from '@server/lib/settings'; import type { MainSettings } from '@server/lib/settings';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
@@ -42,12 +41,20 @@ const SettingsUsers = () => {
mutate: revalidate, mutate: revalidate,
} = useSWR<MainSettings>('/api/v1/settings/main'); } = useSWR<MainSettings>('/api/v1/settings/main');
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) { if (!data && !error) {
return <LoadingSpinner />; return <LoadingSpinner />;
} }
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return ( return (
<> <>
<PageTitle <PageTitle
@@ -121,16 +128,10 @@ const SettingsUsers = () => {
<label htmlFor="localLogin" className="checkbox-label"> <label htmlFor="localLogin" className="checkbox-label">
{intl.formatMessage(messages.localLogin)} {intl.formatMessage(messages.localLogin)}
<span className="label-tip"> <span className="label-tip">
{intl.formatMessage(messages.localLoginTip, { {intl.formatMessage(
mediaServerName: messages.localLoginTip,
settings.currentSettings.mediaServerType === mediaServerFormatValues
MediaServerType.PLEX )}
? 'Plex'
: settings.currentSettings.mediaServerType ===
MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby',
})}
</span> </span>
</label> </label>
<div className="form-input-area"> <div className="form-input-area">
@@ -146,25 +147,15 @@ const SettingsUsers = () => {
</div> </div>
<div className="form-row"> <div className="form-row">
<label htmlFor="newPlexLogin" className="checkbox-label"> <label htmlFor="newPlexLogin" className="checkbox-label">
{intl.formatMessage(messages.newPlexLogin, { {intl.formatMessage(
mediaServerName: messages.newPlexLogin,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' mediaServerFormatValues
? 'Emby' )}
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
<span className="label-tip"> <span className="label-tip">
{intl.formatMessage(messages.newPlexLoginTip, { {intl.formatMessage(
mediaServerName: messages.newPlexLoginTip,
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' mediaServerFormatValues
? 'Emby' )}
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
</span> </span>
</label> </label>
<div className="form-input-area"> <div className="form-input-area">

View File

@@ -1,32 +1,39 @@
import Accordion from '@app/components/Common/Accordion'; import Button from '@app/components/Common/Button';
import JellyfinLogin from '@app/components/Login/JellyfinLogin'; import JellyfinLogin from '@app/components/Login/JellyfinLogin';
import PlexLoginButton from '@app/components/PlexLoginButton'; import PlexLoginButton from '@app/components/PlexLoginButton';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const messages = defineMessages('components.Setup', { const messages = defineMessages('components.Setup', {
welcome: 'Welcome to Jellyseerr', welcome: 'Welcome to Jellyseerr',
signinMessage: 'Get started by signing in', signinMessage: 'Get started by signing in',
signinWithJellyfin: 'Use your {mediaServerName} account', signin: 'Sign in to your account',
signinWithPlex: 'Use your Plex account', signinWithJellyfin: 'Enter your Jellyfin details',
signinWithEmby: 'Enter your Emby details',
signinWithPlex: 'Enter your Plex details',
back: 'Go back',
}); });
interface LoginWithMediaServerProps { interface LoginWithMediaServerProps {
onComplete: (onComplete: MediaServerType) => void; serverType: MediaServerType;
onCancel: () => void;
onComplete: () => void;
} }
const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => { const SetupLogin: React.FC<LoginWithMediaServerProps> = ({
serverType,
onCancel,
onComplete,
}) => {
const [authToken, setAuthToken] = useState<string | undefined>(undefined); const [authToken, setAuthToken] = useState<string | undefined>(undefined);
const [mediaServerType, setMediaServerType] = useState<MediaServerType>( const [mediaServerType, setMediaServerType] = useState<MediaServerType>(
MediaServerType.NOT_CONFIGURED MediaServerType.NOT_CONFIGURED
); );
const { user, revalidate } = useUser(); const { user, revalidate } = useUser();
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth // 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 // 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 _shouid_ come back with a valid user.
@@ -56,71 +63,60 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
useEffect(() => { useEffect(() => {
if (user) { if (user) {
onComplete(mediaServerType); onComplete();
} }
}, [user, mediaServerType, onComplete]); }, [user, mediaServerType, onComplete]);
return ( return (
<div> <div className="p-4">
<div className="mb-2 flex justify-center text-xl font-bold"> <div className="mb-2 flex justify-center text-xl font-bold">
<FormattedMessage {...messages.welcome} /> <FormattedMessage {...messages.signin} />
</div> </div>
<div className="mb-2 flex justify-center pb-6 text-sm"> <div className="mb-2 flex justify-center pb-6 text-sm">
<FormattedMessage {...messages.signinMessage} /> {serverType === MediaServerType.JELLYFIN ? (
</div> <FormattedMessage {...messages.signinWithJellyfin} />
<Accordion single atLeastOne> ) : serverType === MediaServerType.EMBY ? (
{({ openIndexes, handleClick, AccordionContent }) => ( <FormattedMessage {...messages.signinWithEmby} />
<> ) : (
<button <FormattedMessage {...messages.signinWithPlex} />
className={`w-full cursor-default bg-gray-900 py-2 text-center text-sm text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none sm:rounded-t-lg ${
openIndexes.includes(0) && 'text-indigo-500'
} ${openIndexes.includes(1) && 'border-b border-gray-500'}`}
onClick={() => handleClick(0)}
>
<FormattedMessage {...messages.signinWithPlex} />
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div
className="px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<PlexLoginButton
onAuthToken={(authToken) => {
setMediaServerType(MediaServerType.PLEX);
setAuthToken(authToken);
}}
/>
</div>
</AccordionContent>
<div>
<button
className={`w-full cursor-default bg-gray-900 py-2 text-center text-sm text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none ${
openIndexes.includes(1)
? 'text-indigo-500'
: 'sm:rounded-b-lg'
}`}
onClick={() => handleClick(1)}
>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Jellyfin',
})}
</button>
<AccordionContent isOpen={openIndexes.includes(1)}>
<div
className="rounded-b-lg px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<JellyfinLogin initial={true} revalidate={revalidate} />
</div>
</AccordionContent>
</div>
</>
)} )}
</Accordion> </div>
{serverType === MediaServerType.PLEX && (
<>
<div
className="px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<PlexLoginButton
onAuthToken={(authToken) => {
setMediaServerType(MediaServerType.PLEX);
setAuthToken(authToken);
}}
/>
</div>
<div className="mt-4">
<Button buttonType="default" onClick={() => onCancel()}>
<FormattedMessage {...messages.back} />
</Button>
</div>
</>
)}
{serverType === MediaServerType.JELLYFIN && (
<JellyfinLogin
initial={true}
revalidate={revalidate}
serverType={serverType}
onCancel={onCancel}
/>
)}
{serverType === MediaServerType.EMBY && (
<JellyfinLogin
initial={true}
revalidate={revalidate}
serverType={serverType}
onCancel={onCancel}
/>
)}
</div> </div>
); );
}; };

View File

@@ -1,5 +1,7 @@
import EmbyLogo from '@app/assets/services/emby.svg';
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import AppDataWarning from '@app/components/AppDataWarning'; import AppDataWarning from '@app/components/AppDataWarning';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import ImageFader from '@app/components/Common/ImageFader'; import ImageFader from '@app/components/Common/ImageFader';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
@@ -9,26 +11,30 @@ import SettingsPlex from '@app/components/Settings/SettingsPlex';
import SettingsServices from '@app/components/Settings/SettingsServices'; import SettingsServices from '@app/components/Settings/SettingsServices';
import SetupSteps from '@app/components/Setup/SetupSteps'; import SetupSteps from '@app/components/Setup/SetupSteps';
import useLocale from '@app/hooks/useLocale'; import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
import SetupLogin from './SetupLogin'; import SetupLogin from './SetupLogin';
const messages = defineMessages('components.Setup', { const messages = defineMessages('components.Setup', {
welcome: 'Welcome to Jellyseerr',
subtitle: 'Get started by choosing your media server',
configjellyfin: 'Configure Jellyfin',
configplex: 'Configure Plex',
configemby: 'Configure Emby',
setup: 'Setup', setup: 'Setup',
finish: 'Finish Setup', finish: 'Finish Setup',
finishing: 'Finishing…', finishing: 'Finishing…',
continue: 'Continue', continue: 'Continue',
servertype: 'Choose Server Type',
signin: 'Sign In', signin: 'Sign In',
configuremediaserver: 'Configure Media Server', configuremediaserver: 'Configure Media Server',
configureservices: 'Configure Services', configureservices: 'Configure Services',
tip: 'Tip',
scanbackground:
'Scanning will run in the background. You can continue the setup process in the meantime.',
}); });
const Setup = () => { const Setup = () => {
@@ -42,6 +48,7 @@ const Setup = () => {
); );
const router = useRouter(); const router = useRouter();
const { locale } = useLocale(); const { locale } = useLocale();
const settings = useSettings();
const finishSetup = async () => { const finishSetup = async () => {
setIsUpdating(true); setIsUpdating(true);
@@ -76,6 +83,25 @@ const Setup = () => {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
useEffect(() => {
if (settings.currentSettings.initialized) {
router.push('/');
}
if (
settings.currentSettings.mediaServerType !==
MediaServerType.NOT_CONFIGURED
) {
setCurrentStep(3);
setMediaServerType(settings.currentSettings.mediaServerType);
}
}, [
settings.currentSettings.mediaServerType,
settings.currentSettings.initialized,
router,
]);
if (settings.currentSettings.initialized) return <></>;
return ( return (
<div className="relative flex min-h-screen flex-col justify-center bg-gray-900 py-12"> <div className="relative flex min-h-screen flex-col justify-center bg-gray-900 py-12">
<PageTitle title={intl.formatMessage(messages.setup)} /> <PageTitle title={intl.formatMessage(messages.setup)} />
@@ -101,58 +127,120 @@ const Setup = () => {
> >
<SetupSteps <SetupSteps
stepNumber={1} stepNumber={1}
description={intl.formatMessage(messages.signin)} description={intl.formatMessage(messages.servertype)}
active={currentStep === 1} active={currentStep === 1}
completed={currentStep > 1} completed={currentStep > 1}
/> />
<SetupSteps <SetupSteps
stepNumber={2} stepNumber={2}
description={intl.formatMessage(messages.configuremediaserver)} description={intl.formatMessage(messages.signin)}
active={currentStep === 2} active={currentStep === 2}
completed={currentStep > 2} completed={currentStep > 2}
/> />
<SetupSteps <SetupSteps
stepNumber={3} stepNumber={3}
description={intl.formatMessage(messages.configureservices)} description={intl.formatMessage(messages.configuremediaserver)}
active={currentStep === 3} active={currentStep === 3}
completed={currentStep > 3}
/>
<SetupSteps
stepNumber={4}
description={intl.formatMessage(messages.configureservices)}
active={currentStep === 4}
isLastStep isLastStep
/> />
</ul> </ul>
</nav> </nav>
<div className="mt-10 w-full rounded-md border border-gray-600 bg-gray-800 bg-opacity-50 p-4 text-white"> <div className="mt-10 w-full rounded-md border border-gray-600 bg-gray-800 bg-opacity-50 p-4 text-white">
{currentStep === 1 && ( {currentStep === 1 && (
<SetupLogin <div className="flex flex-col items-center pb-6">
onComplete={(mServerType) => { <div className="mb-2 flex justify-center text-xl font-bold">
setMediaServerType(mServerType); {intl.formatMessage(messages.welcome)}
setCurrentStep(2); </div>
}} <div className="mb-2 flex justify-center pb-6 text-sm">
/> {intl.formatMessage(messages.subtitle)}
</div>
<div className="grid grid-cols-3">
<div className="flex flex-col divide-y divide-gray-600 rounded-l border border-gray-600 py-2">
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
<JellyfinLogo className="h-10" />
</div>
<div className="px-2 pt-2">
<button
onClick={() => {
setMediaServerType(MediaServerType.JELLYFIN);
setCurrentStep(2);
}}
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
>
{intl.formatMessage(messages.configjellyfin)}
</button>
</div>
</div>
<div className="flex flex-col divide-y divide-gray-600 border-y border-gray-600 py-2">
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
<PlexLogo className="h-8" />
</div>
<div className="px-2 pt-2">
<button
onClick={() => {
setMediaServerType(MediaServerType.PLEX);
setCurrentStep(2);
}}
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
>
{intl.formatMessage(messages.configplex)}
</button>
</div>
</div>
<div className="flex flex-col divide-y divide-gray-600 rounded-r border border-gray-600 py-2">
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
<EmbyLogo className="h-9" />
</div>
<div className="px-2 pt-2">
<button
onClick={() => {
setMediaServerType(MediaServerType.EMBY);
setCurrentStep(2);
}}
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
>
{intl.formatMessage(messages.configemby)}
</button>
</div>
</div>
</div>
</div>
)} )}
{currentStep === 2 && ( {currentStep === 2 && (
<div> <SetupLogin
serverType={mediaServerType}
onCancel={() => {
setMediaServerType(MediaServerType.NOT_CONFIGURED);
setCurrentStep(1);
}}
onComplete={() => setCurrentStep(3)}
/>
)}
{currentStep === 3 && (
<div className="p-2">
{mediaServerType === MediaServerType.PLEX ? ( {mediaServerType === MediaServerType.PLEX ? (
<SettingsPlex <SettingsPlex
onComplete={() => setMediaServerSettingsComplete(true)} onComplete={() => setMediaServerSettingsComplete(true)}
/> />
) : ( ) : (
<SettingsJellyfin <SettingsJellyfin
showAdvancedSettings={false} isSetupSettings
onComplete={() => setMediaServerSettingsComplete(true)} onComplete={() => setMediaServerSettingsComplete(true)}
/> />
)} )}
<div className="mt-4 text-sm text-gray-500">
<span className="mr-2">
<Badge>{intl.formatMessage(messages.tip)}</Badge>
</span>
{intl.formatMessage(messages.scanbackground)}
</div>
<div className="actions"> <div className="actions">
<div className="flex justify-end"> <div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm"> <span className="ml-3 inline-flex rounded-md shadow-sm">
<Button <Button
buttonType="primary" buttonType="primary"
disabled={!mediaServerSettingsComplete} disabled={!mediaServerSettingsComplete}
onClick={() => setCurrentStep(3)} onClick={() => setCurrentStep(4)}
> >
{intl.formatMessage(messages.continue)} {intl.formatMessage(messages.continue)}
</Button> </Button>
@@ -161,7 +249,7 @@ const Setup = () => {
</div> </div>
</div> </div>
)} )}
{currentStep === 3 && ( {currentStep === 4 && (
<div> <div>
<SettingsServices /> <SettingsServices />
<div className="actions"> <div className="actions">

View File

@@ -9,7 +9,6 @@ import defineMessages from '@app/utils/defineMessages';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { DownloadingItem } from '@server/lib/downloadtracker'; import type { DownloadingItem } from '@server/lib/downloadtracker';
import getConfig from 'next/config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages('components.StatusBadge', { const messages = defineMessages('components.StatusBadge', {
@@ -18,6 +17,7 @@ const messages = defineMessages('components.StatusBadge', {
playonplex: 'Play on {mediaServerName}', playonplex: 'Play on {mediaServerName}',
openinarr: 'Open in {arr}', openinarr: 'Open in {arr}',
managemedia: 'Manage {mediaType}', managemedia: 'Manage {mediaType}',
seasonnumber: 'S{seasonNumber}',
seasonepisodenumber: 'S{seasonNumber}E{episodeNumber}', seasonepisodenumber: 'S{seasonNumber}E{episodeNumber}',
}); });
@@ -47,7 +47,6 @@ const StatusBadge = ({
const intl = useIntl(); const intl = useIntl();
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
let mediaLink: string | undefined; let mediaLink: string | undefined;
let mediaLinkDescription: string | undefined; let mediaLinkDescription: string | undefined;
@@ -85,7 +84,7 @@ const StatusBadge = ({
mediaLink = plexUrl; mediaLink = plexUrl;
mediaLinkDescription = intl.formatMessage(messages.playonplex, { mediaLinkDescription = intl.formatMessage(messages.playonplex, {
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby' ? 'Emby'
: settings.currentSettings.mediaServerType === MediaServerType.PLEX : settings.currentSettings.mediaServerType === MediaServerType.PLEX
? 'Plex' ? 'Plex'
@@ -107,22 +106,34 @@ const StatusBadge = ({
} }
} }
const tooltipContent = ( const tooltipContent =
<ul> mediaType === 'tv' &&
{downloadItem.map((status, index) => ( downloadItem.length > 1 &&
<li downloadItem.every(
key={`dl-status-${status.externalId}-${index}`} (item) =>
className="border-b border-gray-700 last:border-b-0" item.downloadId && item.downloadId === downloadItem[0].downloadId
> ) ? (
<DownloadBlock <DownloadBlock
downloadItem={status} downloadItem={downloadItem[0]}
title={Array.isArray(title) ? title[index] : title} title={Array.isArray(title) ? title[0] : title}
is4k={is4k} is4k={is4k}
/> />
</li> ) : (
))} <ul>
</ul> {downloadItem.map((status, index) => (
); <li
key={`dl-status-${status.externalId}-${index}`}
className="border-b border-gray-700 last:border-b-0"
>
<DownloadBlock
downloadItem={status}
title={Array.isArray(title) ? title[index] : title}
is4k={is4k}
/>
</li>
))}
</ul>
);
const badgeDownloadProgress = ( const badgeDownloadProgress = (
<div <div
@@ -177,14 +188,27 @@ const StatusBadge = ({
</span> </span>
{inProgress && ( {inProgress && (
<> <>
{mediaType === 'tv' && downloadItem[0].episode && ( {mediaType === 'tv' &&
<span className="ml-1"> downloadItem[0].episode &&
{intl.formatMessage(messages.seasonepisodenumber, { (downloadItem.length > 1 &&
seasonNumber: downloadItem[0].episode.seasonNumber, downloadItem.every(
episodeNumber: downloadItem[0].episode.episodeNumber, (item) =>
})} item.downloadId &&
</span> item.downloadId === downloadItem[0].downloadId
)} ) ? (
<span className="ml-1">
{intl.formatMessage(messages.seasonnumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
})}
</span>
) : (
<span className="ml-1">
{intl.formatMessage(messages.seasonepisodenumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
episodeNumber: downloadItem[0].episode.episodeNumber,
})}
</span>
))}
<Spinner className="ml-1 h-3 w-3" /> <Spinner className="ml-1 h-3 w-3" />
</> </>
)} )}
@@ -230,14 +254,27 @@ const StatusBadge = ({
</span> </span>
{inProgress && ( {inProgress && (
<> <>
{mediaType === 'tv' && downloadItem[0].episode && ( {mediaType === 'tv' &&
<span className="ml-1"> downloadItem[0].episode &&
{intl.formatMessage(messages.seasonepisodenumber, { (downloadItem.length > 1 &&
seasonNumber: downloadItem[0].episode.seasonNumber, downloadItem.every(
episodeNumber: downloadItem[0].episode.episodeNumber, (item) =>
})} item.downloadId &&
</span> item.downloadId === downloadItem[0].downloadId
)} ) ? (
<span className="ml-1">
{intl.formatMessage(messages.seasonnumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
})}
</span>
) : (
<span className="ml-1">
{intl.formatMessage(messages.seasonepisodenumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
episodeNumber: downloadItem[0].episode.episodeNumber,
})}
</span>
))}
<Spinner className="ml-1 h-3 w-3" /> <Spinner className="ml-1 h-3 w-3" />
</> </>
)} )}
@@ -283,14 +320,27 @@ const StatusBadge = ({
</span> </span>
{inProgress && ( {inProgress && (
<> <>
{mediaType === 'tv' && downloadItem[0].episode && ( {mediaType === 'tv' &&
<span className="ml-1"> downloadItem[0].episode &&
{intl.formatMessage(messages.seasonepisodenumber, { (downloadItem.length > 1 &&
seasonNumber: downloadItem[0].episode.seasonNumber, downloadItem.every(
episodeNumber: downloadItem[0].episode.episodeNumber, (item) =>
})} item.downloadId &&
</span> item.downloadId === downloadItem[0].downloadId
)} ) ? (
<span className="ml-1">
{intl.formatMessage(messages.seasonnumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
})}
</span>
) : (
<span className="ml-1">
{intl.formatMessage(messages.seasonepisodenumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
episodeNumber: downloadItem[0].episode.episodeNumber,
})}
</span>
))}
<Spinner className="ml-1 h-3 w-3" /> <Spinner className="ml-1 h-3 w-3" />
</> </>
)} )}
@@ -310,6 +360,17 @@ const StatusBadge = ({
</Tooltip> </Tooltip>
); );
case MediaStatus.BLACKLISTED:
return (
<Tooltip content={mediaLinkDescription}>
<Badge badgeType="danger" href={mediaLink}>
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: intl.formatMessage(globalMessages.blacklisted),
})}
</Badge>
</Tooltip>
);
default: default:
return null; return null;
} }

View File

@@ -1,7 +1,9 @@
import Spinner from '@app/assets/spinner.svg'; import Spinner from '@app/assets/spinner.svg';
import BlacklistModal from '@app/components/BlacklistModal';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
import StatusBadgeMini from '@app/components/Common/StatusBadgeMini'; import StatusBadgeMini from '@app/components/Common/StatusBadgeMini';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal'; import RequestModal from '@app/components/RequestModal';
import ErrorCard from '@app/components/TitleCard/ErrorCard'; import ErrorCard from '@app/components/TitleCard/ErrorCard';
import Placeholder from '@app/components/TitleCard/Placeholder'; import Placeholder from '@app/components/TitleCard/Placeholder';
@@ -13,6 +15,8 @@ import { withProperties } from '@app/utils/typeHelpers';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { import {
ArrowDownTrayIcon, ArrowDownTrayIcon,
EyeIcon,
EyeSlashIcon,
MinusCircleIcon, MinusCircleIcon,
StarIcon, StarIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
@@ -20,7 +24,7 @@ import { MediaStatus } from '@server/constants/media';
import type { Watchlist } from '@server/entity/Watchlist'; import type { Watchlist } from '@server/entity/Watchlist';
import type { MediaType } from '@server/models/Search'; import type { MediaType } from '@server/models/Search';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment, useCallback, useEffect, useState } from 'react'; import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import { mutate } from 'swr'; import { mutate } from 'swr';
@@ -65,7 +69,7 @@ const TitleCard = ({
}: TitleCardProps) => { }: TitleCardProps) => {
const isTouch = useIsTouch(); const isTouch = useIsTouch();
const intl = useIntl(); const intl = useIntl();
const { hasPermission } = useUser(); const { user, hasPermission } = useUser();
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [currentStatus, setCurrentStatus] = useState(status); const [currentStatus, setCurrentStatus] = useState(status);
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
@@ -74,6 +78,8 @@ const TitleCard = ({
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>( const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!isAddedToWatchlist !isAddedToWatchlist
); );
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
// Just to get the year from the date // Just to get the year from the date
if (year) { if (year) {
@@ -94,6 +100,11 @@ const TitleCard = ({
[] []
); );
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
[]
);
const onClickWatchlistBtn = async (): Promise<void> => { const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
try { try {
@@ -166,6 +177,99 @@ const TitleCard = ({
} }
}; };
const onClickHideItemBtn = async (): Promise<void> => {
setIsUpdating(true);
const topNode = cardRef.current;
if (topNode) {
const res = await fetch('/api/v1/blacklist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: id,
mediaType,
title,
user: user?.id,
}),
});
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
setCurrentStatus(MediaStatus.BLACKLISTED);
} else if (res.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsUpdating(false);
closeBlacklistModal();
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
};
const onClickShowBlacklistBtn = async (): Promise<void> => {
setIsUpdating(true);
const topNode = cardRef.current;
if (topNode) {
const res = await fetch('/api/v1/blacklist/' + id, {
method: 'DELETE',
});
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
setCurrentStatus(MediaStatus.UNKNOWN);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsUpdating(false);
};
const closeModal = useCallback(() => setShowRequestModal(false), []); const closeModal = useCallback(() => setShowRequestModal(false), []);
const showRequestButton = hasPermission( const showRequestButton = hasPermission(
@@ -178,10 +282,15 @@ const TitleCard = ({
{ type: 'or' } { type: 'or' }
); );
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
type: 'or',
});
return ( return (
<div <div
className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'} className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
data-testid="title-card" data-testid="title-card"
ref={cardRef}
> >
<RequestModal <RequestModal
tmdbId={id} tmdbId={id}
@@ -197,6 +306,20 @@ const TitleCard = ({
onUpdating={requestUpdating} onUpdating={requestUpdating}
onCancel={closeModal} onCancel={closeModal}
/> />
<BlacklistModal
tmdbId={id}
type={
mediaType === 'movie'
? 'movie'
: mediaType === 'collection'
? 'collection'
: 'tv'
}
show={showBlacklistModal}
onCancel={closeBlacklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isUpdating}
/>
<div <div
className={`relative transform-gpu cursor-default overflow-hidden rounded-xl bg-gray-800 bg-cover outline-none ring-1 transition duration-300 ${ className={`relative transform-gpu cursor-default overflow-hidden rounded-xl bg-gray-800 bg-cover outline-none ring-1 transition duration-300 ${
showDetail showDetail
@@ -235,7 +358,7 @@ const TitleCard = ({
/> />
<div className="absolute left-0 right-0 flex items-center justify-between p-2"> <div className="absolute left-0 right-0 flex items-center justify-between p-2">
<div <div
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${ className={`pointer-events-none z-40 self-start rounded-full border bg-opacity-80 shadow-md ${
mediaType === 'movie' || mediaType === 'collection' mediaType === 'movie' || mediaType === 'collection'
? 'border-blue-500 bg-blue-600' ? 'border-blue-500 bg-blue-600'
: 'border-purple-600 bg-purple-600' : 'border-purple-600 bg-purple-600'
@@ -249,8 +372,8 @@ const TitleCard = ({
: intl.formatMessage(globalMessages.tvshow)} : intl.formatMessage(globalMessages.tvshow)}
</div> </div>
</div> </div>
{showDetail && ( {showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
<> <div className="flex flex-col gap-1">
{toggleWatchlist ? ( {toggleWatchlist ? (
<Button <Button
buttonType={'ghost'} buttonType={'ghost'}
@@ -269,15 +392,49 @@ const TitleCard = ({
<MinusCircleIcon className={'h-3'} /> <MinusCircleIcon className={'h-3'} />
</Button> </Button>
)} )}
</> {showHideButton &&
currentStatus !== MediaStatus.PROCESSING &&
currentStatus !== MediaStatus.AVAILABLE &&
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
currentStatus !== MediaStatus.PENDING && (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => setShowBlacklistModal(true)}
>
<EyeSlashIcon className={'h-3'} />
</Button>
)}
</div>
)} )}
{showDetail &&
showHideButton &&
currentStatus == MediaStatus.BLACKLISTED && (
<Tooltip
content={intl.formatMessage(
globalMessages.removefromBlacklist
)}
>
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => onClickShowBlacklistBtn()}
>
<EyeIcon className={'h-3'} />
</Button>
</Tooltip>
)}
{currentStatus && currentStatus !== MediaStatus.UNKNOWN && ( {currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
<div className="pointer-events-none z-40 flex items-center"> <div className="flex flex-col items-center gap-1">
<StatusBadgeMini <div className="pointer-events-none z-40 flex">
status={currentStatus} <StatusBadgeMini
inProgress={inProgress} status={currentStatus}
shrink inProgress={inProgress}
/> shrink
/>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -4,6 +4,7 @@ import RTFresh from '@app/assets/rt_fresh.svg';
import RTRotten from '@app/assets/rt_rotten.svg'; import RTRotten from '@app/assets/rt_rotten.svg';
import Spinner from '@app/assets/spinner.svg'; import Spinner from '@app/assets/spinner.svg';
import TmdbLogo from '@app/assets/tmdb_logo.svg'; import TmdbLogo from '@app/assets/tmdb_logo.svg';
import BlacklistModal from '@app/components/BlacklistModal';
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
@@ -38,6 +39,7 @@ import {
ArrowRightCircleIcon, ArrowRightCircleIcon,
CogIcon, CogIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
EyeSlashIcon,
FilmIcon, FilmIcon,
PlayIcon, PlayIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
@@ -59,10 +61,9 @@ import type { Crew } from '@server/models/common';
import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import { countries } from 'country-flag-icons'; import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import getConfig from 'next/config';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -126,7 +127,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>( const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!tv?.onUserWatchlist !tv?.onUserWatchlist
); );
const { publicRuntimeConfig } = getConfig(); const [isBlacklistUpdating, setIsBlacklistUpdating] =
useState<boolean>(false);
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const { addToast } = useToasts(); const { addToast } = useToasts();
const { const {
@@ -157,6 +160,11 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false); setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]); }, [router.query.manage]);
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
[]
);
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: data?.mediaInfo?.mediaUrl, mediaUrl: data?.mediaInfo?.mediaUrl,
mediaUrl4k: data?.mediaInfo?.mediaUrl4k, mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
@@ -300,7 +308,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
?.flatrate ?? []; ?.flatrate ?? [];
function getAvalaibleMediaServerName() { function getAvalaibleMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
} }
@@ -312,15 +320,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
} }
function getAvalaible4kMediaServerName() { function getAvalaible4kMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
} }
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }); return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' });
} }
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' }); return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
} }
const onClickWatchlistBtn = async (): Promise<void> => { const onClickWatchlistBtn = async (): Promise<void> => {
@@ -399,6 +407,60 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
} }
}; };
const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true);
const res = await fetch('/api/v1/blacklist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: tv?.id,
mediaType: 'tv',
title: tv?.name,
user: user?.id,
}),
});
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
revalidate();
} else if (res.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsBlacklistUpdating(false);
closeBlacklistModal();
};
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
type: 'or',
});
return ( return (
<div <div
className="media-page" className="media-page"
@@ -425,6 +487,14 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</div> </div>
)} )}
<PageTitle title={data.name} /> <PageTitle title={data.name} />
<BlacklistModal
tmdbId={data.id}
type="tv"
show={showBlacklistModal}
onCancel={closeBlacklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isBlacklistUpdating}
/>
<IssueModal <IssueModal
onCancel={() => setShowIssueModal(false)} onCancel={() => setShowIssueModal(false)}
show={showIssueModal} show={showIssueModal}
@@ -530,40 +600,61 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</span> </span>
</div> </div>
<div className="media-actions"> <div className="media-actions">
<> {showHideButton &&
{toggleWatchlist ? ( data?.mediaInfo?.status !== MediaStatus.PROCESSING &&
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}> data?.mediaInfo?.status !== MediaStatus.AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PENDING &&
data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<Tooltip
content={intl.formatMessage(globalMessages.addToBlacklist)}
>
<Button <Button
buttonType={'ghost'} buttonType={'ghost'}
className="z-40 mr-2" className="z-40 mr-2"
buttonSize={'md'} buttonSize={'md'}
onClick={onClickWatchlistBtn} onClick={() => setShowBlacklistModal(true)}
> >
{isUpdating ? ( <EyeSlashIcon className={'h-3'} />
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
</> {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<>
{toggleWatchlist ? (
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<PlayButton links={mediaLinks} /> <PlayButton links={mediaLinks} />
<RequestButton <RequestButton
mediaType="tv" mediaType="tv"

View File

@@ -1,11 +1,11 @@
import Alert from '@app/components/Common/Alert'; import Alert from '@app/components/Common/Alert';
import CachedImage from '@app/components/Common/CachedImage';
import Modal from '@app/components/Common/Modal'; import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import getConfig from 'next/config';
import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -36,7 +36,6 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts(); const { addToast } = useToasts();
const [isImporting, setImporting] = useState(false); const [isImporting, setImporting] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]); const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -81,7 +80,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
userCount: createdUsers.length, userCount: createdUsers.length,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}), }),
{ {
autoDismiss: true, autoDismiss: true,
@@ -96,7 +97,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
addToast( addToast(
intl.formatMessage(messages.importfromJellyfinerror, { intl.formatMessage(messages.importfromJellyfinerror, {
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}), }),
{ {
autoDismiss: true, autoDismiss: true,
@@ -134,7 +137,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
loading={!data && !error} loading={!data && !error}
title={intl.formatMessage(messages.importfromJellyfin, { title={intl.formatMessage(messages.importfromJellyfin, {
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
})} })}
onOk={() => { onOk={() => {
importUsers(); importUsers();
@@ -151,7 +156,8 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<Alert <Alert
title={intl.formatMessage(messages.newJellyfinsigninenabled, { title={intl.formatMessage(messages.newJellyfinsigninenabled, {
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'Emby' ? 'Emby'
: 'Jellyfin', : 'Jellyfin',
strong: (msg: React.ReactNode) => ( strong: (msg: React.ReactNode) => (
@@ -243,7 +249,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
</td> </td>
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6"> <td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
<div className="flex items-center"> <div className="flex items-center">
<Image <CachedImage
className="h-10 w-10 flex-shrink-0 rounded-full" className="h-10 w-10 flex-shrink-0 rounded-full"
src={user.thumb} src={user.thumb}
alt="" alt=""
@@ -277,7 +283,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<Alert <Alert
title={intl.formatMessage(messages.noJellyfinuserstoimport, { title={intl.formatMessage(messages.noJellyfinuserstoimport, {
mediaServerName: mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
})} })}
type="info" type="info"
/> />

View File

@@ -1,6 +1,7 @@
import Alert from '@app/components/Common/Alert'; import Alert from '@app/components/Common/Alert';
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Header from '@app/components/Common/Header'; import Header from '@app/components/Common/Header';
import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal'; import Modal from '@app/components/Common/Modal';
@@ -28,8 +29,6 @@ import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions'; import { hasPermission } from '@server/lib/permissions';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -90,7 +89,6 @@ const UserList = () => {
const intl = useIntl(); const intl = useIntl();
const router = useRouter(); const router = useRouter();
const settings = useSettings(); const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts(); const { addToast } = useToasts();
const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const { user: currentUser, hasPermission: currentHasPermission } = useUser();
const [currentSort, setCurrentSort] = useState<Sort>('displayname'); const [currentSort, setCurrentSort] = useState<Sort>('displayname');
@@ -535,7 +533,8 @@ const UserList = () => {
> >
<InboxArrowDownIcon /> <InboxArrowDownIcon />
<span> <span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.importfrommediaserver, { ? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
@@ -634,7 +633,7 @@ const UserList = () => {
href={`/users/${user.id}`} href={`/users/${user.id}`}
className="h-10 w-10 flex-shrink-0" className="h-10 w-10 flex-shrink-0"
> >
<Image <CachedImage
className="h-10 w-10 rounded-full object-cover" className="h-10 w-10 rounded-full object-cover"
src={user.avatar} src={user.avatar}
alt="" alt=""
@@ -690,7 +689,7 @@ const UserList = () => {
<Badge badgeType="default"> <Badge badgeType="default">
{intl.formatMessage(messages.localuser)} {intl.formatMessage(messages.localuser)}
</Badge> </Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( ) : user.userType === UserType.EMBY ? (
<Badge badgeType="success"> <Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, { {intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Emby', mediaServerName: 'Emby',

View File

@@ -1,9 +1,9 @@
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import type { User } from '@app/hooks/useUser'; import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid'; import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -42,7 +42,7 @@ const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
<div className="flex items-end justify-items-end space-x-5"> <div className="flex items-end justify-items-end space-x-5">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div className="relative"> <div className="relative">
<Image <CachedImage
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700" className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
src={user.avatar} src={user.avatar}
alt="" alt=""

View File

@@ -14,9 +14,9 @@ import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error'; import ErrorPage from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error';
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -43,6 +43,7 @@ const messages = defineMessages(
user: 'User', user: 'User',
toastSettingsSuccess: 'Settings saved successfully!', toastSettingsSuccess: 'Settings saved successfully!',
toastSettingsFailure: 'Something went wrong while saving settings.', toastSettingsFailure: 'Something went wrong while saving settings.',
toastSettingsFailureEmail: 'This email is already taken!',
region: 'Discover Region', region: 'Discover Region',
regionTip: 'Filter content by regional availability', regionTip: 'Filter content by regional availability',
originallanguage: 'Discover Language', originallanguage: 'Discover Language',
@@ -69,7 +70,6 @@ const messages = defineMessages(
const UserGeneralSettings = () => { const UserGeneralSettings = () => {
const intl = useIntl(); const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts(); const { addToast } = useToasts();
const { locale, setLocale } = useLocale(); const { locale, setLocale } = useLocale();
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false); const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
@@ -180,7 +180,7 @@ const UserGeneralSettings = () => {
watchlistSyncTv: values.watchlistSyncTv, watchlistSyncTv: values.watchlistSyncTv,
}), }),
}); });
if (!res.ok) throw new Error(); if (!res.ok) throw new Error(res.statusText, { cause: res });
if (currentUser?.id === user?.id && setLocale) { if (currentUser?.id === user?.id && setLocale) {
setLocale( setLocale(
@@ -195,10 +195,24 @@ const UserGeneralSettings = () => {
appearance: 'success', appearance: 'success',
}); });
} catch (e) { } catch (e) {
addToast(intl.formatMessage(messages.toastSettingsFailure), { let errorData;
autoDismiss: true, try {
appearance: 'error', errorData = await e.cause?.text();
}); errorData = JSON.parse(errorData);
} catch {
/* empty */
}
if (errorData?.message === ApiErrorCode.InvalidEmail) {
addToast(intl.formatMessage(messages.toastSettingsFailureEmail), {
autoDismiss: true,
appearance: 'error',
});
} else {
addToast(intl.formatMessage(messages.toastSettingsFailure), {
autoDismiss: true,
appearance: 'error',
});
}
} finally { } finally {
revalidate(); revalidate();
revalidateUser(); revalidateUser();
@@ -229,7 +243,7 @@ const UserGeneralSettings = () => {
<Badge badgeType="default"> <Badge badgeType="default">
{intl.formatMessage(messages.localuser)} {intl.formatMessage(messages.localuser)}
</Badge> </Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( ) : user?.userType === UserType.EMBY ? (
<Badge badgeType="success"> <Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, { {intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Emby', mediaServerName: 'Emby',

View File

@@ -55,6 +55,16 @@ const globalMessages = defineMessages('i18n', {
noresults: 'No results.', noresults: 'No results.',
open: 'Open', open: 'Open',
resolved: 'Resolved', resolved: 'Resolved',
blacklist: 'Blacklist',
blacklisted: 'Blacklisted',
blacklistSuccess: '<strong>{title}</strong> was successfully blacklisted.',
blacklistError: 'Something went wrong try again.',
blacklistDuplicateError:
'<strong>{title}</strong> has already been blacklisted.',
removeFromBlacklistSuccess:
'<strong>{title}</strong> was successfully removed from the Blacklist.',
addToBlacklist: 'Add to Blacklist',
removefromBlacklist: 'Remove from Blacklist',
}); });
export default globalMessages; export default globalMessages;

View File

@@ -191,7 +191,7 @@
"components.Discover.TvGenreSlider.tvgenres": "Gèneres de Sèries", "components.Discover.TvGenreSlider.tvgenres": "Gèneres de Sèries",
"components.Discover.TvGenreList.seriesgenres": "Gèneres de Sèries", "components.Discover.TvGenreList.seriesgenres": "Gèneres de Sèries",
"components.Discover.StudioSlider.studios": "Estudis", "components.Discover.StudioSlider.studios": "Estudis",
"components.Discover.NetworkSlider.networks": "Plataformes", "components.Discover.NetworkSlider.networks": "Emissors",
"components.Discover.MovieGenreSlider.moviegenres": "Gèneres de Pel·lícules", "components.Discover.MovieGenreSlider.moviegenres": "Gèneres de Pel·lícules",
"components.Discover.MovieGenreList.moviegenres": "Gèneres de Pel·lícules", "components.Discover.MovieGenreList.moviegenres": "Gèneres de Pel·lícules",
"components.Discover.DiscoverTvLanguage.languageSeries": "Sèries en {language}", "components.Discover.DiscoverTvLanguage.languageSeries": "Sèries en {language}",
@@ -397,7 +397,7 @@
"components.TvDetails.originaltitle": "Títol original", "components.TvDetails.originaltitle": "Títol original",
"components.TvDetails.originallanguage": "Idioma original", "components.TvDetails.originallanguage": "Idioma original",
"components.TvDetails.nextAirDate": "Pròxima data d'emissió", "components.TvDetails.nextAirDate": "Pròxima data d'emissió",
"components.TvDetails.network": "{networkCount, plural, one {Plataforma} other {Plataformes}}", "components.TvDetails.network": "{networkCount, plural, one {Emissor} other {Emissors}}",
"components.TvDetails.firstAirDate": "Primera data d'emissió", "components.TvDetails.firstAirDate": "Primera data d'emissió",
"components.TvDetails.episodeRuntimeMinutes": "{runtime} minuts", "components.TvDetails.episodeRuntimeMinutes": "{runtime} minuts",
"components.TvDetails.episodeRuntime": "Duració de l'episodi", "components.TvDetails.episodeRuntime": "Duració de l'episodi",
@@ -494,7 +494,7 @@
"components.Settings.SonarrModal.validationNameRequired": "Heu de proporcionar un nom de servidor", "components.Settings.SonarrModal.validationNameRequired": "Heu de proporcionar un nom de servidor",
"components.Settings.SonarrModal.validationLanguageProfileRequired": "Heu de seleccionar un perfil d'idioma", "components.Settings.SonarrModal.validationLanguageProfileRequired": "Heu de seleccionar un perfil d'idioma",
"components.Settings.SonarrModal.validationHostnameRequired": "Heu de proporcionar un nom damfitrió o una adreça IP vàlides", "components.Settings.SonarrModal.validationHostnameRequired": "Heu de proporcionar un nom damfitrió o una adreça IP vàlides",
"components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "L'URL base no ha d'acabar amb una barra inclinada final", "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "L'URL base no pot acabar amb una barra inclinada final",
"components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "L'URL base ha de tenir una barra inclinada", "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "L'URL base ha de tenir una barra inclinada",
"components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "L'URL no pot acabar amb una barra inclinada final", "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "L'URL no pot acabar amb una barra inclinada final",
"components.Settings.SonarrModal.validationApplicationUrl": "Heu de proporcionar un URL vàlid", "components.Settings.SonarrModal.validationApplicationUrl": "Heu de proporcionar un URL vàlid",
@@ -1135,7 +1135,7 @@
"components.Discover.CreateSlider.needresults": "Cal tenir almenys 1 resultat.", "components.Discover.CreateSlider.needresults": "Cal tenir almenys 1 resultat.",
"components.Discover.CreateSlider.nooptions": "Sense resultats.", "components.Discover.CreateSlider.nooptions": "Sense resultats.",
"components.Discover.CreateSlider.providetmdbgenreid": "Proporciona un ID de categoria TMDB", "components.Discover.CreateSlider.providetmdbgenreid": "Proporciona un ID de categoria TMDB",
"components.Discover.CreateSlider.providetmdbnetwork": "Proporciona l'ID de la plataforma TMDB", "components.Discover.CreateSlider.providetmdbnetwork": "Proporciona l'ID d'emissor TMDB",
"components.Discover.CreateSlider.providetmdbstudio": "Proporciona l'ID d'estudi TMDB", "components.Discover.CreateSlider.providetmdbstudio": "Proporciona l'ID d'estudi TMDB",
"components.Discover.CreateSlider.searchGenres": "Cercar per gènere…", "components.Discover.CreateSlider.searchGenres": "Cercar per gènere…",
"components.Discover.CreateSlider.searchKeywords": "Cercar per paraules clau…", "components.Discover.CreateSlider.searchKeywords": "Cercar per paraules clau…",
@@ -1167,7 +1167,7 @@
"components.Discover.networks": "Emissors", "components.Discover.networks": "Emissors",
"components.Discover.resetwarning": "Restablir tots els controls lliscants al valor predeterminat. Això també suprimirà els controls lliscants personalitzats!", "components.Discover.resetwarning": "Restablir tots els controls lliscants al valor predeterminat. Això també suprimirà els controls lliscants personalitzats!",
"components.Discover.tmdbmoviekeyword": "Paraula clau de pel·lícula TMDB", "components.Discover.tmdbmoviekeyword": "Paraula clau de pel·lícula TMDB",
"components.Discover.tmdbnetwork": "Plataformes TMDB", "components.Discover.tmdbnetwork": "Emissors TMDB",
"components.Discover.FilterSlideover.tmdbuserscore": "Puntuació d'usuaris TMDB", "components.Discover.FilterSlideover.tmdbuserscore": "Puntuació d'usuaris TMDB",
"components.Discover.tvgenres": "Gèneres de sèries", "components.Discover.tvgenres": "Gèneres de sèries",
"components.Discover.DiscoverTvKeyword.keywordSeries": "Sèries {keywordTitle}", "components.Discover.DiscoverTvKeyword.keywordSeries": "Sèries {keywordTitle}",
@@ -1241,18 +1241,11 @@
"components.Settings.SettingsJobsCache.availability-sync": "Sincronització de disponibilitat de contingut", "components.Settings.SettingsJobsCache.availability-sync": "Sincronització de disponibilitat de contingut",
"components.Discover.tmdbmoviestreamingservices": "Serveis de transmissió de pel·lícules TMDB", "components.Discover.tmdbmoviestreamingservices": "Serveis de transmissió de pel·lícules TMDB",
"components.Discover.tmdbtvstreamingservices": "Serveis de transmissió de TV TMDB", "components.Discover.tmdbtvstreamingservices": "Serveis de transmissió de TV TMDB",
"components.Discover.FilterSlideover.tmdbuservotecount": "Recompte de vots dels usuaris de TMDB", "components.Layout.UserWarnings.emailRequired": "És requereix un n correu electrònic.",
"components.Discover.FilterSlideover.voteCount": "Número de vots entre {minValue} i {maxValue}", "components.Layout.UserWarnings.passwordRequired": "Es requereix una contrasenya.",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "So per a les notificacions", "components.Login.description": "Com que és la primera vegada que inicieu sessió a {applicationName}, es necessita afegir un correu electrònic vàlid.",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Dispositiu per defecte", "components.Discover.FilterSlideover.tmdbuservotecount": "Recompte de vots d'usuaris de TMDB",
"components.Settings.Notifications.NotificationsPushover.sound": "So per a les notificacions", "components.Discover.FilterSlideover.voteCount": "Nombre de vots entre {minValue} i {maxValue}",
"components.Settings.SonarrModal.animeSeriesType": "Tipus d'Anime", "components.Layout.UserWarnings.emailInvalid": "El correu electrònic no és vàlid.",
"components.Settings.SonarrModal.seriesType": "Tipus de sèrie", "components.Login.credentialerror": "El nom d'usuari o la contrasenya són incorrectes."
"components.Settings.SonarrModal.tagRequests": "Sol·licituds d'etiquetes",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Dispositiu per defecte",
"i18n.collection": "Col·lecció",
"components.MovieDetails.imdbuserscore": "Puntuació dels usuaris de IMDB",
"components.Settings.RadarrModal.tagRequests": "Sol·licituds d'etiqueta",
"components.Settings.RadarrModal.tagRequestsInfo": "Automàticament afegeix una etiqueta addicional amb el nom d'usuari i nom complet del sol·licitant",
"components.Settings.SonarrModal.tagRequestsInfo": "Automàticament afegeix una etiqueta addicional amb el nom d'usuari i nom complet del sol·licitant"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,18 @@
{ {
"component.BlacklistBlock.blacklistdate": "Blacklisted date",
"component.BlacklistBlock.blacklistedby": "Blacklisted By",
"component.BlacklistModal.blacklisting": "Blacklisting",
"components.AirDateBadge.airedrelative": "Aired {relativeTime}", "components.AirDateBadge.airedrelative": "Aired {relativeTime}",
"components.AirDateBadge.airsrelative": "Airing {relativeTime}", "components.AirDateBadge.airsrelative": "Airing {relativeTime}",
"components.AppDataWarning.dockerVolumeMissingDescription": "The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.", "components.AppDataWarning.dockerVolumeMissingDescription": "The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.",
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> is not blacklisted.",
"components.Blacklist.blacklistSettingsDescription": "Manage blacklisted media.",
"components.Blacklist.blacklistdate": "date",
"components.Blacklist.blacklistedby": "{date} by {user}",
"components.Blacklist.blacklistsettings": "Blacklist Settings",
"components.Blacklist.mediaName": "Name",
"components.Blacklist.mediaTmdbId": "tmdb Id",
"components.Blacklist.mediaType": "Type",
"components.CollectionDetails.numberofmovies": "{count} Movies", "components.CollectionDetails.numberofmovies": "{count} Movies",
"components.CollectionDetails.overview": "Overview", "components.CollectionDetails.overview": "Overview",
"components.CollectionDetails.requestcollection": "Request Collection", "components.CollectionDetails.requestcollection": "Request Collection",
@@ -73,6 +84,7 @@
"components.Discover.FilterSlideover.releaseDate": "Release Date", "components.Discover.FilterSlideover.releaseDate": "Release Date",
"components.Discover.FilterSlideover.runtime": "Runtime", "components.Discover.FilterSlideover.runtime": "Runtime",
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime", "components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime",
"components.Discover.FilterSlideover.status": "Status",
"components.Discover.FilterSlideover.streamingservices": "Streaming Services", "components.Discover.FilterSlideover.streamingservices": "Streaming Services",
"components.Discover.FilterSlideover.studio": "Studio", "components.Discover.FilterSlideover.studio": "Studio",
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score", "components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
@@ -199,6 +211,7 @@
"components.LanguageSelector.originalLanguageDefault": "All Languages", "components.LanguageSelector.originalLanguageDefault": "All Languages",
"components.Layout.LanguagePicker.displaylanguage": "Display Language", "components.Layout.LanguagePicker.displaylanguage": "Display Language",
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV", "components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
"components.Layout.Sidebar.blacklist": "Blacklist",
"components.Layout.Sidebar.browsemovies": "Movies", "components.Layout.Sidebar.browsemovies": "Movies",
"components.Layout.Sidebar.browsetv": "Series", "components.Layout.Sidebar.browsetv": "Series",
"components.Layout.Sidebar.dashboard": "Discover", "components.Layout.Sidebar.dashboard": "Discover",
@@ -220,6 +233,7 @@
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop", "components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable", "components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
"components.Login.adminerror": "You must use an admin account to sign in.", "components.Login.adminerror": "You must use an admin account to sign in.",
"components.Login.back": "Go back",
"components.Login.credentialerror": "The username or password is incorrect.", "components.Login.credentialerror": "The username or password is incorrect.",
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.", "components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
"components.Login.email": "Email Address", "components.Login.email": "Email Address",
@@ -235,6 +249,7 @@
"components.Login.port": "Port", "components.Login.port": "Port",
"components.Login.save": "Add", "components.Login.save": "Add",
"components.Login.saving": "Adding…", "components.Login.saving": "Adding…",
"components.Login.servertype": "Server Type",
"components.Login.signin": "Sign In", "components.Login.signin": "Sign In",
"components.Login.signingin": "Signing In…", "components.Login.signingin": "Signing In…",
"components.Login.signinheader": "Sign in to continue", "components.Login.signinheader": "Sign in to continue",
@@ -256,6 +271,7 @@
"components.Login.validationhostformat": "Valid URL required", "components.Login.validationhostformat": "Valid URL required",
"components.Login.validationhostrequired": "{mediaServerName} URL required", "components.Login.validationhostrequired": "{mediaServerName} URL required",
"components.Login.validationpasswordrequired": "You must provide a password", "components.Login.validationpasswordrequired": "You must provide a password",
"components.Login.validationservertyperequired": "Please select a server type",
"components.Login.validationusernamerequired": "Username required", "components.Login.validationusernamerequired": "Username required",
"components.ManageSlideOver.alltime": "All Time", "components.ManageSlideOver.alltime": "All Time",
"components.ManageSlideOver.downloadstatus": "Downloads", "components.ManageSlideOver.downloadstatus": "Downloads",
@@ -282,6 +298,7 @@
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {play} other {plays}}", "components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {play} other {plays}}",
"components.ManageSlideOver.removearr": "Remove from {arr}", "components.ManageSlideOver.removearr": "Remove from {arr}",
"components.ManageSlideOver.removearr4k": "Remove from 4K {arr}", "components.ManageSlideOver.removearr4k": "Remove from 4K {arr}",
"components.RequestList.RequestItem.removearr": "Remove from {arr}",
"components.ManageSlideOver.tvshow": "series", "components.ManageSlideOver.tvshow": "series",
"components.MediaSlider.ShowMoreCard.seemore": "See More", "components.MediaSlider.ShowMoreCard.seemore": "See More",
"components.MovieDetails.MovieCast.fullcast": "Full Cast", "components.MovieDetails.MovieCast.fullcast": "Full Cast",
@@ -383,8 +400,12 @@
"components.PermissionEdit.autorequestMoviesDescription": "Grant permission to automatically submit requests for non-4K movies via Plex Watchlist.", "components.PermissionEdit.autorequestMoviesDescription": "Grant permission to automatically submit requests for non-4K movies via Plex Watchlist.",
"components.PermissionEdit.autorequestSeries": "Auto-Request Series", "components.PermissionEdit.autorequestSeries": "Auto-Request Series",
"components.PermissionEdit.autorequestSeriesDescription": "Grant permission to automatically submit requests for non-4K series via Plex Watchlist.", "components.PermissionEdit.autorequestSeriesDescription": "Grant permission to automatically submit requests for non-4K series via Plex Watchlist.",
"components.PermissionEdit.blacklistedItems": "Blacklist media.",
"components.PermissionEdit.blacklistedItemsDescription": "Grant permission to blacklist media.",
"components.PermissionEdit.createissues": "Report Issues", "components.PermissionEdit.createissues": "Report Issues",
"components.PermissionEdit.createissuesDescription": "Grant permission to report media issues.", "components.PermissionEdit.createissuesDescription": "Grant permission to report media issues.",
"components.PermissionEdit.manageblacklist": "Manage Blacklist",
"components.PermissionEdit.manageblacklistDescription": "Grant permission to manage blacklisted media.",
"components.PermissionEdit.manageissues": "Manage Issues", "components.PermissionEdit.manageissues": "Manage Issues",
"components.PermissionEdit.manageissuesDescription": "Grant permission to manage media issues.", "components.PermissionEdit.manageissuesDescription": "Grant permission to manage media issues.",
"components.PermissionEdit.managerequests": "Manage Requests", "components.PermissionEdit.managerequests": "Manage Requests",
@@ -403,6 +424,8 @@
"components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.", "components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.",
"components.PermissionEdit.users": "Manage Users", "components.PermissionEdit.users": "Manage Users",
"components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.", "components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
"components.PermissionEdit.viewblacklistedItems": "View blacklisted media.",
"components.PermissionEdit.viewblacklistedItemsDescription": "Grant permission to view blacklisted media.",
"components.PermissionEdit.viewissues": "View Issues", "components.PermissionEdit.viewissues": "View Issues",
"components.PermissionEdit.viewissuesDescription": "Grant permission to view media issues reported by other users.", "components.PermissionEdit.viewissuesDescription": "Grant permission to view media issues reported by other users.",
"components.PermissionEdit.viewrecent": "View Recently Added", "components.PermissionEdit.viewrecent": "View Recently Added",
@@ -555,9 +578,16 @@
"components.ResetPassword.validationpasswordrequired": "You must provide a password", "components.ResetPassword.validationpasswordrequired": "You must provide a password",
"components.Search.search": "Search", "components.Search.search": "Search",
"components.Search.searchresults": "Search Results", "components.Search.searchresults": "Search Results",
"components.Selector.canceled": "Canceled",
"components.Selector.ended": "Ended",
"components.Selector.inProduction": "In Production",
"components.Selector.nooptions": "No results.", "components.Selector.nooptions": "No results.",
"components.Selector.pilot": "Pilot",
"components.Selector.planned": "Planned",
"components.Selector.returningSeries": "Returning Series",
"components.Selector.searchGenres": "Select genres…", "components.Selector.searchGenres": "Select genres…",
"components.Selector.searchKeywords": "Search keywords…", "components.Selector.searchKeywords": "Search keywords…",
"components.Selector.searchStatus": "Select status...",
"components.Selector.searchStudios": "Search studios…", "components.Selector.searchStudios": "Search studios…",
"components.Selector.showless": "Show Less", "components.Selector.showless": "Show Less",
"components.Selector.showmore": "Show More", "components.Selector.showmore": "Show More",
@@ -820,6 +850,7 @@
"components.Settings.SettingsJobsCache.runnow": "Run Now", "components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan", "components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job", "components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
"components.Settings.SettingsJobsCache.usersavatars": "Users' Avatars",
"components.Settings.SettingsLogs.copiedLogMessage": "Copied log message to clipboard.", "components.Settings.SettingsLogs.copiedLogMessage": "Copied log message to clipboard.",
"components.Settings.SettingsLogs.copyToClipboard": "Copy to Clipboard", "components.Settings.SettingsLogs.copyToClipboard": "Copy to Clipboard",
"components.Settings.SettingsLogs.extraData": "Additional Data", "components.Settings.SettingsLogs.extraData": "Additional Data",
@@ -939,6 +970,7 @@
"components.Settings.address": "Address", "components.Settings.address": "Address",
"components.Settings.addsonarr": "Add Sonarr Server", "components.Settings.addsonarr": "Add Sonarr Server",
"components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality", "components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality",
"components.Settings.apiKey": "API key",
"components.Settings.cancelscan": "Cancel Scan", "components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard.", "components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.currentlibrary": "Current Library: {name}", "components.Settings.currentlibrary": "Current Library: {name}",
@@ -999,6 +1031,7 @@
"components.Settings.save": "Save Changes", "components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving…", "components.Settings.saving": "Saving…",
"components.Settings.scan": "Sync Libraries", "components.Settings.scan": "Sync Libraries",
"components.Settings.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
"components.Settings.scanning": "Syncing…", "components.Settings.scanning": "Syncing…",
"components.Settings.serverLocal": "local", "components.Settings.serverLocal": "local",
"components.Settings.serverRemote": "remote", "components.Settings.serverRemote": "remote",
@@ -1019,6 +1052,7 @@
"components.Settings.tautulliSettings": "Tautulli Settings", "components.Settings.tautulliSettings": "Tautulli Settings",
"components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Jellyseerr fetches watch history data for your Plex media from Tautulli.", "components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Jellyseerr fetches watch history data for your Plex media from Tautulli.",
"components.Settings.timeout": "Timeout", "components.Settings.timeout": "Timeout",
"components.Settings.tip": "Tip",
"components.Settings.toastPlexConnecting": "Attempting to connect to Plex…", "components.Settings.toastPlexConnecting": "Attempting to connect to Plex…",
"components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.", "components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.",
"components.Settings.toastPlexConnectingSuccess": "Plex connection established successfully!", "components.Settings.toastPlexConnectingSuccess": "Plex connection established successfully!",
@@ -1039,23 +1073,29 @@
"components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app", "components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app",
"components.Settings.webhook": "Webhook", "components.Settings.webhook": "Webhook",
"components.Settings.webpush": "Web Push", "components.Settings.webpush": "Web Push",
"components.Setup.back": "Go back",
"components.Setup.configemby": "Configure Emby",
"components.Setup.configjellyfin": "Configure Jellyfin",
"components.Setup.configplex": "Configure Plex",
"components.Setup.configuremediaserver": "Configure Media Server", "components.Setup.configuremediaserver": "Configure Media Server",
"components.Setup.configureservices": "Configure Services", "components.Setup.configureservices": "Configure Services",
"components.Setup.continue": "Continue", "components.Setup.continue": "Continue",
"components.Setup.finish": "Finish Setup", "components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing…", "components.Setup.finishing": "Finishing…",
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.", "components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup", "components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In", "components.Setup.signin": "Sign in to your account",
"components.Setup.signinMessage": "Get started by signing in", "components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithJellyfin": "Use your {mediaServerName} account", "components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithPlex": "Use your Plex account", "components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
"components.Setup.tip": "Tip", "components.Setup.signinWithPlex": "Enter your Plex details",
"components.Setup.subtitle": "Get started by choosing your media server",
"components.Setup.welcome": "Welcome to Jellyseerr", "components.Setup.welcome": "Welcome to Jellyseerr",
"components.StatusBadge.managemedia": "Manage {mediaType}", "components.StatusBadge.managemedia": "Manage {mediaType}",
"components.StatusBadge.openinarr": "Open in {arr}", "components.StatusBadge.openinarr": "Open in {arr}",
"components.StatusBadge.playonplex": "Play on {mediaServerName}", "components.StatusBadge.playonplex": "Play on {mediaServerName}",
"components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}", "components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}",
"components.StatusBadge.seasonnumber": "S{seasonNumber}",
"components.StatusBadge.status": "{status}", "components.StatusBadge.status": "{status}",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.StatusChecker.appUpdated": "{applicationTitle} Updated", "components.StatusChecker.appUpdated": "{applicationTitle} Updated",
@@ -1194,6 +1234,7 @@
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Saving…", "components.UserProfile.UserSettings.UserGeneralSettings.saving": "Saving…",
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Series Request Limit", "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Series Request Limit",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "This email is already taken!",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User", "components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID", "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
@@ -1273,6 +1314,7 @@
"components.UserProfile.seriesrequest": "Series Requests", "components.UserProfile.seriesrequest": "Series Requests",
"components.UserProfile.totalrequests": "Total Requests", "components.UserProfile.totalrequests": "Total Requests",
"components.UserProfile.unlimited": "Unlimited", "components.UserProfile.unlimited": "Unlimited",
"i18n.addToBlacklist": "Add to Blacklist",
"i18n.advanced": "Advanced", "i18n.advanced": "Advanced",
"i18n.all": "All", "i18n.all": "All",
"i18n.approve": "Approve", "i18n.approve": "Approve",
@@ -1280,6 +1322,11 @@
"i18n.areyousure": "Are you sure?", "i18n.areyousure": "Are you sure?",
"i18n.available": "Available", "i18n.available": "Available",
"i18n.back": "Back", "i18n.back": "Back",
"i18n.blacklist": "Blacklist",
"i18n.blacklistDuplicateError": "<strong>{title}</strong> has already been blacklisted.",
"i18n.blacklistError": "Something went wrong try again.",
"i18n.blacklistSuccess": "<strong>{title}</strong> was successfully blacklisted.",
"i18n.blacklisted": "Blacklisted",
"i18n.cancel": "Cancel", "i18n.cancel": "Cancel",
"i18n.canceling": "Canceling…", "i18n.canceling": "Canceling…",
"i18n.close": "Close", "i18n.close": "Close",
@@ -1305,6 +1352,8 @@
"i18n.pending": "Pending", "i18n.pending": "Pending",
"i18n.previous": "Previous", "i18n.previous": "Previous",
"i18n.processing": "Processing", "i18n.processing": "Processing",
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> was successfully removed from the Blacklist.",
"i18n.removefromBlacklist": "Remove from Blacklist",
"i18n.request": "Request", "i18n.request": "Request",
"i18n.request4k": "Request in 4K", "i18n.request4k": "Request in 4K",
"i18n.requested": "Requested", "i18n.requested": "Requested",

View File

@@ -201,7 +201,7 @@
"components.Settings.SonarrModal.animerootfolder": "Carpeta raíz de anime", "components.Settings.SonarrModal.animerootfolder": "Carpeta raíz de anime",
"components.Settings.SonarrModal.animequalityprofile": "Perfil de calidad de anime", "components.Settings.SonarrModal.animequalityprofile": "Perfil de calidad de anime",
"components.Settings.SettingsAbout.timezone": "Zona horaria", "components.Settings.SettingsAbout.timezone": "Zona horaria",
"components.Settings.SettingsAbout.supportoverseerr": "Apoya a Jellyseerr", "components.Settings.SettingsAbout.supportoverseerr": "Apoya a Overseerr",
"components.Settings.SettingsAbout.helppaycoffee": "Ayúdame invitándome a un café", "components.Settings.SettingsAbout.helppaycoffee": "Ayúdame invitándome a un café",
"components.Settings.SettingsAbout.Releases.viewongithub": "Ver en GitHub", "components.Settings.SettingsAbout.Releases.viewongithub": "Ver en GitHub",
"components.Settings.SettingsAbout.Releases.viewchangelog": "Ver registro de cambios", "components.Settings.SettingsAbout.Releases.viewchangelog": "Ver registro de cambios",
@@ -299,14 +299,14 @@
"components.RequestButton.viewrequest": "Ver Solicitud", "components.RequestButton.viewrequest": "Ver Solicitud",
"components.RequestButton.requestmore4k": "Solicitar más en 4K", "components.RequestButton.requestmore4k": "Solicitar más en 4K",
"components.RequestButton.requestmore": "Solicitar más", "components.RequestButton.requestmore": "Solicitar más",
"components.RequestButton.declinerequests": "Rechazar {requestCount, plural, one {solicitud} other {{requestCount} solicitudes}}", "components.RequestButton.declinerequests": "Rechazar {requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.declinerequest4k": "Rechazar Solicitud 4K", "components.RequestButton.declinerequest4k": "Rechazar Solicitud 4K",
"components.RequestButton.declinerequest": "Rechazar Solicitud", "components.RequestButton.declinerequest": "Rechazar Solicitud",
"components.RequestButton.decline4krequests": "Rechazar {requestCount, plural, one {solicitud en 4K} other {{requestCount} solicitudes en 4K}}", "components.RequestButton.decline4krequests": "Rechazar {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
"components.RequestButton.approverequests": "Aprobar {requestCount, plural, one {solicitud} other {{requestCount} solicitudes}}", "components.RequestButton.approverequests": "Aprobar {requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.approverequest4k": "Aprobar Solicitud 4K", "components.RequestButton.approverequest4k": "Aprobar Solicitud 4K",
"components.RequestButton.approverequest": "Aprobar Solicitud", "components.RequestButton.approverequest": "Aprobar Solicitud",
"components.RequestButton.approve4krequests": "Aprobar {requestCount, plural, one {petición en 4K} other {requestCount} peticiones en 4K}}", "components.RequestButton.approve4krequests": "Aprobar {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
"components.RequestBlock.server": "Servidor de Destino", "components.RequestBlock.server": "Servidor de Destino",
"components.RequestBlock.rootfolder": "Carpeta Raíz", "components.RequestBlock.rootfolder": "Carpeta Raíz",
"components.RequestBlock.profilechanged": "Perfil de Calidad", "components.RequestBlock.profilechanged": "Perfil de Calidad",
@@ -656,7 +656,7 @@
"components.QuotaSelector.unlimited": "Ilimitadas", "components.QuotaSelector.unlimited": "Ilimitadas",
"components.MovieDetails.originaltitle": "Título Original", "components.MovieDetails.originaltitle": "Título Original",
"components.LanguageSelector.originalLanguageDefault": "Todos los Idiomas", "components.LanguageSelector.originalLanguageDefault": "Todos los Idiomas",
"components.LanguageSelector.languageServerDefault": "({Language}) por defecto", "components.LanguageSelector.languageServerDefault": "({language}) por defecto",
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {cambio} other {cambios}} por detrás", "components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {cambio} other {cambios}} por detrás",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Tu cuenta no tiene configurada una contraseña actualmente. Configure una contraseña a continuación para habilitar el acceso como \"usuario local\" utilizando tu dirección de email.", "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Tu cuenta no tiene configurada una contraseña actualmente. Configure una contraseña a continuación para habilitar el acceso como \"usuario local\" utilizando tu dirección de email.",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Esta cuenta de usuario no tiene configurada una contraseña actualmente. Configure una contraseña a continuación para habilitar el acceso como \"usuario local\"", "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Esta cuenta de usuario no tiene configurada una contraseña actualmente. Configure una contraseña a continuación para habilitar el acceso como \"usuario local\"",
@@ -784,7 +784,7 @@
"components.DownloadBlock.estimatedtime": "Estimación de {time}", "components.DownloadBlock.estimatedtime": "Estimación de {time}",
"components.Settings.Notifications.encryptionOpportunisticTls": "Usa siempre STARTTLS", "components.Settings.Notifications.encryptionOpportunisticTls": "Usa siempre STARTTLS",
"components.TvDetails.streamingproviders": "Emisión Actual en", "components.TvDetails.streamingproviders": "Emisión Actual en",
"components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "{{Language}} por defecto", "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "({language}) por defecto",
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Para recibir notificaciones web push, Jellyseerr debe servirse mediante HTTPS.", "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Para recibir notificaciones web push, Jellyseerr debe servirse mediante HTTPS.",
"components.Settings.Notifications.NotificationsWebhook.validationTypes": "Debes seleccionar, al menos, un tipo de notificación", "components.Settings.Notifications.NotificationsWebhook.validationTypes": "Debes seleccionar, al menos, un tipo de notificación",
"components.Settings.Notifications.validationTypes": "Debes seleccionar, al menos, un tipo de notificación", "components.Settings.Notifications.validationTypes": "Debes seleccionar, al menos, un tipo de notificación",
@@ -816,10 +816,10 @@
"components.Settings.Notifications.encryptionTip": "Normalmente, TLS Implícito usa el puerto 465 y STARTTLS usa el puerto 587", "components.Settings.Notifications.encryptionTip": "Normalmente, TLS Implícito usa el puerto 465 y STARTTLS usa el puerto 587",
"components.UserList.localLoginDisabled": "El ajuste para <strong>Habilitar el Inicio de Sesión Local</strong> está actualmente deshabilitado.", "components.UserList.localLoginDisabled": "El ajuste para <strong>Habilitar el Inicio de Sesión Local</strong> está actualmente deshabilitado.",
"components.Settings.SettingsUsers.defaultPermissionsTip": "Permisos iniciales asignados a nuevos usuarios", "components.Settings.SettingsUsers.defaultPermissionsTip": "Permisos iniciales asignados a nuevos usuarios",
"components.Settings.SettingsAbout.runningDevelop": "Estás utilizando la rama de <code>develop</code> de Jellyseerr, la cual solo se recomienda para aquellos que contribuyen al desarrollo o al soporte de las pruebas de nuevos desarrollos.", "components.Settings.SettingsAbout.runningDevelop": "Estás utilizando la rama de <code>desarrollo</code> de Jellyseerr, la cual solo se recomienda para aquellos que contribuyen al desarrollo o al soporte de las pruebas de nuevos desarrollos.",
"components.StatusBadge.status": "{status}", "components.StatusBadge.status": "{status}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Cada {jobScheduleMinutes, plural, one {minuto} other {{jobScheduleMinutes} minutos}}", "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Cada {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Cada {jobScheduleHours, plural, one {hora} other {{jobScheduleHours} horas}}", "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Cada {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Algo fue mal al guardar la tarea programada.", "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Algo fue mal al guardar la tarea programada.",
"components.Settings.SettingsJobsCache.editJobSchedule": "Modificar tarea programada", "components.Settings.SettingsJobsCache.editJobSchedule": "Modificar tarea programada",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Nueva frecuencia", "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Nueva frecuencia",
@@ -848,7 +848,7 @@
"components.IssueDetails.nocomments": "Sin comentarios.", "components.IssueDetails.nocomments": "Sin comentarios.",
"components.IssueDetails.openedby": "#{issueId} abierta {relativeTime} por {username}", "components.IssueDetails.openedby": "#{issueId} abierta {relativeTime} por {username}",
"components.IssueDetails.openin4karr": "Abrir en {arr} 4K", "components.IssueDetails.openin4karr": "Abrir en {arr} 4K",
"components.IssueDetails.openinarr": "Abierta en {arr}", "components.IssueDetails.openinarr": "Abrir en {arr}",
"components.IssueDetails.play4konplex": "Ver en 4K en {mediaServerName}", "components.IssueDetails.play4konplex": "Ver en 4K en {mediaServerName}",
"components.IssueDetails.playonplex": "Ver en {mediaServerName}", "components.IssueDetails.playonplex": "Ver en {mediaServerName}",
"components.IssueDetails.problemepisode": "Episodio Afectado", "components.IssueDetails.problemepisode": "Episodio Afectado",
@@ -1193,7 +1193,7 @@
"components.RequestList.RequestItem.tvdbid": "Identificador de TheTVDB", "components.RequestList.RequestItem.tvdbid": "Identificador de TheTVDB",
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Limpieza de la caché de imágenes", "components.Settings.SettingsJobsCache.image-cache-cleanup": "Limpieza de la caché de imágenes",
"components.Settings.SettingsJobsCache.imagecache": "Caché de imágenes", "components.Settings.SettingsJobsCache.imagecache": "Caché de imágenes",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Cada {jobScheduleSeconds, plural, one {segundo} other {{jobScheduleSeconds} segundos}}", "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Cada {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
"components.Settings.SettingsJobsCache.availability-sync": "Sincronización de la disponibilidad de medios", "components.Settings.SettingsJobsCache.availability-sync": "Sincronización de la disponibilidad de medios",
"components.Discover.tmdbmoviestreamingservices": "Servicios de streaming de películas TMDB", "components.Discover.tmdbmoviestreamingservices": "Servicios de streaming de películas TMDB",
"components.Discover.tmdbtvstreamingservices": "Servicios de TV en streaming TMDB", "components.Discover.tmdbtvstreamingservices": "Servicios de TV en streaming TMDB",
@@ -1205,10 +1205,110 @@
"components.Settings.RadarrModal.tagRequestsInfo": "Añadir automáticamente una etiqueta adicional con el nombre de usuario y el nombre para mostrar del solicitante", "components.Settings.RadarrModal.tagRequestsInfo": "Añadir automáticamente una etiqueta adicional con el nombre de usuario y el nombre para mostrar del solicitante",
"components.Settings.SonarrModal.tagRequestsInfo": "Añadir automáticamente una etiqueta adicional con el nombre de usuario y el nombre para mostrar del solicitante", "components.Settings.SonarrModal.tagRequestsInfo": "Añadir automáticamente una etiqueta adicional con el nombre de usuario y el nombre para mostrar del solicitante",
"components.MovieDetails.imdbuserscore": "Puntuación de los usuarios de IMDB", "components.MovieDetails.imdbuserscore": "Puntuación de los usuarios de IMDB",
"components.Settings.SonarrModal.animeSeriesType": "Tipo de anime", "components.Layout.UserWarnings.passwordRequired": "Se requiere una contraseña.",
"components.Settings.SonarrModal.seriesType": "Tipo de series", "components.Login.credentialerror": "El usuario o contraseña es incorrecto.",
"components.Settings.Notifications.NotificationsPushover.sound": "Sonido para las notificaciones", "components.Login.host": "{mediaServerName} URL",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Dispositivo predeterminado", "components.Login.initialsignin": "Conectar",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Sonido para las notificaciones", "components.Login.initialsigningin": "Conectando…",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Dispositivo predeterminado" "components.Login.emailtooltip": "No es necesario asociar la dirección con su instancia de {mediaServerName}.",
"components.Login.saving": "Añadiendo…",
"components.Login.title": "Añadir Email",
"components.Login.username": "Nombre de usuario",
"components.Login.validationEmailFormat": "El email es inválido",
"components.Login.validationEmailRequired": "Debes proporcional un email",
"components.Login.validationemailformat": "Se requiere de un email válido",
"components.Login.validationhostformat": "Se requiere de una URL válida",
"components.Login.validationusernamerequired": "Se requiere un nombre de usuario",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Esto eliminará de manera irreversible esta {mediaType} de {arr}, incluyendo todos los archivos.",
"i18n.open": "Abierto",
"components.MovieDetails.downloadstatus": "Estado de la descarga",
"components.MovieDetails.openradarr": "Abrir Película en Radarr",
"components.MovieDetails.openradarr4k": "Abrir Película 4K en Radarr",
"components.MovieDetails.play": "Reproducir en {mediaServerName}",
"components.MovieDetails.play4k": "Reproducir 4K en {mediaServerName}",
"components.NotificationTypeSelector.issueresolved": "Incidencia Resuelta",
"components.NotificationTypeSelector.userissuecommentDescription": "Notificame cuando haya nuevos comentarios en incidencias que haya abierto.",
"components.NotificationTypeSelector.userissuecreatedDescription": "Notificame cuando otros usuarios reporten incidencias.",
"components.PermissionEdit.viewissues": "Ver incidencias",
"components.PermissionEdit.manageissuesDescription": "Dar permiso para administrar incidencias.",
"components.PermissionEdit.viewissuesDescription": "Dar permiso para ver incidencias reportadas por otros usuarios.",
"components.Settings.Notifications.NotificationsPushover.sound": "Sonido de Notificacion",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Escanear Añadidos Recientemente de Jellyfin",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Escaneo Completo de la libreria de Jellyfin",
"components.Settings.jellyfinSettings": "Ajustes de {mediaServerName}",
"components.Settings.jellyfinsettings": "Ajustes de {mediaServerName}",
"components.Settings.jellyfinSettingsFailure": "Algo salió mal al guardar la configuración de {mediaServerName}.",
"components.Settings.jellyfinSettingsSuccess": "¡La configuración de {mediaServerName} se guardó correctamente!",
"components.Settings.jellyfinlibraries": "Bibliotecas {mediaServerName}",
"components.Settings.jellyfinlibrariesDescription": "La biblioteca {mediaServerName} busca títulos. Haga clic en el botón a continuación si no aparece ninguna biblioteca.",
"components.Settings.manualscanDescriptionJellyfin": "Normalmente, esto sólo se ejecutará una vez cada 24 horas. Jellyseerr comprobará de forma más agresiva los añadidos recientemente de su servidor {mediaServerName}. ¡Si es la primera vez que configura Jellyseerr, se recomienda un escaneo manual completo de la biblioteca!",
"components.Settings.save": "Guardar Cambios",
"components.Settings.saving": "Guardando…",
"components.Settings.syncing": "Sincronizando",
"components.Settings.timeout": "Tiempo agotado",
"components.Setup.signinWithPlex": "Usa tu cuenta de Plex",
"components.Setup.configuremediaserver": "Configurar servidor multimedia",
"components.TitleCard.addToWatchList": "Añadir a lista de seguimiento",
"components.TitleCard.watchlistCancel": "Lista de seguimiento para <strong>{title}</strong> cancelada.",
"components.TitleCard.watchlistError": "Algo salió mal, intenta de nuevo.",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> añadido correctamente a la lista de seguimiento!",
"components.TvDetails.play": "Reproducir en {mediaServerName}",
"components.UserList.importfromJellyfin": "Importar Usuarios de {mediaServerName}",
"components.UserList.mediaServerUser": "Usuario de {mediaServerName}",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} importado correctamente!",
"components.UserList.importfromJellyfinerror": "Se produjo un error al importar usuarios de {mediaServerName}.",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Guardar Cambios",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Email",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Dispositivo Predeterminado",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Sonido de Notificacion",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Fallo al guardar los ajustes de la notificación Pushbullet.",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "¡Los ajustes de notificación Pushbullet se han guardado con éxito!",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "Token de aplicación API",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Clave de usuario o grupo",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "Tu <UsersGroupsLink>identificador de usuario o grupo</UsersGroupsLink> de 30 caracteres",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "¡Se han guardado los ajustes de notificación de Pushover!",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "<ApplicationRegistrationLink>Registrar una aplicación</ApplicationRegistrationLink> para usar con {applicationTitle}",
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Debes proporcionar una clave de usuario o grupo válida",
"components.Login.validationhostrequired": "{mediaServerName} URL requerida",
"i18n.resolved": "Resuelto",
"components.UserList.importfrommediaserver": "Importar Usuarios de {mediaServerName}",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Dispositivo Predeterminado",
"components.ManageSlideOver.removearr": "Eliminar de {arr}",
"components.NotificationTypeSelector.issuereopenedDescription": "Enviar notificación cuando se reabran incidencias.",
"components.Layout.UserWarnings.emailRequired": "Se requiere una dirección de email.",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Token de Acceso",
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Debes proporcionar un token de aplicación válido",
"components.Settings.syncJellyfin": "Sincronizar Bibliotecas",
"components.Layout.UserWarnings.emailInvalid": "La dirección de correo es inválida.",
"components.Login.description": "Como es la primera vez que inicias sesión en {applicationName}, es necesario añadir una dirección de email válida.",
"components.Login.save": "Añadir",
"components.NotificationTypeSelector.issueresolvedDescription": "Enviar notificación cuando se resuelvan incidencias.",
"components.PermissionEdit.manageissues": "Administrar incidencias",
"components.PermissionEdit.createissues": "Notificar incidencia",
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Guardando…",
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "Debes indicar un token de acceso",
"components.Login.signinwithjellyfin": "Utiliza tu cuenta de {mediaServerName}",
"components.ManageSlideOver.removearr4k": "Eliminar de {arr} 4K",
"components.Settings.internalUrl": "URL Interna",
"components.TvDetails.play4k": "Reproducir 4K en {mediaServerName}",
"components.Setup.signin": "Iniciar Sesión",
"components.Setup.signinWithJellyfin": "Utiliza tu cuenta de {mediaServerName}",
"components.UserList.noJellyfinuserstoimport": "No hay usuarios de {mediaServerName} que importar.",
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Usuario de {mediaServerName}",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Crea un token desde tu <PushbulletSettingsLink>Opciones de Cuenta</PushbulletSettingsLink>",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "No se pudo guardar la configuración de notificaciones de Pushover.",
"components.NotificationTypeSelector.userissuereopenedDescription": "Recibir notificaciones cuando se vuelvan a abrir incidencias que haya reportado.",
"components.NotificationTypeSelector.userissueresolvedDescription": "Reciba notificaciones cuando se resuelvan las incidencias que haya reportado.",
"components.PermissionEdit.createissuesDescription": "Dar permiso para informar incidencias.",
"components.Settings.SettingsAbout.supportjellyseerr": "Apoya a Jellyseerr",
"components.Settings.Notifications.userEmailRequired": "Requerir email de usuario",
"components.Settings.SonarrModal.animeSeriesType": "Tipo de Serie Anime",
"components.Settings.SonarrModal.seriesType": "Tipo Serie",
"components.Settings.jellyfinSettingsDescription": "Opcionalmente, configure los puntos finales internos y externos para su servidor {mediaServerName}. En la mayoría de los casos, la URL externa es diferente a la URL interna. También se puede configurar una URL de restablecimiento de contraseña personalizada para el inicio de sesión de {mediaServerName}, en caso de que desee redirigir a una página de restablecimiento de contraseña diferente.",
"components.Settings.jellyfinsettingsDescription": "Configure los ajustes para su servidor {mediaServerName}. {mediaServerName} escanea sus bibliotecas de {mediaServerName} para ver qué contenido está disponible.",
"components.Settings.manualscanJellyfin": "Escanear Libreria Manualmente",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> Eliminado correctamente de la lista de seguimiento!",
"components.UserList.newJellyfinsigninenabled": "La configuración <strong>Habilitar nuevo inicio de sesión de {mediaServerName}</strong> está actualmente habilitada. Los usuarios de {mediaServerName} con acceso a la biblioteca no necesitan ser importados para poder iniciar sesión.",
"components.UserProfile.localWatchlist": "Lista de seguimiento de {username}"
} }

View File

@@ -59,7 +59,7 @@
"components.Discover.DiscoverTvGenre.genreSeries": "Séries {genre}", "components.Discover.DiscoverTvGenre.genreSeries": "Séries {genre}",
"components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Séries", "components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Séries",
"components.Discover.DiscoverTvLanguage.languageSeries": "Séries en {language}", "components.Discover.DiscoverTvLanguage.languageSeries": "Séries en {language}",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Votre watchlist Plex", "components.Discover.DiscoverWatchlist.discoverwatchlist": "Votre watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Watchlist Plex", "components.Discover.DiscoverWatchlist.watchlist": "Watchlist Plex",
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Filtre actif} other {# Filtres actifs}}", "components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Filtre actif} other {# Filtres actifs}}",
"components.Discover.FilterSlideover.clearfilters": "Effacer les filtres actifs", "components.Discover.FilterSlideover.clearfilters": "Effacer les filtres actifs",
@@ -176,7 +176,7 @@
"components.Settings.RadarrModal.port": "Port", "components.Settings.RadarrModal.port": "Port",
"components.Settings.RadarrModal.qualityprofile": "Profil de qualité", "components.Settings.RadarrModal.qualityprofile": "Profil de qualité",
"components.Settings.RadarrModal.rootfolder": "Dossier racine", "components.Settings.RadarrModal.rootfolder": "Dossier racine",
"components.Settings.RadarrModal.selectMinimumAvailability": "Sélectionner une disponibilité minimale", "components.Settings.RadarrModal.selectMinimumAvailability": "Sélectionner une disponibilté minimale",
"components.Settings.RadarrModal.selectQualityProfile": "Sélectionner un profil qualité", "components.Settings.RadarrModal.selectQualityProfile": "Sélectionner un profil qualité",
"components.Settings.RadarrModal.selectRootFolder": "Sélectionner un dossier racine", "components.Settings.RadarrModal.selectRootFolder": "Sélectionner un dossier racine",
"components.Settings.RadarrModal.server4k": "Serveur 4K", "components.Settings.RadarrModal.server4k": "Serveur 4K",
@@ -277,7 +277,7 @@
"i18n.tvshows": "Séries", "i18n.tvshows": "Séries",
"i18n.unavailable": "Indisponible", "i18n.unavailable": "Indisponible",
"pages.oops": "Oups", "pages.oops": "Oups",
"pages.returnHome": "Retourner à l'accueil", "pages.returnHome": "Retourner à l'acceuil",
"components.TvDetails.TvCast.fullseriescast": "Casting complet de la série", "components.TvDetails.TvCast.fullseriescast": "Casting complet de la série",
"components.MovieDetails.MovieCast.fullcast": "Casting complet", "components.MovieDetails.MovieCast.fullcast": "Casting complet",
"components.Settings.Notifications.emailsettingssaved": "Paramètres de notification par e-mail enregistrés avec succès !", "components.Settings.Notifications.emailsettingssaved": "Paramètres de notification par e-mail enregistrés avec succès !",
@@ -323,7 +323,7 @@
"components.Settings.SettingsAbout.Releases.viewchangelog": "Voir le journal des modifications", "components.Settings.SettingsAbout.Releases.viewchangelog": "Voir le journal des modifications",
"components.Settings.SettingsAbout.Releases.versionChangelog": "Journal des modifications de la version {version}", "components.Settings.SettingsAbout.Releases.versionChangelog": "Journal des modifications de la version {version}",
"components.Settings.SettingsAbout.Releases.releases": "Versions", "components.Settings.SettingsAbout.Releases.releases": "Versions",
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Les données de version sont actuellement indisponibles.", "components.Settings.SettingsAbout.Releases.releasedataMissing": "Les données de version sont actuellement indisponible.",
"components.Settings.SettingsAbout.Releases.latestversion": "Dernière version", "components.Settings.SettingsAbout.Releases.latestversion": "Dernière version",
"components.Settings.SettingsAbout.Releases.currentversion": "Actuelle", "components.Settings.SettingsAbout.Releases.currentversion": "Actuelle",
"components.UserList.importfromplexerror": "Une erreur s'est produite durant l'importation des utilisateurs de Plex.", "components.UserList.importfromplexerror": "Une erreur s'est produite durant l'importation des utilisateurs de Plex.",
@@ -1071,7 +1071,7 @@
"components.ManageSlideOver.manageModalMedia4k": "Média(s) 4K", "components.ManageSlideOver.manageModalMedia4k": "Média(s) 4K",
"components.ManageSlideOver.markallseasons4kavailable": "Marquer toutes les saisons comme disponibles en 4K", "components.ManageSlideOver.markallseasons4kavailable": "Marquer toutes les saisons comme disponibles en 4K",
"components.ManageSlideOver.playedby": "Joué par", "components.ManageSlideOver.playedby": "Joué par",
"components.Settings.validationUrlTrailingSlash": "L'URL ne doit pas se terminer par un slash", "components.Settings.validationUrlTrailingSlash": "L'URL ne doit pas ce terminer par un slash",
"components.Settings.externalUrl": "URL externe", "components.Settings.externalUrl": "URL externe",
"components.Settings.tautulliApiKey": "Clé API", "components.Settings.tautulliApiKey": "Clé API",
"components.Settings.tautulliSettings": "Paramètres Tautulli", "components.Settings.tautulliSettings": "Paramètres Tautulli",
@@ -1252,10 +1252,70 @@
"components.Settings.SonarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur", "components.Settings.SonarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur",
"i18n.collection": "Collection", "i18n.collection": "Collection",
"components.Settings.RadarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur", "components.Settings.RadarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur",
"components.Settings.SonarrModal.seriesType": "Type de série", "components.IssueModal.issueVideo": "Vidéo",
"components.Settings.SonarrModal.animeSeriesType": "Types d'anime", "components.Settings.Notifications.NotificationsPushover.sound": "Son de notification",
"components.Settings.jellyfinSettings": "Paramètres pour {mediaServerName}",
"components.Settings.jellyfinSettingsFailure": "Une erreur est survenue lors de l'enregistrement des paramètres pour {mediaServerName}.",
"components.Settings.jellyfinSettingsSuccess": "Les paramètres pour {mediaServerName} ont été enregistrés avec succès!",
"components.Settings.jellyfinlibraries": "Bibliothèques {mediaServerName}",
"components.Settings.jellyfinlibrariesDescription": "Les bibliothèques de {mediaServerName} sont en cours d'analyze. Cliquez sur le bouton ci-dessous si aucune bibliothèque n'est répertoriée.",
"components.Settings.jellyfinsettings": "Paramètres pour {mediaServerName}",
"components.Settings.manualscanJellyfin": "Analyse manuelle de la bibliothèque",
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
"components.Settings.save": "Enregistrer les modifications",
"components.Settings.saving": "Sauvegarde en cours…",
"components.Settings.syncing": "Synchronisation en cours",
"components.Setup.signin": "Se connecter",
"components.Setup.signinWithPlex": "Utilisez votre compte Plex",
"components.StatusBadge.managemedia": "Gérer {mediaType}",
"components.StatusBadge.openinarr": "Ouvrir dans {arr}",
"components.StatusBadge.playonplex": "Lire sur {mediaServerName}",
"components.TitleCard.addToWatchList": "Ajouter à votre watchlist",
"components.TitleCard.watchlistCancel": "Watchlist pour <strong>{title}</strong> annulée.",
"components.TitleCard.watchlistError": "Une erreur est survenue. Veuillez réessayer.",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> a été ajouté à votre watchlist avec succès !",
"components.TvDetails.Season.somethingwentwrong": "Une erreur est survenue lors de la récupération des données de la saison.",
"components.TvDetails.manageseries": "Gérer les séries",
"components.TvDetails.play": "Jouer sur {mediaServerName}",
"components.TvDetails.play4k": "Jouer en 4K sur {mediaServerName}",
"components.TvDetails.rtcriticsscore": "Tomatometer sur Rotten Tomatoes",
"components.TvDetails.seasonnumber": "Saison {seasonNumber}",
"components.TvDetails.seasonstitle": "Saisons",
"components.TvDetails.status4k": "{status} 4K",
"components.TvDetails.tmdbuserscore": "Score utilisateur sur TMDB",
"components.UserList.importfromJellyfin": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.importfromJellyfinerror": "Une erreur est survenue lors de l'importation des utilisateurs de {mediaServerName}.",
"components.UserList.importfrommediaserver": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.noJellyfinuserstoimport": "Il n'y a aucun utilisateur à importer pour {mediaServerName}.",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Sauvegarde en cours…",
"components.UserProfile.plexwatchlist": "Watchlist Plex",
"components.Settings.syncJellyfin": "Synchroniser les bibliothèques",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> a été retiré de votre watchlist avec succès !",
"components.IssueModal.issueSubtitles": "Sous-titre",
"components.Login.emailtooltip": "L'adresse ne nécessite pas d'être associée avec votre instance {mediaServerName}.",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Appareil par défaut", "components.Settings.Notifications.NotificationsPushover.deviceDefault": "Appareil par défaut",
"components.Settings.Notifications.userEmailRequired": "E-mail utilisateur requis",
"components.Settings.SettingsAbout.supportjellyseerr": "Soutenez Jellyseerr",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Scan complet des bibliothèques Jellyfin",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Scan des ajouts récents aux bibliothèques Jellyfin",
"components.Settings.SonarrModal.animeSeriesType": "Type de série anime",
"components.Settings.SonarrModal.seriesType": "Type de série",
"components.Settings.internalUrl": "URL interne",
"components.Settings.jellyfinsettingsDescription": "Configurez les paramètres de votre serveur {mediaServerName}. {mediaServerName} analyse vos bibliothèques {mediaServerName} pour voir quel contenu est disponible.",
"components.Settings.jellyfinSettingsDescription": "Configurez facultativement les URL internes et externes pour votre serveur {mediaServerName}. Dans la plupart des cas, l'URL externe est différente de l'URL interne. Vous pouvez également définir une URL de réinitialisation de mot de passe personnalisée pour la connexion à {mediaServerName}, au cas où vous souhaiteriez rediriger vers une page de réinitialisation de mot de passe différente.",
"components.Settings.manualscanDescriptionJellyfin": "Normalement, cette tâche est executée qu'une fois toutes les 24 heures. Jellyseerr vérifiera plus agressivement les éléments récemment ajoutés à votre serveur {mediaServerName}. Si c'est la première fois que vous configurez Jellyseerr, une analyse complète manuelle de la bibliothèque est recommandée !",
"components.Settings.timeout": "Temps écoulé",
"components.Setup.configuremediaserver": "Configurer le serveur multimédia",
"components.TvDetails.rtaudiencescore": "Score de l'audience sur Rotten Tomatoes",
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Utilisateur pour {mediaServerName}",
"components.Setup.signinWithJellyfin": "Utilisez votre compte {mediaServerName}",
"components.UserList.mediaServerUser": "Utilisateur de {mediaServerName}",
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Épisode} other {# Épisodes}}",
"components.UserList.newJellyfinsigninenabled": "Le paramètre <strong>Activer la nouvelle connexion à {mediaServerName}</strong> est actuellement activé. Les utilisateurs de {mediaServerName} avec accès à la bibliothèque n'ont pas besoin d'être importés pour se connecter.",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Enregistrer les modifications",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Appareil par défaut", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Appareil par défaut",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} importé(s) avec succès !",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Son de notification", "components.UserProfile.UserSettings.UserNotificationSettings.sound": "Son de notification",
"components.Settings.Notifications.NotificationsPushover.sound": "Son de notification" "components.UserProfile.localWatchlist": "Watchlist de {username}"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,12 @@
"components.Discover.discovertv": "Populaire series", "components.Discover.discovertv": "Populaire series",
"components.Discover.popularmovies": "Populaire films", "components.Discover.popularmovies": "Populaire films",
"components.Discover.populartv": "Populaire series", "components.Discover.populartv": "Populaire series",
"components.Discover.recentlyAdded": "Recent toegevoegd", "components.Discover.recentlyAdded": "Onlangs toegevoegd",
"components.Discover.recentrequests": "Recente verzoeken", "components.Discover.recentrequests": "Recente verzoeken",
"components.Discover.trending": "Trending", "components.Discover.trending": "Trending",
"components.Discover.upcoming": "Verwachte films", "components.Discover.upcoming": "Verwachte films",
"components.Discover.upcomingmovies": "Verwachte films", "components.Discover.upcomingmovies": "Verwachte films",
"components.Layout.SearchInput.searchPlaceholder": "Zoek films en series", "components.Layout.SearchInput.searchPlaceholder": "Films en series zoeken",
"components.Layout.Sidebar.dashboard": "Ontdekken", "components.Layout.Sidebar.dashboard": "Ontdekken",
"components.Layout.Sidebar.requests": "Verzoeken", "components.Layout.Sidebar.requests": "Verzoeken",
"components.Layout.Sidebar.settings": "Instellingen", "components.Layout.Sidebar.settings": "Instellingen",
@@ -16,7 +16,7 @@
"components.Layout.UserDropdown.signout": "Uitloggen", "components.Layout.UserDropdown.signout": "Uitloggen",
"components.MovieDetails.budget": "Budget", "components.MovieDetails.budget": "Budget",
"components.MovieDetails.cast": "Cast", "components.MovieDetails.cast": "Cast",
"components.MovieDetails.originallanguage": "Originele taal", "components.MovieDetails.originallanguage": "Oorspronkelijke taal",
"components.MovieDetails.overview": "Overzicht", "components.MovieDetails.overview": "Overzicht",
"components.MovieDetails.overviewunavailable": "Overzicht niet beschikbaar.", "components.MovieDetails.overviewunavailable": "Overzicht niet beschikbaar.",
"components.MovieDetails.recommendations": "Aanbevelingen", "components.MovieDetails.recommendations": "Aanbevelingen",
@@ -114,7 +114,7 @@
"components.Settings.hostname": "Hostnaam of IP-adres", "components.Settings.hostname": "Hostnaam of IP-adres",
"components.Settings.librariesRemaining": "Resterende bibliotheken: {count}", "components.Settings.librariesRemaining": "Resterende bibliotheken: {count}",
"components.Settings.manualscan": "Handmatige bibliotheekscan", "components.Settings.manualscan": "Handmatige bibliotheekscan",
"components.Settings.manualscanDescription": "Normaal wordt dit eens elke 24 uur uitgevoerd. Jellyseerr controleert de recent toegevoegde items van je Plex-server agressiever. Als je Plex voor de eerste keer configureert, is een eenmalige handmatige volledige bibliotheekscan aanbevolen!", "components.Settings.manualscanDescription": "Normaliter wordt dit eenmaal per 24 uur uitgevoerd. Jellyseerr zal de lijst met onlangs toegevoegde media op je Plex-server vaker controleren. Als dit de eerste keer is dat je Jellyseerr instelt, wordt aanbevolen eenmalig een handmatige, volledige bibliotheekscan uit te voeren!",
"components.Settings.menuAbout": "Over", "components.Settings.menuAbout": "Over",
"components.Settings.menuGeneralSettings": "Algemeen", "components.Settings.menuGeneralSettings": "Algemeen",
"components.Settings.menuJobs": "Taken en cache", "components.Settings.menuJobs": "Taken en cache",
@@ -127,7 +127,7 @@
"components.Settings.plexlibraries": "Plex-bibliotheken", "components.Settings.plexlibraries": "Plex-bibliotheken",
"components.Settings.plexlibrariesDescription": "De bibliotheken die Jellyseerr scant voor titels. Stel je Plex-verbinding in en sla ze op. Klik daarna op de onderstaande knop als er geen bibliotheken staan.", "components.Settings.plexlibrariesDescription": "De bibliotheken die Jellyseerr scant voor titels. Stel je Plex-verbinding in en sla ze op. Klik daarna op de onderstaande knop als er geen bibliotheken staan.",
"components.Settings.plexsettings": "Plex-instellingen", "components.Settings.plexsettings": "Plex-instellingen",
"components.Settings.plexsettingsDescription": "Configureer de instellingen voor je Plex-server. Jellyseerr scant je Plex-bibliotheken om te zien welke content beschikbaar is.", "components.Settings.plexsettingsDescription": "Configureer de instellingen voor je Plex-server. Jellyseerr scant je Plex-bibliotheken om te zien welke inhoud beschikbaar is.",
"components.Settings.port": "Poort", "components.Settings.port": "Poort",
"components.Settings.radarrsettings": "Radarr-instellingen", "components.Settings.radarrsettings": "Radarr-instellingen",
"components.Settings.sonarrsettings": "Sonarr-instellingen", "components.Settings.sonarrsettings": "Sonarr-instellingen",
@@ -137,12 +137,12 @@
"components.Setup.configureservices": "Diensten configureren", "components.Setup.configureservices": "Diensten configureren",
"components.Setup.continue": "Doorgaan", "components.Setup.continue": "Doorgaan",
"components.Setup.finish": "Installatie voltooien", "components.Setup.finish": "Installatie voltooien",
"components.Setup.finishing": "Bezig met voltooien…", "components.Setup.finishing": "Voltooien…",
"components.Setup.loginwithplex": "Inloggen met Plex", "components.Setup.loginwithplex": "Inloggen met Plex",
"components.Setup.signinMessage": "Ga aan de slag door in te loggen met je Plex-account", "components.Setup.signinMessage": "Ga aan de slag door je aan te melden",
"components.Setup.welcome": "Welkom bij Jellyseerr", "components.Setup.welcome": "Welkom bij Jellyseerr",
"components.TvDetails.cast": "Cast", "components.TvDetails.cast": "Cast",
"components.TvDetails.originallanguage": "Originele taal", "components.TvDetails.originallanguage": "Oorspronkelijke taal",
"components.TvDetails.overview": "Overzicht", "components.TvDetails.overview": "Overzicht",
"components.TvDetails.overviewunavailable": "Overzicht niet beschikbaar.", "components.TvDetails.overviewunavailable": "Overzicht niet beschikbaar.",
"components.TvDetails.recommendations": "Aanbevelingen", "components.TvDetails.recommendations": "Aanbevelingen",
@@ -164,7 +164,7 @@
"i18n.movies": "Films", "i18n.movies": "Films",
"i18n.partiallyavailable": "Deels beschikbaar", "i18n.partiallyavailable": "Deels beschikbaar",
"i18n.pending": "In behandeling", "i18n.pending": "In behandeling",
"i18n.processing": "Bezig met verwerken", "i18n.processing": "Verwerken",
"i18n.tvshows": "Series", "i18n.tvshows": "Series",
"i18n.unavailable": "Niet beschikbaar", "i18n.unavailable": "Niet beschikbaar",
"pages.oops": "Oeps", "pages.oops": "Oeps",
@@ -187,14 +187,14 @@
"components.Setup.tip": "Tip", "components.Setup.tip": "Tip",
"components.Settings.SonarrModal.testFirstRootFolders": "Test verbinding om hoofdmappen te laden", "components.Settings.SonarrModal.testFirstRootFolders": "Test verbinding om hoofdmappen te laden",
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test verbinding om kwaliteitsprofielen te laden", "components.Settings.SonarrModal.testFirstQualityProfiles": "Test verbinding om kwaliteitsprofielen te laden",
"components.Settings.SonarrModal.loadingrootfolders": "Bezig met laden van hoofdmappen…", "components.Settings.SonarrModal.loadingrootfolders": "Hoofdmappen laden…",
"components.Settings.SonarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…", "components.Settings.SonarrModal.loadingprofiles": "Kwaliteitsprofielen laden…",
"components.Settings.SettingsAbout.gettingsupport": "Ondersteuning krijgen", "components.Settings.SettingsAbout.gettingsupport": "Ondersteuning krijgen",
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Je moet een minimale beschikbaarheid selecteren", "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Je moet een minimale beschikbaarheid selecteren",
"components.Settings.RadarrModal.testFirstRootFolders": "Test verbinding om hoofdmappen te laden", "components.Settings.RadarrModal.testFirstRootFolders": "Test verbinding om hoofdmappen te laden",
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test verbinding om kwaliteitsprofielen te laden", "components.Settings.RadarrModal.testFirstQualityProfiles": "Test verbinding om kwaliteitsprofielen te laden",
"components.Settings.RadarrModal.loadingrootfolders": "Bezig met laden van hoofdmappen…", "components.Settings.RadarrModal.loadingrootfolders": "Hoofdmappen laden…",
"components.Settings.RadarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…", "components.Settings.RadarrModal.loadingprofiles": "Kwaliteitsprofielen laden…",
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Versiegegevens zijn momenteel niet beschikbaar.", "components.Settings.SettingsAbout.Releases.releasedataMissing": "Versiegegevens zijn momenteel niet beschikbaar.",
"components.Settings.SettingsAbout.Releases.latestversion": "Nieuwste", "components.Settings.SettingsAbout.Releases.latestversion": "Nieuwste",
"components.Settings.SettingsAbout.Releases.currentversion": "Huidig", "components.Settings.SettingsAbout.Releases.currentversion": "Huidig",
@@ -208,7 +208,7 @@
"i18n.retry": "Opnieuw proberen", "i18n.retry": "Opnieuw proberen",
"i18n.requested": "Aangevraagd", "i18n.requested": "Aangevraagd",
"i18n.failed": "Mislukt", "i18n.failed": "Mislukt",
"i18n.deleting": "Bezig met verwijderen…", "i18n.deleting": "Verwijderen…",
"i18n.close": "Sluiten", "i18n.close": "Sluiten",
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.", "components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.",
"components.UserList.userdeleted": "Gebruiker succesvol verwijderd!", "components.UserList.userdeleted": "Gebruiker succesvol verwijderd!",
@@ -223,7 +223,7 @@
"components.TvDetails.network": "{networkCount, plural, one {Netwerk} other {Netwerken}}", "components.TvDetails.network": "{networkCount, plural, one {Netwerk} other {Netwerken}}",
"components.TvDetails.firstAirDate": "Datum eerste uitzending", "components.TvDetails.firstAirDate": "Datum eerste uitzending",
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
"components.StatusChacker.reloadOverseerr": "Herladen", "components.StatusChacker.reloadJellyseerr": "Herladen",
"components.StatusChacker.newversionavailable": "Toepassingsupdate", "components.StatusChacker.newversionavailable": "Toepassingsupdate",
"components.StatusChacker.newversionDescription": "Jellyseerr is geüpdatet! Klik op de onderstaande knop om de pagina opnieuw te laden.", "components.StatusChacker.newversionDescription": "Jellyseerr is geüpdatet! Klik op de onderstaande knop om de pagina opnieuw te laden.",
"components.Settings.toastSettingsSuccess": "Instellingen succesvol opgeslagen!", "components.Settings.toastSettingsSuccess": "Instellingen succesvol opgeslagen!",
@@ -303,10 +303,10 @@
"components.UserList.create": "Aanmaken", "components.UserList.create": "Aanmaken",
"components.UserList.createlocaluser": "Lokale gebruiker aanmaken", "components.UserList.createlocaluser": "Lokale gebruiker aanmaken",
"components.UserList.usercreatedfailed": "Er ging iets mis bij het aanmaken van de gebruiker.", "components.UserList.usercreatedfailed": "Er ging iets mis bij het aanmaken van de gebruiker.",
"components.UserList.creating": "Bezig met aanmaken…", "components.UserList.creating": "Aanmaken…",
"components.UserList.validationpasswordminchars": "Wachtwoord is te kort; moet minimaal 8 tekens bevatten", "components.UserList.validationpasswordminchars": "Wachtwoord is te kort; moet minimaal 8 tekens bevatten",
"components.UserList.usercreatedsuccess": "Gebruiker succesvol aangemaakt!", "components.UserList.usercreatedsuccess": "Gebruiker succesvol aangemaakt!",
"components.UserList.passwordinfodescription": "Configureer een applicatie-URL en schakel e-mailmeldingen in om automatische wachtwoordgeneratie mogelijk te maken.", "components.UserList.passwordinfodescription": "Stel een applicatie-URL in en schakel e-mailmeldingen in om automatische wachtwoordgeneratie mogelijk te maken.",
"components.UserList.password": "Wachtwoord", "components.UserList.password": "Wachtwoord",
"components.UserList.localuser": "Lokale gebruiker", "components.UserList.localuser": "Lokale gebruiker",
"components.UserList.email": "E-mailadres", "components.UserList.email": "E-mailadres",
@@ -340,23 +340,23 @@
"components.RequestModal.SearchByNameModal.notvdbiddescription": "We kunnen deze serie niet automatisch matchen. Selecteer hieronder de juiste match.", "components.RequestModal.SearchByNameModal.notvdbiddescription": "We kunnen deze serie niet automatisch matchen. Selecteer hieronder de juiste match.",
"components.Login.signinwithplex": "Plex-account gebruiken", "components.Login.signinwithplex": "Plex-account gebruiken",
"components.Login.signinheader": "Log in om verder te gaan", "components.Login.signinheader": "Log in om verder te gaan",
"components.Login.signingin": "Bezig met inloggen…", "components.Login.signingin": "Aanmelden…",
"components.Login.signin": "Inloggen", "components.Login.signin": "Inloggen",
"components.Settings.notificationAgentSettingsDescription": "Meldingsagenten configureren en inschakelen.", "components.Settings.notificationAgentSettingsDescription": "Meldingsagenten configureren en inschakelen.",
"components.PlexLoginButton.signinwithplex": "Inloggen", "components.PlexLoginButton.signinwithplex": "Inloggen",
"components.PlexLoginButton.signingin": "Bezig met inloggen…", "components.PlexLoginButton.signingin": "Aanmelden…",
"components.PermissionEdit.advancedrequest": "Geavanceerde aanvragen", "components.PermissionEdit.advancedrequest": "Geavanceerde aanvragen",
"components.PermissionEdit.admin": "Beheerder", "components.PermissionEdit.admin": "Beheerder",
"components.UserList.userssaved": "Gebruikersrechten succesvol opgeslagen!", "components.UserList.userssaved": "Gebruikersrechten succesvol opgeslagen!",
"components.Settings.toastPlexRefreshSuccess": "Serverlijst van Plex succesvol opgehaald!", "components.Settings.toastPlexRefreshSuccess": "Serverlijst van Plex succesvol opgehaald!",
"components.Settings.toastPlexRefresh": "Bezig met serverlijst ophalen van Plex…", "components.Settings.toastPlexRefresh": "Bezig met serverlijst ophalen van Plex…",
"components.Settings.toastPlexConnecting": "Bezig met verbinden met Plex-server…", "components.Settings.toastPlexConnecting": "Verbinden met Plex…",
"components.UserList.bulkedit": "Meerdere bewerken", "components.UserList.bulkedit": "Meerdere bewerken",
"components.Settings.toastPlexRefreshFailure": "Kan serverlijst van Plex niet ophalen.", "components.Settings.toastPlexRefreshFailure": "Kan serverlijst van Plex niet ophalen.",
"components.Settings.toastPlexConnectingSuccess": "Succesvol verbonden met Plex-server!", "components.Settings.toastPlexConnectingSuccess": "Succesvol verbonden met Plex-server!",
"components.Settings.toastPlexConnectingFailure": "Kan geen verbinding maken met Plex.", "components.Settings.toastPlexConnectingFailure": "Kan geen verbinding maken met Plex.",
"components.Settings.settingUpPlexDescription": "Om Plex in te stellen, kan je de gegevens handmatig invoeren of een server selecteren die is opgehaald van <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Druk op de knop rechts van de vervolgkeuzelijst om de lijst van beschikbare servers op te halen.", "components.Settings.settingUpPlexDescription": "Om Plex in te stellen, kan je de gegevens handmatig invoeren of een server selecteren die is opgehaald van <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Druk op de knop rechts van de vervolgkeuzelijst om de lijst van beschikbare servers op te halen.",
"components.Settings.serverpresetRefreshing": "Bezig met servers ophalen…", "components.Settings.serverpresetRefreshing": "Servers ophalen…",
"components.Settings.serverpresetManualMessage": "Handmatige configuratie", "components.Settings.serverpresetManualMessage": "Handmatige configuratie",
"components.Settings.serverpresetLoad": "Klik op de knop om de beschikbare servers te laden", "components.Settings.serverpresetLoad": "Klik op de knop om de beschikbare servers te laden",
"components.Settings.serverpreset": "Server", "components.Settings.serverpreset": "Server",
@@ -431,7 +431,7 @@
"components.RequestModal.AdvancedRequester.requestas": "Aanvragen als", "components.RequestModal.AdvancedRequester.requestas": "Aanvragen als",
"components.Discover.discover": "Ontdekken", "components.Discover.discover": "Ontdekken",
"components.Settings.validationApplicationTitle": "Je moet een toepassingstitel opgeven", "components.Settings.validationApplicationTitle": "Je moet een toepassingstitel opgeven",
"components.AppDataWarning.dockerVolumeMissingDescription": "De volumekoppeling <code>{appDataPath}</code> was niet correct geconfigureerd. Alle gegevens zullen worden gewist wanneer de container wordt gestopt of opnieuw wordt gestart.", "components.AppDataWarning.dockerVolumeMissingDescription": "De volumekoppeling <code>{appDataPath}</code> is niet correct geconfigureerd. Alle gegevens zullen worden gewist wanneer de container wordt gestopt of opnieuw wordt gestart.",
"components.Settings.validationApplicationUrlTrailingSlash": "URL mag niet eindigen op een schuine streep", "components.Settings.validationApplicationUrlTrailingSlash": "URL mag niet eindigen op een schuine streep",
"components.Settings.validationApplicationUrl": "Je moet een geldige URL opgeven", "components.Settings.validationApplicationUrl": "Je moet een geldige URL opgeven",
"components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "URL mag niet eindigen op een schuine streep", "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "URL mag niet eindigen op een schuine streep",
@@ -520,7 +520,7 @@
"components.Layout.UserDropdown.myprofile": "Profiel", "components.Layout.UserDropdown.myprofile": "Profiel",
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Je moet een geldige gebruikers-ID opgeven", "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Je moet een geldige gebruikers-ID opgeven",
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Het <FindDiscordIdLink>meercijferige ID-nummer</FindDiscordIdLink> van je gebruikersaccount", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Het <FindDiscordIdLink>meercijferige ID-nummer</FindDiscordIdLink> van je gebruikersaccount",
"components.CollectionDetails.requestcollection4k": "Collectie in 4K aanvragen", "components.CollectionDetails.requestcollection4k": "Collectie aanvragen in 4K",
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Inhoud filteren op regionale beschikbaarheid", "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Inhoud filteren op regionale beschikbaarheid",
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Regio van Ontdekken", "components.UserProfile.UserSettings.UserGeneralSettings.region": "Regio van Ontdekken",
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Inhoud filteren op oorspronkelijke taal", "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Inhoud filteren op oorspronkelijke taal",
@@ -546,7 +546,7 @@
"components.Settings.SettingsJobsCache.download-sync-reset": "Reset download sync", "components.Settings.SettingsJobsCache.download-sync-reset": "Reset download sync",
"components.Settings.SettingsJobsCache.download-sync": "Synchronisatie downloads", "components.Settings.SettingsJobsCache.download-sync": "Synchronisatie downloads",
"components.TvDetails.seasons": "{seasonCount, plural, one {# seizoen} other {# seizoenen}}", "components.TvDetails.seasons": "{seasonCount, plural, one {# seizoen} other {# seizoenen}}",
"i18n.loading": "Bezig met laden…", "i18n.loading": "Laden…",
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Je moet een geldige chat-ID opgeven", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Je moet een geldige chat-ID opgeven",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Een chat starten</TelegramBotLink>, <GetIdBotLink>@get_id_bot</GetIdBotLink> toevoegen en de opdracht <code>/my_id</code> geven", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Een chat starten</TelegramBotLink>, <GetIdBotLink>@get_id_bot</GetIdBotLink> toevoegen en de opdracht <code>/my_id</code> geven",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chat-ID", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chat-ID",
@@ -558,14 +558,14 @@
"components.Discover.DiscoverNetwork.networkSeries": "Series van {network}", "components.Discover.DiscoverNetwork.networkSeries": "Series van {network}",
"components.Discover.DiscoverMovieGenre.genreMovies": "{genre} films", "components.Discover.DiscoverMovieGenre.genreMovies": "{genre} films",
"components.Setup.scanbackground": "Het scannen wordt op de achtergrond uitgevoerd. Je kunt in de tussentijd doorgaan met het installatieproces.", "components.Setup.scanbackground": "Het scannen wordt op de achtergrond uitgevoerd. Je kunt in de tussentijd doorgaan met het installatieproces.",
"components.Settings.scanning": "Bezig met synchroniseren…", "components.Settings.scanning": "Synchroniseren…",
"components.Settings.scan": "Bibliotheken synchroniseren", "components.Settings.scan": "Bibliotheken synchroniseren",
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr-scan", "components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr-scan",
"components.Settings.SettingsJobsCache.radarr-scan": "Radarr-scan", "components.Settings.SettingsJobsCache.radarr-scan": "Radarr-scan",
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex recent toegevoegde scan", "components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex recent toegevoegde scan",
"components.Settings.SettingsJobsCache.plex-full-scan": "Plex volledige bibliotheekscan", "components.Settings.SettingsJobsCache.plex-full-scan": "Plex volledige bibliotheekscan",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "volledige bibliotheekscan Jellyfin", "components.Settings.SettingsJobsCache.jellyfin-full-scan": "volledige bibliotheekscan Jellyfin",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Jellyfin recent toegevoegde scan", "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Scan van 'onlangs toegevoegd' in Jellyfin",
"components.Settings.Notifications.validationUrl": "Je moet een geldige URL opgeven", "components.Settings.Notifications.validationUrl": "Je moet een geldige URL opgeven",
"components.Settings.Notifications.botAvatarUrl": "URL bot-avatar", "components.Settings.Notifications.botAvatarUrl": "URL bot-avatar",
"components.RequestList.RequestItem.requested": "Aangevraagd", "components.RequestList.RequestItem.requested": "Aangevraagd",
@@ -670,27 +670,27 @@
"components.QuotaSelector.unlimited": "Onbeperkt", "components.QuotaSelector.unlimited": "Onbeperkt",
"i18n.view": "Bekijken", "i18n.view": "Bekijken",
"i18n.tvshow": "Serie", "i18n.tvshow": "Serie",
"i18n.testing": "Bezig met testen…", "i18n.testing": "Testen…",
"i18n.test": "Test", "i18n.test": "Test",
"i18n.status": "Status", "i18n.status": "Status",
"i18n.showingresults": "<strong>{from}</strong> tot <strong>{to}</strong> van de <strong>{total}</strong> resultaten worden weergegeven", "i18n.showingresults": "<strong>{from}</strong> tot <strong>{to}</strong> van de <strong>{total}</strong> resultaten worden weergegeven",
"i18n.saving": "Bezig met opslaan…", "i18n.saving": "Opslaan…",
"i18n.save": "Wijzigingen opslaan", "i18n.save": "Wijzigingen opslaan",
"i18n.resultsperpage": "{pageSize} resultaten per pagina weergeven", "i18n.resultsperpage": "{pageSize} resultaten per pagina weergeven",
"i18n.requesting": "Bezig met aanvragen…", "i18n.requesting": "Aanvragen…",
"i18n.request4k": "Aanvragen in 4K", "i18n.request4k": "Aanvragen in 4K",
"i18n.previous": "Vorige", "i18n.previous": "Vorige",
"i18n.notrequested": "Niet aangevraagd", "i18n.notrequested": "Niet aangevraagd",
"i18n.noresults": "Geen resultaten.", "i18n.noresults": "Geen resultaten.",
"i18n.next": "Volgende", "i18n.next": "Volgende",
"i18n.movie": "Film", "i18n.movie": "Film",
"i18n.canceling": "Bezig met annuleren…", "i18n.canceling": "Annuleren…",
"i18n.back": "Terug", "i18n.back": "Terug",
"i18n.areyousure": "Weet je het zeker?", "i18n.areyousure": "Weet je het zeker?",
"i18n.all": "Alle", "i18n.all": "Alle",
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Deze gebruiker heeft nog minstens <strong>{seasons}</strong> {seasons, plural, one {seizoensverzoek} other {seizoensverzoeken}} nodig om deze serie aan te vragen.", "components.RequestModal.QuotaDisplay.requiredquotaUser": "Deze gebruiker heeft nog minstens <strong>{seasons}</strong> {seasons, plural, one {seizoensverzoek} other {seizoensverzoeken}} nodig om deze serie aan te vragen.",
"components.TvDetails.originaltitle": "Originele titel", "components.TvDetails.originaltitle": "Oorspronkelijke titel",
"components.MovieDetails.originaltitle": "Originele titel", "components.MovieDetails.originaltitle": "Oorspronkelijke titel",
"components.LanguageSelector.originalLanguageDefault": "Alle talen", "components.LanguageSelector.originalLanguageDefault": "Alle talen",
"components.LanguageSelector.languageServerDefault": "Standaard ({language})", "components.LanguageSelector.languageServerDefault": "Standaard ({language})",
"components.Settings.SonarrModal.testFirstTags": "Test de verbinding om labels te laden", "components.Settings.SonarrModal.testFirstTags": "Test de verbinding om labels te laden",
@@ -733,9 +733,9 @@
"components.RequestModal.pendingapproval": "Je verzoek is in afwachting van goedkeuring.", "components.RequestModal.pendingapproval": "Je verzoek is in afwachting van goedkeuring.",
"components.RequestList.RequestItem.cancelRequest": "Verzoek annuleren", "components.RequestList.RequestItem.cancelRequest": "Verzoek annuleren",
"components.NotificationTypeSelector.notificationTypes": "Meldingtypes", "components.NotificationTypeSelector.notificationTypes": "Meldingtypes",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Er is voor jouw account momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord om in te kunnen loggen als een \"lokale gebruiker\" met uw e-mailadres.", "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Er is voor jouw account momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord om in te kunnen loggen als een \"lokale gebruiker\" met je e-mailadres.",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Deze gebruikersaccount heeft momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord zodat deze account in staat is om zich aan te melden als een \"lokale gebruiker\".", "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Deze gebruikersaccount heeft momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord zodat deze account in staat is om zich aan te melden als een \"lokale gebruiker\".",
"components.Settings.serviceSettingsDescription": "Configureer je {serverType} server(s) hieronder. Je kunt meerdere {serverType} servers verbinden, maar slechts twee ervan kunnen als standaard worden gemarkeerd (één niet-4K en één 4K). Beheerders kunnen vóór de goedkeuring de server die gebruikt wordt om nieuwe aanvragen te verwerken aanpassen.", "components.Settings.serviceSettingsDescription": "Stel je {serverType}-server(s) hieronder in. Je kunt meerdere {serverType}-servers verbinden, maar slechts twee ervan kunnen als standaard worden gemarkeerd (één niet-4K en één 4K). Beheerders kunnen vóór goedkeuring de server aanpassen die voor nieuwe aanvragen gebruikt wordt.",
"components.Settings.noDefaultServer": "Ten minste één {serverType} server moet als standaard worden gemarkeerd om {mediaType}verzoeken te kunnen verwerken.", "components.Settings.noDefaultServer": "Ten minste één {serverType} server moet als standaard worden gemarkeerd om {mediaType}verzoeken te kunnen verwerken.",
"components.Settings.noDefaultNon4kServer": "Als je slechts één enkele {serverType} server hebt voor zowel niet-4K als 4K-inhoud (of als je alleen 4K-inhoud downloadt), dan moet je {serverType} server <strong>NIET</strong> aangeduid worden als een 4K-server.", "components.Settings.noDefaultNon4kServer": "Als je slechts één enkele {serverType} server hebt voor zowel niet-4K als 4K-inhoud (of als je alleen 4K-inhoud downloadt), dan moet je {serverType} server <strong>NIET</strong> aangeduid worden als een 4K-server.",
"components.Settings.mediaTypeSeries": "serie", "components.Settings.mediaTypeSeries": "serie",
@@ -747,7 +747,7 @@
"components.Layout.VersionStatus.outofdate": "Verouderd", "components.Layout.VersionStatus.outofdate": "Verouderd",
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} achter", "components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} achter",
"components.UserList.autogeneratepasswordTip": "Een door de server gegenereerd wachtwoord naar de gebruiker e-mailen", "components.UserList.autogeneratepasswordTip": "Een door de server gegenereerd wachtwoord naar de gebruiker e-mailen",
"i18n.retrying": "Bezig met opnieuw proberen…", "i18n.retrying": "Opnieuw proberen…",
"components.Settings.serverSecure": "veilig", "components.Settings.serverSecure": "veilig",
"components.UserList.usercreatedfailedexisting": "Het opgegeven e-mailadres wordt al gebruikt door een andere gebruiker.", "components.UserList.usercreatedfailedexisting": "Het opgegeven e-mailadres wordt al gebruikt door een andere gebruiker.",
"components.RequestModal.edit": "Verzoek bewerken", "components.RequestModal.edit": "Verzoek bewerken",
@@ -847,7 +847,7 @@
"components.NotificationTypeSelector.usermediadeclinedDescription": "Een melding ontvangen wanneer je mediaverzoeken worden geweigerd.", "components.NotificationTypeSelector.usermediadeclinedDescription": "Een melding ontvangen wanneer je mediaverzoeken worden geweigerd.",
"components.NotificationTypeSelector.usermediaavailableDescription": "Een melding ontvangen wanneer je mediaverzoeken beschikbaar zijn.", "components.NotificationTypeSelector.usermediaavailableDescription": "Een melding ontvangen wanneer je mediaverzoeken beschikbaar zijn.",
"components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Een melding ontvangen wanneer andere gebruikers nieuwe mediaverzoeken indienen die automatisch worden goedgekeurd.", "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Een melding ontvangen wanneer andere gebruikers nieuwe mediaverzoeken indienen die automatisch worden goedgekeurd.",
"components.Settings.SettingsAbout.betawarning": "Dit is BETA software. Functies kunnen kapot en/of instabiel zijn. Meld eventuele problemen op GitHub!", "components.Settings.SettingsAbout.betawarning": "Dit is BETA-software. Functies kunnen kapot en/of instabiel zijn. Meld eventuele problemen op GitHub!",
"components.Layout.LanguagePicker.displaylanguage": "Weergavetaal", "components.Layout.LanguagePicker.displaylanguage": "Weergavetaal",
"components.MovieDetails.showmore": "Meer tonen", "components.MovieDetails.showmore": "Meer tonen",
"components.MovieDetails.showless": "Minder tonen", "components.MovieDetails.showless": "Minder tonen",
@@ -971,7 +971,7 @@
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Je moet een geldige gebruikers- of groepssleutel opgeven", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Je moet een geldige gebruikers- of groepssleutel opgeven",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Instellingen voor Pushbullet-meldingen succesvol opgeslagen!", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Instellingen voor Pushbullet-meldingen succesvol opgeslagen!",
"components.IssueDetails.playonplex": "Afspelen op {mediaServerName}", "components.IssueDetails.playonplex": "Afspelen op {mediaServerName}",
"components.IssueDetails.play4konplex": "Afspelen in 4K op {mediaServerName}", "components.IssueDetails.play4konplex": "Afspelen op {mediaServerName} in 4K",
"components.IssueDetails.openin4karr": "Openen in 4K {arr}", "components.IssueDetails.openin4karr": "Openen in 4K {arr}",
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {aflevering} other {afleveringen}}", "components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {aflevering} other {afleveringen}}",
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {seizoen} other {seizoenen}}", "components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {seizoen} other {seizoenen}}",
@@ -982,7 +982,7 @@
"components.NotificationTypeSelector.adminissuereopenedDescription": "Ontvang een melding wanneer problemen door andere gebruikers opnieuw worden ingediend.", "components.NotificationTypeSelector.adminissuereopenedDescription": "Ontvang een melding wanneer problemen door andere gebruikers opnieuw worden ingediend.",
"components.NotificationTypeSelector.issuereopenedDescription": "Stuur meldingen wanneer problemen opnieuw worden ingediend.", "components.NotificationTypeSelector.issuereopenedDescription": "Stuur meldingen wanneer problemen opnieuw worden ingediend.",
"components.NotificationTypeSelector.userissuereopenedDescription": "Ontvang een bericht wanneer problemen die jij hebt gemeld, opnieuw worden ingediend.", "components.NotificationTypeSelector.userissuereopenedDescription": "Ontvang een bericht wanneer problemen die jij hebt gemeld, opnieuw worden ingediend.",
"components.RequestModal.requestseasons4k": "{seasonCount} {seasonCount, plural, one {seizoen} other {seizoenen}} in 4K aanvragen", "components.RequestModal.requestseasons4k": "{seasonCount} {seasonCount, plural, one {seizoen} other {seizoenen}} aanvragen in 4K",
"components.RequestModal.requestmovies": "{count} {count, plural, one {film} other {films}} aanvragen", "components.RequestModal.requestmovies": "{count} {count, plural, one {film} other {films}} aanvragen",
"components.RequestModal.selectmovies": "Film(s) selecteren", "components.RequestModal.selectmovies": "Film(s) selecteren",
"components.MovieDetails.productioncountries": "Productie{countryCount, plural, one {land} other {landen}}", "components.MovieDetails.productioncountries": "Productie{countryCount, plural, one {land} other {landen}}",
@@ -998,7 +998,7 @@
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Agent inschakelen", "components.Settings.Notifications.NotificationsGotify.agentenabled": "Agent inschakelen",
"components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Instellingen voor meldingen Gotify succesvol opgeslagen!", "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Instellingen voor meldingen Gotify succesvol opgeslagen!",
"components.Settings.Notifications.NotificationsGotify.token": "Toepassingstoken", "components.Settings.Notifications.NotificationsGotify.token": "Toepassingstoken",
"i18n.importing": "Bezig met importeren…", "i18n.importing": "Importeren…",
"components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Instellingen voor meldingen Gotify niet opgeslagen.", "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Instellingen voor meldingen Gotify niet opgeslagen.",
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Testmelding Gotify niet verzonden.", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Testmelding Gotify niet verzonden.",
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "Testmelding Gotify verzonden!", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "Testmelding Gotify verzonden!",
@@ -1012,7 +1012,7 @@
"components.UserList.newplexsigninenabled": "De instelling <strong>Nieuwe Plex-aanmelding inschakelen</strong> is momenteel ingeschakeld. Plex-gebruikers met bibliotheektoegang hoeven niet te worden geïmporteerd om in te loggen.", "components.UserList.newplexsigninenabled": "De instelling <strong>Nieuwe Plex-aanmelding inschakelen</strong> is momenteel ingeschakeld. Plex-gebruikers met bibliotheektoegang hoeven niet te worden geïmporteerd om in te loggen.",
"components.ManageSlideOver.manageModalAdvanced": "Geavanceerd", "components.ManageSlideOver.manageModalAdvanced": "Geavanceerd",
"components.ManageSlideOver.alltime": "Altijd", "components.ManageSlideOver.alltime": "Altijd",
"components.ManageSlideOver.markallseasons4kavailable": "Alle seizoenen als beschikbaar in 4K markeren", "components.ManageSlideOver.markallseasons4kavailable": "Alle seizoenen markeren als beschikbaar in 4K",
"components.ManageSlideOver.opentautulli": "In Tautulli openen", "components.ManageSlideOver.opentautulli": "In Tautulli openen",
"components.ManageSlideOver.pastdays": "Afgelopen {days, number} dagen", "components.ManageSlideOver.pastdays": "Afgelopen {days, number} dagen",
"components.ManageSlideOver.playedby": "Afgespeeld door", "components.ManageSlideOver.playedby": "Afgespeeld door",
@@ -1046,8 +1046,8 @@
"components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.", "components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
"components.MovieDetails.digitalrelease": "Digitale release", "components.MovieDetails.digitalrelease": "Digitale release",
"i18n.restartRequired": "Opnieuw opstarten vereist", "i18n.restartRequired": "Opnieuw opstarten vereist",
"components.PermissionEdit.viewrecentDescription": "Toestemming geven om de lijst met recent toegevoegde media te bekijken.", "components.PermissionEdit.viewrecentDescription": "Toestemming geven om de lijst met onlangs toegevoegde media weer te geven.",
"components.PermissionEdit.viewrecent": "Recent toegevoegd bekijken", "components.PermissionEdit.viewrecent": "Onlangs toegevoegd weergeven",
"components.Settings.deleteServer": "{serverType}-server verwijderen", "components.Settings.deleteServer": "{serverType}-server verwijderen",
"components.StatusChecker.appUpdated": "{applicationTitle} bijgewerkt", "components.StatusChecker.appUpdated": "{applicationTitle} bijgewerkt",
"components.RequestList.RequestItem.tmdbid": "TMDB ID", "components.RequestList.RequestItem.tmdbid": "TMDB ID",
@@ -1072,8 +1072,8 @@
"components.TvDetails.seasonnumber": "Seizoen {seasonNumber}", "components.TvDetails.seasonnumber": "Seizoen {seasonNumber}",
"components.TvDetails.Season.somethingwentwrong": "Er ging iets mis bij het ophalen van de seizoensgegevens.", "components.TvDetails.Season.somethingwentwrong": "Er ging iets mis bij het ophalen van de seizoensgegevens.",
"components.TvDetails.seasonstitle": "Seizoenen", "components.TvDetails.seasonstitle": "Seizoenen",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Je Plex-kijklijst", "components.Discover.DiscoverWatchlist.discoverwatchlist": "Jouw kijklijst",
"components.Discover.plexwatchlist": "Je Plex Kijklijst", "components.Discover.plexwatchlist": "Jouw kijklijst",
"components.MovieDetails.physicalrelease": "Fysieke release", "components.MovieDetails.physicalrelease": "Fysieke release",
"components.PermissionEdit.autorequest": "Automatisch aanvragen", "components.PermissionEdit.autorequest": "Automatisch aanvragen",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Kijklijst synchroniseren", "components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Kijklijst synchroniseren",
@@ -1099,7 +1099,7 @@
"components.TvDetails.manageseries": "Serie beheren", "components.TvDetails.manageseries": "Serie beheren",
"components.MovieDetails.managemovie": "Film beheren", "components.MovieDetails.managemovie": "Film beheren",
"components.MovieDetails.reportissue": "Probleem melden", "components.MovieDetails.reportissue": "Probleem melden",
"components.PermissionEdit.autorequestMoviesDescription": "Toestemming geven om niet-4K films in je Plex Kijklijst automatisch aan te vragen.", "components.PermissionEdit.autorequestMoviesDescription": "Toestemming geven om niet-4K films in je Plex-kijklijst automatisch aan te vragen.",
"components.PermissionEdit.autorequestSeries": "Series automatisch aanvragen", "components.PermissionEdit.autorequestSeries": "Series automatisch aanvragen",
"components.PermissionEdit.autorequestMovies": "Films automatisch aanvragen", "components.PermissionEdit.autorequestMovies": "Films automatisch aanvragen",
"components.Settings.experimentalTooltip": "Deze instelling inschakelen, kan leiden tot onverwacht gedrag van de toepassing", "components.Settings.experimentalTooltip": "Deze instelling inschakelen, kan leiden tot onverwacht gedrag van de toepassing",
@@ -1152,8 +1152,8 @@
"components.Discover.DiscoverSliderEdit.remove": "Verwijderen", "components.Discover.DiscoverSliderEdit.remove": "Verwijderen",
"components.Discover.resetfailed": "Er is iets fout gegaan bij het resetten van de instellingen van Ontdekken.", "components.Discover.resetfailed": "Er is iets fout gegaan bij het resetten van de instellingen van Ontdekken.",
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.", "components.Discover.PlexWatchlistSlider.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Je Plex Kijklijst", "components.Discover.PlexWatchlistSlider.plexwatchlist": "Jouw kijklijst",
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Recent toegevoegd", "components.Discover.RecentlyAddedSlider.recentlyAdded": "Onlangs toegevoegd",
"components.Discover.networks": "Netwerken", "components.Discover.networks": "Netwerken",
"components.Discover.CreateSlider.searchStudios": "Studio's zoeken…", "components.Discover.CreateSlider.searchStudios": "Studio's zoeken…",
"components.Discover.CreateSlider.starttyping": "Begin met typen om te zoeken.", "components.Discover.CreateSlider.starttyping": "Begin met typen om te zoeken.",
@@ -1184,8 +1184,8 @@
"components.Discover.DiscoverMovies.sortPopularityAsc": "Populariteit oplopend", "components.Discover.DiscoverMovies.sortPopularityAsc": "Populariteit oplopend",
"components.Discover.DiscoverMovies.sortPopularityDesc": "Populariteit aflopend", "components.Discover.DiscoverMovies.sortPopularityDesc": "Populariteit aflopend",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Releasedatum oplopend", "components.Discover.DiscoverMovies.sortReleaseDateAsc": "Releasedatum oplopend",
"components.Discover.DiscoverMovies.sortTitleAsc": "Titel (A-Z) oplopend", "components.Discover.DiscoverMovies.sortTitleAsc": "Titel oplopend (A-Z)",
"components.Discover.DiscoverMovies.sortTitleDesc": "Titel (Z-A) aflopend", "components.Discover.DiscoverMovies.sortTitleDesc": "Titel aflopend (Z-A)",
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMDB-beoordeling oplopend", "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMDB-beoordeling oplopend",
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "TMDB-beoordeling aflopend", "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "TMDB-beoordeling aflopend",
"components.Discover.DiscoverSliderEdit.deletefail": "Slider verwijderen mislukt.", "components.Discover.DiscoverSliderEdit.deletefail": "Slider verwijderen mislukt.",
@@ -1202,7 +1202,7 @@
"components.Discover.FilterSlideover.from": "Van", "components.Discover.FilterSlideover.from": "Van",
"components.Discover.FilterSlideover.genres": "Genres", "components.Discover.FilterSlideover.genres": "Genres",
"components.Discover.FilterSlideover.keywords": "Trefwoorden", "components.Discover.FilterSlideover.keywords": "Trefwoorden",
"components.Discover.FilterSlideover.originalLanguage": "Originele taal", "components.Discover.FilterSlideover.originalLanguage": "Oorspronkelijke taal",
"components.Discover.FilterSlideover.ratingText": "Beoordelingen tussen {minValue} en {maxValue}", "components.Discover.FilterSlideover.ratingText": "Beoordelingen tussen {minValue} en {maxValue}",
"components.Discover.FilterSlideover.releaseDate": "Releasedatum", "components.Discover.FilterSlideover.releaseDate": "Releasedatum",
"components.Discover.FilterSlideover.runtime": "Duur", "components.Discover.FilterSlideover.runtime": "Duur",
@@ -1262,18 +1262,87 @@
"components.Settings.SettingsJobsCache.availability-sync": "Synchronisatie van mediabeschikbaarheid", "components.Settings.SettingsJobsCache.availability-sync": "Synchronisatie van mediabeschikbaarheid",
"components.Discover.tmdbmoviestreamingservices": "Streamingdiensten voor films TMDB", "components.Discover.tmdbmoviestreamingservices": "Streamingdiensten voor films TMDB",
"components.Discover.tmdbtvstreamingservices": "Streamingdiensten voor series TMDB", "components.Discover.tmdbtvstreamingservices": "Streamingdiensten voor series TMDB",
"components.Discover.FilterSlideover.tmdbuservotecount": "Aantal stemmen TMDB-gebruikers", "components.Login.validationhostrequired": "{mediaServerName}-URL vereist",
"components.Discover.FilterSlideover.voteCount": "Aantal stemmen tussen {minValue} en {maxValue}", "components.Layout.UserWarnings.emailInvalid": "E-mailadres is ongeldig.",
"components.Settings.RadarrModal.tagRequests": "Tagverzoeken", "components.Login.description": "Aangezien dit de eerste keer is dat je je aanmeldt bij {applicationName}, dien je een geldig e-mailadres op te geven.",
"components.Settings.RadarrModal.tagRequestsInfo": "Voeg automatisch een extra tag toe met de gebruikers-ID en weergavenaam van de aanvrager", "components.Login.saving": "Toevoegen…",
"components.MovieDetails.imdbuserscore": "Gebruikersscore IMDB", "components.ManageSlideOver.removearr": "Verwijderen van {arr}",
"components.Settings.SonarrModal.tagRequests": "Tagverzoeken", "components.Settings.RadarrModal.tagRequests": "Aanvragen taggen",
"components.Settings.SonarrModal.tagRequestsInfo": "Voeg automatisch een extra tag toe met de gebruikers-ID en weergavenaam van de aanvrager", "components.MovieDetails.openradarr4k": "Film openen in 4K-Radarr",
"i18n.collection": "Collectie", "components.Settings.RadarrModal.tagRequestsInfo": "Automatisch een extra label toevoegen met de gebruikers-id en weergavenaam van de aanvrager",
"components.Settings.Notifications.NotificationsPushover.sound": "Meldingsgeluid",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Apparaatstandaard",
"components.Settings.SonarrModal.animeSeriesType": "Serietype anime", "components.Settings.SonarrModal.animeSeriesType": "Serietype anime",
"components.Settings.SonarrModal.tagRequestsInfo": "Automatisch een extra label toevoegen met de gebruikers-id en weergavenaam van de aanvrager",
"components.Settings.internalUrl": "Interne URL",
"components.Settings.jellyfinsettings": "{mediaServerName}-instellingen",
"components.Settings.jellyfinlibrariesDescription": "De {mediaServerName}-bibliotheken die op titels worden gescand. Klik op onderstaande knop als er geen bibliotheken in de lijst staan.",
"components.Settings.manualscanDescriptionJellyfin": "Normaliter wordt dit eenmaal per 24 uur uitgevoerd. Jellyseerr zal de lijst met onlangs toegevoegde media op je {mediaServerName}-server vaker controleren. Als dit de eerste keer is dat je Jellyseerr instelt, wordt aanbevolen eenmalig een handmatige, volledige bibliotheekscan uit te voeren!",
"components.Settings.save": "Wijzigingen opslaan",
"components.Settings.syncJellyfin": "Bibliotheken synchoniseren",
"components.TvDetails.play": "Afspelen op {mediaServerName}",
"components.Discover.FilterSlideover.tmdbuservotecount": "Aantal gebruikersstemmen TMDB",
"components.Login.save": "Toevoegen",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Hiermee wordt deze {mediaType} onomkeerbaar verwijderd van {arr}, inclusief alle bestanden.",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Apparaatstandaard",
"components.Settings.Notifications.userEmailRequired": "Gebruikerse-mail vereisen",
"components.Settings.SettingsAbout.supportjellyseerr": "Jellyseerr ondersteunen",
"components.Settings.SonarrModal.seriesType": "Serietype", "components.Settings.SonarrModal.seriesType": "Serietype",
"components.Settings.jellyfinSettings": "{mediaServerName}-instellingen",
"components.Setup.configuremediaserver": "Mediaserver instellen",
"components.TvDetails.play4k": "Afspelen op {mediaServerName} in 4K",
"components.UserList.mediaServerUser": "{mediaServerName}-gebruiker",
"components.UserList.noJellyfinuserstoimport": "Er zijn geen {mediaServerName}-gebruikers om te importeren.",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Apparaatstandaard",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Meldingsgeluid", "components.UserProfile.UserSettings.UserNotificationSettings.sound": "Meldingsgeluid",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Apparaatstandaard" "components.Login.signinwithjellyfin": "{mediaServerName}-account gebruiken",
"components.Discover.FilterSlideover.voteCount": "Aantal stemmen tussen {minValue} en {maxValue}",
"components.Layout.UserWarnings.emailRequired": "Een e-mailadres is vereist.",
"components.Layout.UserWarnings.passwordRequired": "Een wachtwoord is vereist.",
"components.Login.credentialerror": "Gebruikersnaam of wachtwoord is onjuist.",
"components.Login.emailtooltip": "Het adres hoeft niet gelieerd te zijn aan je {mediaServerName}-instantie.",
"components.Login.host": "{mediaServerName}-URL",
"components.Login.initialsignin": "Verbinden",
"components.Login.initialsigningin": "Verbinden…",
"components.Login.title": "E-mail toevoegen",
"components.Login.username": "Gebruikersnaam",
"components.Login.validationEmailRequired": "Je moet een e-mailadres opgeven",
"components.Login.validationEmailFormat": "Ongeldig e-mailadres",
"components.Login.validationemailformat": "Geldig e-mailadres vereist",
"components.Login.validationhostformat": "Geldige URL vereist",
"components.Login.validationusernamerequired": "Gebruikersnaam vereist",
"components.ManageSlideOver.removearr4k": "Verwijderen van 4K-{arr}",
"components.MovieDetails.downloadstatus": "Downloadstatus",
"components.MovieDetails.imdbuserscore": "Gebruikersbeoordeling IMDB",
"components.MovieDetails.openradarr": "Film openen in Radarr",
"components.MovieDetails.play": "Afspelen op {mediaServerName}",
"components.MovieDetails.play4k": "Afspelen op {mediaServerName} in 4K",
"components.Settings.Notifications.NotificationsPushover.sound": "Meldingsgeluid",
"components.Settings.SonarrModal.tagRequests": "Aanvragen taggen",
"components.Settings.jellyfinSettingsFailure": "Er is iets misgegaan bij het opslaan van de {mediaServerName}-instellingen.",
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName}-instellingen opgeslagen!",
"components.Settings.jellyfinlibraries": "{mediaServerName}-bibliotheken",
"components.Settings.manualscanJellyfin": "Handmatige bibliotheekscan",
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
"components.Settings.saving": "Opslaan…",
"components.Settings.syncing": "Synchroniseren",
"components.Settings.timeout": "Time-out",
"components.Setup.signin": "Aanmelden",
"components.Setup.signinWithJellyfin": "{mediaServerName}-account gebruiken",
"components.TitleCard.addToWatchList": "Toevoegen aan kijklijst",
"components.TitleCard.watchlistError": "Er is iets misgegaan. Probeer het opnieuw.",
"components.UserList.importfromJellyfin": "{mediaServerName}-gebruikers importeren",
"components.UserList.importfromJellyfinerror": "Er is iets misgegaan bij het importeren van {mediaServerName}-gebruikers.",
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "{mediaServerName}-gebruiker",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Wijzigingen opslaan",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Opslaan…",
"i18n.collection": "Collectie",
"components.UserProfile.localWatchlist": "Kijklijst van {username}",
"components.Setup.signinWithPlex": "Plex-account gebruiken",
"components.Settings.jellyfinSettingsDescription": "Optioneel, configureer de interne en externe eindpunten voor uw {mediaServerName} server. In de meeste gevallen verschilt de externe URL met de interne URL. Een aangepaste wachtwoord reset URL kan ook gebruikt worden voor de {mediaServerName} login, voor het geval dat u doorverwezen wilt worden naar een andere wachtwoord reset pagina.",
"components.Settings.jellyfinsettingsDescription": "Configureer de instellingen voor uw {mediaServerName} server. {mediaServerName} scanned uw {mediaServerName} bibliotheken om te zien welke content beschikbaar is.",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> Is succesvol verwijderd van de kijklijst!",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> succesvol toegevoegd aan de kijklijst!",
"components.TitleCard.watchlistCancel": "kijklijst voor <strong>{title}</strong> is geannuleerd.",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} succesvol geimporteerd!",
"components.UserList.newJellyfinsigninenabled": "De <strong>Gebruik Nieuwe {mediaServerName} Login</strong> instelling staat momenteel aan. {mediaServerName} gebruikers met toegang tot de bibliotheek, hoeven niet geïmporteerd te worden om in te kunnen loggen."
} }

View File

@@ -1061,196 +1061,212 @@
"components.MovieDetails.rtaudiencescore": "Ocena Rotten Tomatoes", "components.MovieDetails.rtaudiencescore": "Ocena Rotten Tomatoes",
"components.MovieDetails.rtcriticsscore": "Tomatometer Rotten Tomatoes", "components.MovieDetails.rtcriticsscore": "Tomatometer Rotten Tomatoes",
"components.MovieDetails.tmdbuserscore": "Ocena użytkowników TMDB", "components.MovieDetails.tmdbuserscore": "Ocena użytkowników TMDB",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Bieżąca częstotliwość", "components.Discover.CreateSlider.addSlider": "Dodaj suwak",
"components.TvDetails.seasonnumber": "Sezon {seasonNumber}", "components.Discover.CreateSlider.addcustomslider": "Utwórz niestandardowy suwak",
"components.TvDetails.seasonstitle": "Sezony", "components.Discover.CreateSlider.addfail": "Nie udało się utworzyć nowego suwaka.",
"components.Settings.SettingsJobsCache.imagecache": "Pamięć podręczna obrazów", "components.Discover.CreateSlider.addsuccess": "Stworzony nowy suwak i zapisano dostosowywania odkrywania.",
"components.PermissionEdit.viewrecent": "Wyświetl ostatnio dodane", "components.Discover.CreateSlider.editSlider": "Edytuj suwak",
"components.PermissionEdit.viewrecentDescription": "Przyznaj uprawnienia do przeglądania listy ostatnio dodanych multimediów.", "components.Discover.CreateSlider.editfail": "Nie udało się edytować suwaka.",
"components.TitleCard.cleardata": "Wyczyść dane", "components.Discover.CreateSlider.needresults": "Musisz mieć przynajmniej 1 wynik.",
"components.RequestList.RequestItem.tmdbid": "Identyfikator TMDB",
"components.RequestList.RequestItem.tvdbid": "Identyfikator TVDB",
"components.TitleCard.tmdbid": "Identyfikator TMDB",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Synchronizacja listy obserwowanych Plex",
"components.TitleCard.mediaerror": "Nie znaleziono {mediaType}",
"components.TitleCard.tvdbid": "Identyfikator TVDB",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Automatyczne zamawianie filmów z <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink>",
"components.PermissionEdit.autorequestSeriesDescription": "Udziel zgody na automatyczne przesyłanie próśb dotyczących multimediów innych niż 4K za pośrednictwem listy obserwowanych Plex.",
"components.PermissionEdit.viewwatchlists": "Wyświetlanie list obserwacyjnych Plex",
"components.PermissionEdit.viewwatchlistsDescription": "Przyznaj uprawnienia do przeglądania list obserwowanych Plex innych użytkowników.",
"components.RequestCard.tmdbid": "Identyfikator TMDB",
"components.RequestCard.tvdbid": "Identyfikator TVDB",
"components.Settings.SettingsLogs.viewdetails": "Zobacz szczegóły",
"components.Settings.restartrequiredTooltip": "Overseerr musi zostać ponownie uruchomiony, aby zmiany tego ustawienia zaczęły obowiązywać",
"components.TvDetails.reportissue": "Zgłoś problem",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Automatyczna prośba o serial",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Automatyczne zamawianie filmów z <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink>",
"components.UserProfile.plexwatchlist": "Lista obserwowanych Plex",
"components.RequestCard.cancelrequest": "Anuluj prośbę",
"components.RequestCard.declinerequest": "Odrzuć prośbę",
"components.RequestCard.approverequest": "Zatwierdź prośbę",
"components.DownloadBlock.formattedTitle": "{title}: Sezon {seasonNumber} Odcinek {episodeNumber}",
"components.RequestBlock.approve": "Zatwierdź prośbę",
"components.RequestBlock.decline": "Odrzuć prośbę",
"components.RequestBlock.delete": "Usuń prośbę",
"components.RequestBlock.edit": "Edytuj prośbę",
"components.RequestBlock.lastmodifiedby": "Ostatnio zmodyfikowane przez",
"components.RequestBlock.requestdate": "Data złożenia prośby",
"components.RequestBlock.requestedby": "Prośba zgłoszona przez",
"components.RequestCard.editrequest": "Edytuj prośbę",
"components.RequestModal.requestcollection4ktitle": "Poproś o kolekcję w 4K",
"components.RequestModal.requestcollectiontitle": "Poproś o kolekcję",
"components.RequestModal.requestmovie4ktitle": "Poproś o film w 4K",
"components.RequestModal.requestmovietitle": "Poproś o film",
"components.RequestModal.requestseries4ktitle": "Poproś o serial w 4K",
"components.RequestModal.requestseriestitle": "Poproś o serial",
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Czyszczenie pamięci podręcznej obrazów",
"components.Settings.SettingsJobsCache.imagecacheDescription": "Po włączeniu w ustawieniach Overseerr będzie pośredniczyć i buforować obrazy ze wstępnie skonfigurowanych źródeł zewnętrznych. Obrazy z pamięci podręcznej są zapisywane w folderze konfiguracji. Możesz znaleźć pliki w <code>{appDataPath}/cache/images</code>.",
"components.Settings.SettingsJobsCache.imagecachecount": "Obrazy zapisane w pamięci podręcznej",
"components.Settings.SettingsJobsCache.imagecachesize": "Całkowity rozmiar pamięci podręcznej",
"components.Settings.advancedTooltip": "Nieprawidłowe skonfigurowanie tego ustawienia może spowodować nieprawidłowe działanie",
"components.Settings.experimentalTooltip": "Włączenie tego ustawienia może spowodować nieoczekiwane zachowanie aplikacji",
"components.TvDetails.rtaudiencescore": "Ocena publiczności Rotten Tomatoes",
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometr",
"components.TvDetails.status4k": "4K {status}",
"components.TvDetails.tmdbuserscore": "Ocena użytkowników TMDB",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Filmy z próśb automatycznych",
"components.UserProfile.emptywatchlist": "W tym miejscu pojawią się multimedia dodane do <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink>.",
"components.RequestCard.unknowntitle": "Nieznany tytuł",
"components.RequestList.RequestItem.unknowntitle": "Nieznany tytuł",
"components.StatusBadge.playonplex": "Odtwórz na Plex",
"components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}",
"components.TvDetails.Season.noepisodes": "Lista odcinków jest niedostępna.",
"components.TvDetails.Season.somethingwentwrong": "Coś poszło nie tak podczas pobierania danych o sezonie.",
"components.TvDetails.manageseries": "Zarządzaj serialem",
"components.Discover.emptywatchlist": "W tym miejscu pojawią się multimedia dodane do <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink>.",
"components.RequestModal.SearchByNameModal.nomatches": "Nie udało nam się znaleźć dopasowania do tej serii.",
"components.StatusBadge.managemedia": "Zarządzaj {mediaType}",
"components.StatusBadge.openinarr": "Otwórz w {arr}",
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Odcinek} other {# Odcinki}}",
"components.Discover.CreateSlider.addSlider": "Dodaj Suwak",
"components.Discover.CreateSlider.addcustomslider": "Utwórz niestandardowy Suwak",
"components.Discover.CreateSlider.addfail": "Nie udało się utworzyć nowego Suwaka.",
"components.Discover.CreateSlider.addsuccess": "Utworzono nowy Suwaki zapisano ustawienia dostosowywania odnajdywania.",
"components.Discover.CreateSlider.editSlider": "Edytuj Suwak",
"components.Discover.CreateSlider.editfail": "Nie udało się edytować Suwaka.",
"components.Discover.CreateSlider.editsuccess": "Edytowano Suwak i zapisano ustawienia odkrywania.",
"components.Discover.CreateSlider.needresults": "Musisz mieć co najmniej 1 wynik.",
"components.Discover.CreateSlider.nooptions": "Brak wyników.", "components.Discover.CreateSlider.nooptions": "Brak wyników.",
"components.Discover.CreateSlider.providetmdbgenreid": "Podaj identyfikator gatunku TMDB", "components.Discover.CreateSlider.providetmdbgenreid": "Podaj ID gatunku TMDB",
"components.Discover.CreateSlider.providetmdbkeywordid": "Podaj identyfikator słowa kluczowego TMDB", "components.Discover.CreateSlider.providetmdbnetwork": "Podaj ID stacji TMDB",
"components.Discover.CreateSlider.providetmdbnetwork": "Podaj identyfikator platformy TMDB",
"components.Discover.CreateSlider.providetmdbsearch": "Podaj zapytanie wyszukiwania", "components.Discover.CreateSlider.providetmdbsearch": "Podaj zapytanie wyszukiwania",
"components.Discover.CreateSlider.providetmdbstudio": "Podaj identyfikator studia TMDB", "components.Discover.CreateSlider.providetmdbstudio": "Podaj ID studia TMDB",
"components.Discover.CreateSlider.searchGenres": "Szukaj gatunków…", "components.Discover.CreateSlider.searchGenres": "Szukaj gatunków…",
"components.Discover.CreateSlider.searchKeywords": "Szukaj słów kluczowych…", "components.Discover.CreateSlider.searchStudios": "Wyszukaj studia…",
"components.Discover.CreateSlider.slidernameplaceholder": "Nazwa slidera", "components.Discover.CreateSlider.slidernameplaceholder": "Nazwa suwaka",
"components.Discover.CreateSlider.starttyping": "Zacznij pisać, aby wyszukać.", "components.Discover.CreateSlider.starttyping": "Zacznij pisać aby wyszukać.",
"components.Discover.CreateSlider.validationDatarequired": "Musisz uzyskać wartość danych.", "components.Discover.CreateSlider.validationDatarequired": "Należy wprowadzić wartość danych.",
"components.Discover.CreateSlider.validationTitlerequired": "Musisz podać tytuł.", "components.Discover.CreateSlider.validationTitlerequired": "Należy podać tytuł.",
"components.Discover.DiscoverMovieKeyword.keywordMovies": "{keywordTitle} Filmy", "components.Discover.DiscoverMovieKeyword.keywordMovies": "Filmy: {keywordTitle}",
"components.Discover.DiscoverSliderEdit.deletefail": "Nie udało się usunąć slidera.",
"components.Discover.DiscoverSliderEdit.remove": "Usuń",
"components.Discover.DiscoverTvKeyword.keywordSeries": "Serial {keywordTitle}",
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# aktywny filtr} other {# aktywnych filtrów}}",
"components.Discover.DiscoverMovies.discovermovies": "Filmy", "components.Discover.DiscoverMovies.discovermovies": "Filmy",
"components.Discover.DiscoverMovies.sortPopularityAsc": "Popularność rosnąco", "components.Discover.DiscoverMovies.sortPopularityAsc": "Popularność rosnąco",
"components.Discover.DiscoverMovies.sortPopularityDesc": "Popularność malejąco", "components.Discover.DiscoverMovies.sortPopularityDesc": "Popularność malejąco",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Data wydania rosnąco",
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Data wydania malejąco", "components.Discover.DiscoverMovies.sortReleaseDateDesc": "Data wydania malejąco",
"components.Discover.DiscoverMovies.sortTitleAsc": "Tytuł (A-Z) rosnąco", "components.Discover.DiscoverMovies.sortTitleAsc": "Tytuł (A-Z) rosnąco",
"components.Discover.DiscoverMovies.sortTitleDesc": "Tytuł (Z-A) malejąco", "components.Discover.DiscoverMovies.sortTitleDesc": "Tytuł (Z-A) malejąco",
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Ocena TMDB rosnąco", "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Ocena TMDB rosnąco",
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Ocena TMDB malejąco", "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Ocena TMDB malejąco",
"components.Discover.DiscoverSliderEdit.deletesuccess": "Pomyślnie usunięto slider.", "components.Discover.DiscoverSliderEdit.deletefail": "Nie udało się usunąć suwaka.",
"components.Discover.DiscoverSliderEdit.deletesuccess": "Pomyślnie usunięto suwak.",
"components.Discover.DiscoverSliderEdit.enable": "Przełącz widoczność", "components.Discover.DiscoverSliderEdit.enable": "Przełącz widoczność",
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# aktywny filtr} other {# aktywnych filtrów}}", "components.Discover.DiscoverSliderEdit.remove": "Usuń",
"components.Discover.DiscoverTv.discovertv": "Seriale", "components.Discover.DiscoverTv.sortFirstAirDateAsc": "Data premiery rosnąco",
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Data pierwszej emisji rosnąco", "components.Discover.DiscoverTv.sortFirstAirDateDesc": "Data premiery malejąco",
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Pierwsza data emisji malejąco",
"components.Discover.DiscoverTv.sortPopularityAsc": "Popularność rosnąco", "components.Discover.DiscoverTv.sortPopularityAsc": "Popularność rosnąco",
"components.Discover.DiscoverTv.sortPopularityDesc": "Popularność malejąco", "components.Discover.DiscoverTv.sortPopularityDesc": "Popularność malejąco",
"components.Discover.DiscoverTv.sortTitleAsc": "Tytuł (A-Z) rosnąco", "components.Discover.DiscoverTv.sortTitleAsc": "Tytuł (A-Z) rosnąco",
"components.Discover.DiscoverTv.sortTitleDesc": "Tytuł (Z-A) malejąco", "components.Discover.DiscoverTv.sortTitleDesc": "Tytuł (Z-A) malejąco",
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB rosnąco",
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "Ocena TMDB malejąco", "components.Discover.DiscoverTv.sortTmdbRatingDesc": "Ocena TMDB malejąco",
"components.Discover.CreateSlider.searchStudios": "Szukaj studiów…",
"components.Settings.SettingsMain.general": "Ogólne",
"components.Settings.SettingsMain.generalsettings": "Ustawienia ogólne",
"components.Settings.SettingsMain.originallanguageTip": "Filtruj zawartość według języka oryginału",
"components.Discover.PlexWatchlistSlider.emptywatchlist": "W tym miejscu pojawią się multimedia dodane do <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink>.",
"components.Discover.networks": "Platformy",
"components.Discover.moviegenres": "Gatunki filmowe",
"components.Discover.tmdbnetwork": "Platforma TMDB",
"components.Discover.tmdbstudio": "Studio TMDB",
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Twoja lista obserwowanych Plex",
"components.Discover.createnewslider": "Utwórz nowy suwak",
"components.Discover.customizediscover": "Dostosowywanie funkcji Odkryj",
"components.Discover.resetfailed": "Wystąpił problem podczas resetowania ustawień odnajdywania.",
"components.Discover.resetsuccess": "Pomyślnie zresetowano ustawienia odnajdywania.",
"components.Discover.resetwarning": "Przywróć wszystkie Suwaki do ustawień domyślnych. Spowoduje to również usunięcie wszystkich niestandardowych Suwaków!",
"components.Discover.resettodefault": "Przywróć ustawienia domyślne",
"components.Discover.studios": "Studia",
"components.Discover.tmdbmoviegenre": "Gatunek filmu TMDB",
"components.Discover.tvgenres": "Gatunki serialu",
"components.Settings.SettingsMain.csrfProtectionTip": "Ustaw zewnętrzny dostęp api na tylko do odczytu (wymaga HTTPS)",
"components.Settings.SettingsMain.csrfProtectionHoverTip": "NIE włączaj tego ustawienia, chyba że rozumiesz, co robisz!",
"components.Settings.SettingsMain.generalsettingsDescription": "Skonfiguruj globalne i domyślne ustawienia dla Overseerr.",
"components.Settings.SettingsMain.hideAvailable": "Ukryj dostępne multimedia",
"components.Settings.SettingsMain.trustProxyTip": "Pozwól Overseerr poprawnie rejestrować adresy IP klientów za serwerem proxy",
"components.Settings.SettingsMain.toastSettingsSuccess": "Ustawienia zostały zapisane pomyślnie!",
"components.Settings.SettingsMain.trustProxy": "Włącz obsługę proxy",
"components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "Adres URL nie może kończyć się ukośnikiem",
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# aktywny filtr} other {# aktywnych filtrów}}",
"components.Discover.FilterSlideover.clearfilters": "Wyczyść aktywne filtry", "components.Discover.FilterSlideover.clearfilters": "Wyczyść aktywne filtry",
"components.Discover.FilterSlideover.filters": "Filtry", "components.Discover.FilterSlideover.filters": "Filtry",
"components.Discover.FilterSlideover.firstAirDate": "Pierwsza data emisji", "components.Discover.FilterSlideover.firstAirDate": "Data pierwszego wyemitowania",
"components.Discover.FilterSlideover.from": "Od", "components.Discover.FilterSlideover.from": "Od",
"components.Discover.FilterSlideover.genres": "Gatunki", "components.Discover.FilterSlideover.genres": "Gatunki",
"components.Discover.FilterSlideover.keywords": "Słowa kluczowe", "components.Discover.FilterSlideover.keywords": "Słowa kluczowe",
"components.Discover.FilterSlideover.originalLanguage": "Język oryginalny", "components.Discover.FilterSlideover.ratingText": "Oceny między {minValue} a {maxValue}",
"components.Discover.FilterSlideover.ratingText": "Oceny pomiędzy {minValue} a {maxValue}", "components.Discover.CreateSlider.providetmdbkeywordid": "Podaj ID słowa kluczowego TMDB",
"components.Discover.CreateSlider.searchKeywords": "Wyszukaj słowa kluczowe…",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Data wydania rosnąco",
"components.Discover.DiscoverTv.discovertv": "Seriale",
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB rosnąco",
"components.Discover.DiscoverTvKeyword.keywordSeries": "Seriale: {keywordTitle}",
"components.Discover.FilterSlideover.originalLanguage": "Oryginalny język",
"components.Discover.FilterSlideover.releaseDate": "Data wydania", "components.Discover.FilterSlideover.releaseDate": "Data wydania",
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} czas odtwarzania w minutach", "components.Discover.FilterSlideover.runtime": "Długość",
"components.Discover.FilterSlideover.runtime": "Czas odtwarzania", "components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minut trwania",
"components.Discover.FilterSlideover.tmdbuserscore": "Ocena użytkowników TMDB", "components.Discover.FilterSlideover.streamingservices": "Serwisy streamingowe",
"components.Discover.FilterSlideover.studio": "Studio",
"components.Discover.FilterSlideover.tmdbuservotecount": "Liczba głosów użytkowników TMDB",
"components.Discover.FilterSlideover.to": "Do", "components.Discover.FilterSlideover.to": "Do",
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Ostatnio dodane", "components.Discover.FilterSlideover.voteCount": "Liczba głosów między {minValue} a {maxValue}",
"components.Discover.stopediting": "Zatrzymaj edycję", "components.Discover.PlexWatchlistSlider.plexwatchlist": "Twoja lista obserwowanych Plex",
"components.Discover.tmdbmoviekeyword": "Słowo kluczowe filmu TMDB", "components.Discover.RecentlyAddedSlider.recentlyAdded": "Niedawno dodane",
"components.Discover.tmdbsearch": "Wyszukiwanie TMDB", "components.Discover.createnewslider": "Dodaj nowy suwak",
"components.Layout.Sidebar.browsemovies": "Filmy", "components.Discover.customizediscover": "Dostosuj Odkryj",
"components.Layout.Sidebar.browsetv": "Seriale", "components.Discover.PlexWatchlistSlider.emptywatchlist": "Media dodane do twojej <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink> zostaną wyświetlone tutaj.",
"components.Selector.searchGenres": "Wybierz gatunki…", "components.Discover.emptywatchlist": "Media dodane do twojej <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink> zostaną wyświetlone tutaj.",
"components.Selector.searchKeywords": "Szukaj słów kluczowych…", "components.Discover.tmdbstudio": "Studio TMDB",
"components.Selector.showmore": "Pokaż więcej",
"components.Selector.starttyping": "Zacznij pisać, aby wyszukać.",
"components.Settings.SettingsMain.applicationTitle": "Tytuł aplikacji",
"components.Settings.SettingsMain.applicationurl": "Adres URL aplikacji",
"components.Settings.SettingsMain.cacheImages": "Włącz buforowanie obrazów",
"components.Settings.SettingsMain.csrfProtection": "Włącz ochronę CSRF",
"components.Settings.SettingsMain.locale": "Język wyświetlania",
"components.Settings.SettingsMain.originallanguage": "Odkryj język",
"components.Settings.SettingsMain.partialRequestsEnabled": "Zezwalaj na prośby o część serialu",
"components.Settings.SettingsMain.region": "Odkryj region",
"components.Settings.SettingsMain.regionTip": "Filtruj zawartość według dostępności regionalnej",
"components.Settings.SettingsMain.toastApiKeyFailure": "Coś poszło nie tak podczas generowania nowego klucza API.",
"components.Settings.SettingsMain.toastApiKeySuccess": "Nowy klucz API został pomyślnie wygenerowany!",
"components.Settings.SettingsMain.validationApplicationTitle": "Należy podać tytuł aplikacji",
"components.Settings.SettingsMain.validationApplicationUrl": "Musisz podać prawidłowy adres URL",
"components.Discover.FilterSlideover.streamingservices": "Usługi streamingowe",
"components.Discover.FilterSlideover.studio": "Studia",
"components.Discover.tmdbtvgenre": "Gatunek serialu TMDB",
"components.Discover.tmdbtvkeyword": "Słowo kluczowe serialu TMDB", "components.Discover.tmdbtvkeyword": "Słowo kluczowe serialu TMDB",
"components.Discover.updatesuccess": "Zaktualizowano ustawienia odnajdywania.", "components.Discover.tmdbtvgenre": "Gatunek serialu TMDB",
"components.Selector.nooptions": "Brak wyników.", "components.Discover.tmdbsearch": "Wyszukiwanie TMDB",
"components.Discover.FilterSlideover.tmdbuserscore": "Ocena użytkownika TMDB",
"components.Discover.moviegenres": "Gatunki filmu",
"components.Discover.networks": "Kanały",
"components.Discover.CreateSlider.editsuccess": "Edytowano suwak i zapisano ustawienia personalizacji.",
"components.Discover.stopediting": "Zakończ edytowanie",
"components.Discover.studios": "Studia",
"components.DownloadBlock.formattedTitle": "{title}: Sezon {seasonNumber} Odcinek {episodeNumber}",
"components.Discover.tvgenres": "Gatunki seriali",
"components.Discover.updatefailed": "Coś poszło nie tak podczas aktualizacji ustawień personalizacji wykrywania.",
"components.Discover.updatesuccess": "Zaktualizowano ustawienia personalizacji wykrywania.",
"components.Discover.resettodefault": "Przywróć domyślne",
"components.Discover.resetfailed": "Coś poszło nie tak podczas resetowania ustawień personalizacji wykrywania.",
"components.Discover.resetsuccess": "Pomyślnie zresetowano ustawienia wykrywania.",
"components.Discover.tmdbmoviegenre": "Gatunek filmu TMDB",
"components.Layout.Sidebar.browsetv": "Seriale",
"components.Layout.UserWarnings.emailInvalid": "Adres e-mail jest nieprawidłowy.",
"components.Layout.UserWarnings.passwordRequired": "Wymagane jest podanie hasła.",
"components.Login.credentialerror": "Nazwa użytkownika lub hasło są nieprawidłowe.",
"components.Login.emailtooltip": "Adres nie musi być powiązany z instancją {mediaServerName}.",
"components.Login.host": "{mediaServerName} URL",
"components.Login.initialsignin": "Połącz",
"components.Login.initialsigningin": "Łączenie…",
"components.Login.save": "Reklama",
"components.Login.saving": "Dodaję…",
"components.Login.signinwithjellyfin": "Użyj swojego konta {mediaServerName}",
"components.Login.title": "Dodaj e-mail",
"components.Login.username": "Nazwa użytkownika",
"components.Login.validationEmailRequired": "Musisz podać adres e-mail",
"components.Login.validationemailformat": "Wymagany poprawny adres e-mail",
"components.Login.validationhostformat": "Wymagany poprawny adres URL",
"components.Login.validationhostrequired": "{mediaServerName} URL wymagany",
"components.Login.validationusernamerequired": "Wymagana nazwa użytkownika",
"components.ManageSlideOver.removearr4k": "Usuń z 4k {arr}",
"components.MovieDetails.downloadstatus": "Status pobierania",
"components.MovieDetails.openradarr4k": "Otwórz film 4k w Radarr",
"components.MovieDetails.play": "Odtwórz na {mediaServerName}",
"components.MovieDetails.play4k": "Odtwórz w 4K na {mediaServerName}",
"components.PermissionEdit.viewrecent": "Wyświetl ostatnio dodane",
"components.PermissionEdit.viewrecentDescription": "Zezwolenie na wyświetlanie listy ostatnio dodanych multimediów.",
"components.RequestBlock.approve": "Zatwierdź żądanie",
"components.RequestBlock.decline": "Odrzuć żądanie",
"components.RequestBlock.delete": "Usuń żądanie",
"components.RequestBlock.lastmodifiedby": "Ostatnio modyfikowany przez",
"components.RequestBlock.requestdate": "Data żądania",
"components.RequestBlock.requestedby": "Żądany przez",
"components.RequestCard.cancelrequest": "Anuluj prośbę",
"components.RequestCard.declinerequest": "Odrzuć prośbę",
"components.RequestCard.editrequest": "Edytuj prośbę",
"components.RequestCard.unknowntitle": "Nieznany tytuł",
"components.RequestModal.requestcollectiontitle": "Poproś o kolekcję",
"components.RequestModal.requestmovie4ktitle": "Poproś o film w 4K",
"components.RequestModal.requestmovietitle": "Poproś o film",
"components.RequestModal.requestseries4ktitle": "Poproś o serial w 4K",
"components.RequestModal.requestseriestitle": "Poproś o serial",
"components.Selector.searchGenres": "Wybierz gatunki…",
"components.Selector.searchKeywords": "Wyszukaj słowa kluczowe…",
"components.Selector.searchStudios": "Szukaj studiów…", "components.Selector.searchStudios": "Szukaj studiów…",
"components.Selector.showless": "Pokaż mniej", "components.Selector.showless": "Pokaż mniej",
"components.Discover.updatefailed": "Wystąpił problem podczas aktualizowania ustawień odnajdywania.", "components.Selector.starttyping": "Rozpocznij wpisywanie, aby wyszukać.",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Ostatnio dodane skanowanie Jellyfin",
"components.Settings.SettingsMain.apikey": "Klucz API", "components.Settings.SettingsMain.apikey": "Klucz API",
"components.Settings.SettingsMain.cacheImagesTip": "Pamięć podręczna dla obrazów pochodzących z zewnętrznych źródeł (wymaga znacznej ilości miejsca na dysku)", "components.Settings.SettingsMain.applicationTitle": "Nazwa aplikacji",
"components.Settings.SettingsMain.applicationurl": "URL aplikacji",
"components.Settings.SettingsMain.csrfProtection": "Włącz ochronę CSRF",
"components.Settings.SettingsMain.csrfProtectionHoverTip": "NIE włączaj tego ustawienia, jeśli nie rozumiesz, co robisz!",
"components.Settings.SettingsMain.csrfProtectionTip": "Ustawienie dostępu do zewnętrznego API tylko do odczytu (wymaga HTTPS)",
"components.Settings.SettingsMain.general": "Ogólne",
"components.Settings.SettingsMain.generalsettings": "Ustawienia ogólne",
"components.Settings.SettingsMain.generalsettingsDescription": "Konfiguracja globalnych i domyślnych ustawień Overseerr.",
"components.Settings.SettingsMain.hideAvailable": "Ukryj dostępne media",
"components.Settings.SettingsMain.locale": "Wyświetlany język",
"components.Settings.SettingsMain.partialRequestsEnabled": "Zezwalaj na częściowe żądania seriali",
"components.Settings.SettingsMain.region": "Region odkrywania",
"components.Settings.SettingsMain.regionTip": "Filtrowanie zawartości według dostępności regionalnej",
"components.Settings.SettingsMain.toastSettingsFailure": "Coś poszło nie tak podczas zapisywania ustawień.", "components.Settings.SettingsMain.toastSettingsFailure": "Coś poszło nie tak podczas zapisywania ustawień.",
"i18n.collection": "Kolekcja", "components.Settings.SettingsMain.trustProxyTip": "Umożliwienie Overseerr poprawnego rejestrowania adresów IP klientów za serwerem proxy",
"components.MovieDetails.imdbuserscore": "Ocena użytkowników IMDB", "components.Settings.jellyfinSettings": "{mediaServerName} Ustawienia",
"components.Settings.SonarrModal.seriesType": "Typ serialu" "components.Settings.jellyfinSettingsFailure": "Coś poszło nie tak podczas zapisywania ustawień {mediaServerName}.",
"components.Settings.jellyfinSettingsSuccess": "Ustawienia {mediaServerName} zostały pomyślnie zapisane!",
"components.Settings.jellyfinlibrariesDescription": "Biblioteki {mediaServerName} skanują w poszukiwaniu tytułów. Kliknij poniższy przycisk, jeśli na liście nie ma żadnych bibliotek.",
"components.Settings.jellyfinsettings": "{mediaServerName} ustawienia",
"components.Settings.jellyfinsettingsDescription": "Konfiguracja ustawień serwera {mediaServerName}. {mediaServerName} skanuje biblioteki {mediaServerName}, aby sprawdzić, jaka zawartość jest dostępna.",
"components.Settings.manualscanJellyfin": "Ręczne skanowanie biblioteki",
"components.Settings.saving": "Zapisywanie…",
"components.Settings.syncing": "Synchronizowanie",
"components.Setup.signinWithPlex": "Użyj konta Plex",
"components.StatusBadge.openinarr": "Otwórz w {arr}",
"components.StatusBadge.playonplex": "Odtwórz na {mediaServerName}",
"components.TitleCard.watchlistError": "Coś poszło nie tak, spróbuj ponownie.",
"components.TvDetails.Season.somethingwentwrong": "Coś poszło nie tak podczas pobierania danych sezonu.",
"components.TvDetails.seasonnumber": "Sezon {seasonNumber}",
"components.TvDetails.seasonstitle": "Sezony",
"components.TvDetails.status4k": "4K {status}",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Automatyczne żądania filmów",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Zapisz zmiany",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Zapisywanie…",
"components.Layout.Sidebar.browsemovies": "Filmy",
"components.Login.validationEmailFormat": "Nieprawidłowy adres e-mail",
"components.RequestModal.SearchByNameModal.nomatches": "Nie udało nam się znaleźć odpowiednika dla tego serialu.",
"components.Selector.showmore": "Pokaż więcej",
"components.Settings.SettingsMain.cacheImagesTip": "Pamięć podręczna obrazów pochodzących z zewnątrz (wymaga znacznej ilości miejsca na dysku)",
"components.Settings.SettingsMain.originallanguageTip": "Filtrowanie zawartości według oryginalnego języka",
"components.Settings.SettingsMain.toastSettingsSuccess": "Ustawienia zapisane pomyślnie!",
"components.Settings.experimentalTooltip": "Włączenie tego ustawienia może spowodować nieoczekiwane zachowanie aplikacji",
"components.UserList.mediaServerUser": "{mediaServerName} Użytkownik",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Email",
"components.Discover.resetwarning": "Przywróć domyślne ustawienia wszystkich suwaków. Spowoduje to również usunięcie wszystkich niestandardowych suwaków!",
"components.Layout.UserWarnings.emailRequired": "Wymagany jest adres e-mail.",
"components.MovieDetails.openradarr": "Otwórz film w Radarr",
"components.RequestModal.requestcollection4ktitle": "Poproś o kolekcję w 4K",
"components.Setup.signinWithJellyfin": "Użyj konta {mediaServerName}",
"components.Login.description": "Ponieważ logujesz się do {applicationName} po raz pierwszy, musisz dodać prawidłowy adres e-mail.",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Spowoduje to nieodwracalne usunięcie tego {mediaType} z {arr}, w tym wszystkich plików.",
"components.ManageSlideOver.removearr": "Usuń z {arr}",
"components.PermissionEdit.autorequestSeriesDescription": "Zezwolenie na automatyczne przesyłanie żądań dotyczących seriali innych niż 4K za pośrednictwem listy obserwowanych Plex.",
"components.PermissionEdit.viewwatchlists": "Wyświetl {mediaServerName} watchlistę",
"components.RequestCard.approverequest": "Zaakceptuj prośbę",
"components.Settings.RadarrModal.tagRequestsInfo": "Automatycznie dodawaj dodatkowy znacznik z identyfikatorem użytkownika i wyświetlaną nazwą użytkownika",
"components.Settings.SettingsAbout.supportjellyseerr": "Wspomóż Jellyseerr",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Pełne skanowanie bibliotek Jellyfin",
"components.Settings.SettingsLogs.viewdetails": "Zobacz szczegóły",
"components.Settings.jellyfinSettingsDescription": "Opcjonalnie skonfiguruj wewnętrzne i zewnętrzne punkty końcowe dla serwera {mediaServerName}. W większości przypadków zewnętrzny adres URL różni się od wewnętrznego adresu URL. Niestandardowy adres URL resetowania hasła można również ustawić dla logowania {mediaServerName}, na wypadek gdybyś chciał przekierować na inną stronę resetowania hasła.",
"components.Settings.save": "Zapisz zmiany",
"components.Settings.syncJellyfin": "Synchronizuj biblioteki",
"components.TvDetails.Season.noepisodes": "Lista odcinków jest niedostępna.",
"components.Settings.Notifications.NotificationsPushover.sound": "Dźwięk powiadomień",
"components.Settings.RadarrModal.tagRequests": "Żądania tagów",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Aktualna częstotliwość",
"components.Settings.SettingsMain.validationApplicationTitle": "Musisz podać tytuł aplikacji",
"components.Settings.SonarrModal.seriesType": "Typ seriali",
"components.Settings.advancedTooltip": "Nieprawidłowe skonfigurowanie tego ustawienia może spowodować nieprawidłowe działanie",
"components.TvDetails.reportissue": "Zgłoś problem",
"components.RequestBlock.edit": "Edytuj żądanie",
"components.RequestList.RequestItem.unknowntitle": "Nieznany tytuł",
"components.Selector.nooptions": "Brak wyników.",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Domyślne urządzenie",
"components.Settings.Notifications.userEmailRequired": "Wymagaj adresu e-mail użytkownika",
"components.Settings.manualscanDescriptionJellyfin": "Zwykle będzie to uruchamiane tylko raz na 24 godziny. Jellyseerr będzie bardziej agresywnie sprawdzać ostatnio dodane biblioteki serwera {mediaServerName}. Jeśli po raz pierwszy konfigurujesz Jellyseerr, zalecane jest jednorazowe pełne ręczne skanowanie biblioteki!"
} }

View File

@@ -1257,9 +1257,35 @@
"components.Discover.FilterSlideover.tmdbuservotecount": "Qtd de Votos de Usuários TMDB", "components.Discover.FilterSlideover.tmdbuservotecount": "Qtd de Votos de Usuários TMDB",
"components.Discover.FilterSlideover.voteCount": "Qtd the votos entre {minValue} e {maxValue}", "components.Discover.FilterSlideover.voteCount": "Qtd the votos entre {minValue} e {maxValue}",
"components.Settings.RadarrModal.tagRequestsInfo": "Adicione automaticamente uma tag extra com o ID de usuário e o nome de exibição do solicitante", "components.Settings.RadarrModal.tagRequestsInfo": "Adicione automaticamente uma tag extra com o ID de usuário e o nome de exibição do solicitante",
"i18n.collection": "Coleção", "components.Layout.UserWarnings.emailRequired": "Um endereço de e-mail é necessário.",
"components.MovieDetails.imdbuserscore": "Pontuação de usuário IMDB", "components.Login.credentialerror": "O nome de usuário ou senha está incorreto.",
"components.Settings.SonarrModal.tagRequestsInfo": "Adiciona automaticamente uma tag adicional com o ID de usuário e nome de exibição de quem pediu", "components.Login.description": "Já que é sua primeira vez entrando em {applicationName}, você precisa adicionar um e-mail válido.",
"components.Settings.SonarrModal.tagRequests": "Marcar Pedidos", "components.Login.host": "URL de {mediaServerName}",
"components.Settings.RadarrModal.tagRequests": "Marcar Pedidos" "components.Login.initialsignin": "Conectar",
"components.Login.initialsigningin": "Conectando…",
"components.Login.save": "Adicionar",
"components.Login.saving": "Adicionando…",
"components.Login.signinwithjellyfin": "Use sua conta de {mediaServerName}",
"components.Login.title": "Adicionar E-Mail",
"components.Login.username": "Nome de usuário",
"components.Login.validationEmailFormat": "E-mail inválido",
"components.Login.validationEmailRequired": "Você precisa providenciar um e-mail",
"components.Login.validationemailformat": "E-mail válido necessário",
"components.Login.validationhostformat": "URL válido necessário",
"components.Login.validationusernamerequired": "Nome de usuário necessário",
"components.ManageSlideOver.removearr": "Remover de {arr}",
"components.ManageSlideOver.removearr4k": "Remover de {arr} 4K",
"components.MovieDetails.downloadstatus": "Status de download",
"components.MovieDetails.imdbuserscore": "Avaliação de usuário no IMDB",
"components.MovieDetails.openradarr": "Abrir filme no Radarr",
"components.Settings.Notifications.NotificationsPushover.sound": "Som de notificação",
"components.Login.emailtooltip": "Endereço não precisa ser associado à sua instância de {mediaServerName}.",
"components.Layout.UserWarnings.emailInvalid": "Endereço de e-mail inválido.",
"components.Layout.UserWarnings.passwordRequired": "Uma senha é necessária.",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Padrão do dispositivo",
"components.Login.validationhostrequired": "URL de {mediaServerName} necessário",
"components.MovieDetails.openradarr4k": "Abrir filme em Radarr 4K",
"components.MovieDetails.play": "Reproduzir em {mediaServerName}",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Isto irá remover este {mediaType} de {arr}, incluindo todos os arquivos.",
"components.MovieDetails.play4k": "Reproduzir em 4K em {mediaServerName}"
} }

View File

@@ -370,7 +370,7 @@
"components.PlexLoginButton.signinwithplex": "Conectat", "components.PlexLoginButton.signinwithplex": "Conectat",
"components.QuotaSelector.movieRequests": "{quotaLimit} <quotaUnits>{movies} per {quotaDays} {days}</quotaUnits>", "components.QuotaSelector.movieRequests": "{quotaLimit} <quotaUnits>{movies} per {quotaDays} {days}</quotaUnits>",
"components.PersonDetails.lifespan": "{birthdate} {deathdate}", "components.PersonDetails.lifespan": "{birthdate} {deathdate}",
"components.RequestBlock.seasons": "{seasonCount, plural, one {Sezon} other {Sezoane}}", "components.RequestBlock.seasons": "{seasonCount, plural, un {Season} alte {Seasons}}",
"components.PermissionEdit.requestMoviesDescription": "Acordați permisiunea de a trimite solicitări pentru filme non-4K.", "components.PermissionEdit.requestMoviesDescription": "Acordați permisiunea de a trimite solicitări pentru filme non-4K.",
"components.PermissionEdit.viewissuesDescription": "Acordați permisiunea de a vizualiza problemele media raportate de alți utilizatori.", "components.PermissionEdit.viewissuesDescription": "Acordați permisiunea de a vizualiza problemele media raportate de alți utilizatori.",
"components.PermissionEdit.viewwatchlistsDescription": "Acordați permisiunea de a vizualiza listele de urmărire Plex ale altor utilizatori.", "components.PermissionEdit.viewwatchlistsDescription": "Acordați permisiunea de a vizualiza listele de urmărire Plex ale altor utilizatori.",
@@ -403,62 +403,40 @@
"components.RequestBlock.approve": "Aprobă Solicitarea", "components.RequestBlock.approve": "Aprobă Solicitarea",
"components.RequestBlock.decline": "Respinge Solicitarea", "components.RequestBlock.decline": "Respinge Solicitarea",
"components.RequestBlock.requestedby": "Solicitat de", "components.RequestBlock.requestedby": "Solicitat de",
"components.RequestButton.approve4krequests": "Aprobă {requestCount, plural, one {Cerere 4K} other {{requestCount} Cereri 4K}}", "components.RequestButton.approve4krequests": "Aprobă {requestCount, plural, o {4K Request} alte {{requestCount} 4K Requests}}",
"components.RequestBlock.lastmodifiedby": "Ultima Dată Modificat de", "components.RequestBlock.lastmodifiedby": "Ultima Dată Modificat de",
"components.RequestBlock.profilechanged": "Profil Calitate", "components.RequestBlock.profilechanged": "Profil Calitate",
"components.RequestBlock.requestdate": "Dată Solicitare", "components.RequestBlock.requestdate": "Dată Solicitare",
"components.RequestButton.approverequests": "Aprobă {requestCount, plural, one {Cerere} other {{requestCount} Cereri}}", "components.Layout.UserWarnings.emailRequired": "Este necesara o adresa de email.",
"components.RequestButton.requestmore4k": "Cere mai mult în 4K", "components.Layout.UserWarnings.passwordRequired": "Este necesara o parola.",
"components.RequestButton.approverequest4k": "Aproba Cereri 4K", "components.Login.credentialerror": "Numele de utilizator sau parola sunt incorecte.",
"components.RequestCard.tmdbid": "ID TMDB", "components.Login.emailtooltip": "Nu este necesar ca adresa ta sa fie asociata cu instanta ta {mediaServerName} .",
"components.RequestCard.failedretry": "A apărut o eroare la reîncercarea solicitării.", "components.Login.save": "Adauga",
"components.RequestButton.declinerequest": "Respinge Cerere", "components.Login.signinwithjellyfin": "Foloseste-ti contul de {mediaServerName}",
"components.RequestCard.seasons": "{seasonCount, plural, one {Sezon} other {Sezoane}}", "components.Login.title": "Adauga email",
"components.RequestCard.declinerequest": "Respinge Cererea", "components.Login.username": "Nume utilizator",
"components.RequestButton.viewrequest": "Vezi Cerere", "components.Login.validationEmailFormat": "Adresa email invalida",
"components.RequestButton.declinerequests": "Respinge {requestCount, plural, one {Cererea} other {{requestCount} Cererile}}", "components.Login.validationemailformat": "Necesar email valid",
"components.RequestCard.mediaerror": "{mediaType} Nu a fost găsit", "components.Login.validationhostformat": "Necesar URL valid",
"components.RequestCard.editrequest": "Editează Cererea", "components.Login.validationhostrequired": "{mediaServerName} URL necesar",
"components.RequestButton.viewrequest4k": "Vezi Cerere 4K", "components.Login.validationusernamerequired": "Nume utilizator necesar",
"components.RequestButton.decline4krequests": "Respinge {requestCount, plural, one {Cererea 4K} other {{requestCount} Cererile 4K}}", "components.MovieDetails.downloadstatus": "Status descarcare",
"components.RequestButton.declinerequest4k": "Respinge Cerere 4K", "components.MovieDetails.openradarr": "Deschide film in Radarr",
"components.RequestCard.approverequest": "Aprobă Cererea", "components.MovieDetails.openradarr4k": "Deschide film in 4K Radarr",
"components.RequestButton.approverequest": "Cereri Aprobate", "components.MovieDetails.play": "Ruleaza pe {mediaServerName}",
"components.RequestCard.deleterequest": "Șterge Cererea", "components.MovieDetails.play4k": "Ruleaza 4k pe {mediaServerName}",
"components.RequestCard.unknowntitle": "Titlu necunoscut", "components.RequestButton.declinerequest": "Refuza cerere",
"components.RequestList.RequestItem.cancelRequest": "Anulează Cerere", "components.RequestButton.declinerequest4k": "Refuza cerere 4k",
"components.RequestCard.tvdbid": "ID TheTVDB", "components.Layout.UserWarnings.emailInvalid": "Adresa de email este invalida.",
"components.RequestButton.requestmore": "Cere mai mult", "components.Login.description": "Deoarece este prima ta authentificare in {applicationName}, este necesara sa introduci o adresa de email valida.",
"components.RequestList.RequestItem.deleterequest": "Șterge Cerere", "components.Login.initialsigningin": "Se conecteaza…",
"components.RequestCard.cancelrequest": "Anulează Cererea", "components.Login.saving": "Se adauga…",
"components.RequestModal.AdvancedRequester.folder": "{path} ({space})", "components.RequestButton.approverequest": "Aproba cerere",
"components.RequestList.RequestItem.modified": "Modificat", "components.RequestButton.approverequest4k": "Aproba cerere 4k",
"components.RequestList.RequestItem.editrequest": "Editează Cererea", "components.Login.host": "{mediaServerName} URL",
"components.RequestModal.AdvancedRequester.qualityprofile": "Profil de Calitate", "components.Login.initialsignin": "Conecteaza-te",
"components.RequestList.requests": "Cereri", "components.ManageSlideOver.removearr4k": "Elimina din 4K {arr}",
"components.RequestModal.AdvancedRequester.advancedoptions": "Avansat", "components.ManageSlideOver.manageModalRemoveMediaWarning": "* Acesta va elimina ireversibil {mediaType} din {arr}, incluzand toate fisierele asociate.",
"components.RequestModal.AdvancedRequester.notagoptions": "Fără etichete.", "components.Login.validationEmailRequired": "Trebuie sa introduci o adresa email",
"components.RequestList.RequestItem.modifieduserdate": "{date} de {user}", "components.ManageSlideOver.removearr": "Elimina din {arr}"
"components.RequestModal.AdvancedRequester.requestas": "Cere ca",
"components.RequestList.showallrequests": "Afișează toate cererile",
"components.RequestList.RequestItem.tmdbid": "ID-ul TMDB",
"components.RequestList.RequestItem.requesteddate": "Solicitat",
"components.RequestModal.QuotaDisplay.movie": "film",
"components.RequestList.RequestItem.failedretry": "Ceva a mers greșit în timpul reîncercării cererii.",
"components.RequestList.RequestItem.unknowntitle": "Titlu Necunoscut",
"components.RequestModal.AdvancedRequester.destinationserver": "Server Destinație",
"components.RequestModal.AdvancedRequester.rootfolder": "Folder Rădăcină",
"components.RequestList.sortAdded": "Cele Mai Recente",
"components.RequestModal.AdvancedRequester.tags": "Etichete",
"components.RequestList.RequestItem.mediaerror": "{mediaType} nu a fost găsit",
"components.RequestList.sortModified": "Ultima Modificată",
"components.RequestList.RequestItem.tvdbid": "ID-ul TheTVDB",
"components.RequestModal.AdvancedRequester.selecttags": "Selectați Etichetele",
"components.RequestList.RequestItem.requested": "Solicitat",
"components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Nu sunt suficiente cereri de sezon rămase",
"components.RequestModal.AdvancedRequester.default": "{name} (Implicit)",
"components.RequestModal.AdvancedRequester.languageprofile": "Profil de Limbă",
"components.RequestModal.QuotaDisplay.allowedRequestsUser": "Acest utilizator are voie sa ceara <strong>{limit}</strong> {type} la fiecare <strong>{days}</strong> zile.",
"components.RequestList.RequestItem.seasons": "",
"components.RequestModal.QuotaDisplay.allowedRequests": "Aveți voie să cereți <strong>{limit}</strong> de {type} la fiecare <strong>{days}</strong> zile."
} }

View File

@@ -33,7 +33,7 @@
"components.RequestModal.cancel": "Отменить запрос", "components.RequestModal.cancel": "Отменить запрос",
"components.RequestModal.extras": "Дополнительно", "components.RequestModal.extras": "Дополнительно",
"components.RequestModal.numberofepisodes": "# эпизодов", "components.RequestModal.numberofepisodes": "# эпизодов",
"components.RequestModal.pendingrequest": "", "components.RequestModal.pendingrequest": "Ожидающий запрос",
"components.RequestModal.requestCancel": "Запрос на <strong>{title}</strong> отменён.", "components.RequestModal.requestCancel": "Запрос на <strong>{title}</strong> отменён.",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> успешно запрошен!", "components.RequestModal.requestSuccess": "<strong>{title}</strong> успешно запрошен!",
"components.RequestModal.requestadmin": "Этот запрос будет одобрен автоматически.", "components.RequestModal.requestadmin": "Этот запрос будет одобрен автоматически.",
@@ -170,7 +170,7 @@
"pages.oops": "Упс", "pages.oops": "Упс",
"pages.returnHome": "Вернуться домой", "pages.returnHome": "Вернуться домой",
"components.CollectionDetails.overview": "Обзор", "components.CollectionDetails.overview": "Обзор",
"components.CollectionDetails.numberofmovies": "{count} {count, plural, one {фильм} few {фильма} other {фильмов}}", "components.CollectionDetails.numberofmovies": "{count} фильмов",
"components.CollectionDetails.requestcollection": "Запросить Коллекцию", "components.CollectionDetails.requestcollection": "Запросить Коллекцию",
"components.Login.email": "Адрес электронной почты", "components.Login.email": "Адрес электронной почты",
"components.UserList.users": "Пользователи", "components.UserList.users": "Пользователи",
@@ -427,9 +427,9 @@
"components.Settings.RadarrModal.testFirstRootFolders": "Протестировать подключение для загрузки корневых каталогов", "components.Settings.RadarrModal.testFirstRootFolders": "Протестировать подключение для загрузки корневых каталогов",
"components.Settings.RadarrModal.loadingrootfolders": "Загрузка корневых каталогов…", "components.Settings.RadarrModal.loadingrootfolders": "Загрузка корневых каталогов…",
"components.RequestModal.AdvancedRequester.destinationserver": "Сервер-получатель", "components.RequestModal.AdvancedRequester.destinationserver": "Сервер-получатель",
"components.RequestList.RequestItem.mediaerror": "Название, связанное с этим запросом, больше недоступно.", "components.RequestList.RequestItem.mediaerror": "{mediaType} не найдено",
"components.RequestList.RequestItem.failedretry": "Что-то пошло не так при попытке повторить запрос.", "components.RequestList.RequestItem.failedretry": "Что-то пошло не так при попытке повторить запрос.",
"components.RequestCard.mediaerror": "Название, связанное с этим запросом, больше недоступно.", "components.RequestCard.mediaerror": "{mediaType} не найдено",
"components.RequestCard.failedretry": "Что-то пошло не так при попытке повторить запрос.", "components.RequestCard.failedretry": "Что-то пошло не так при попытке повторить запрос.",
"components.RequestButton.viewrequest4k": "Посмотреть 4К запрос", "components.RequestButton.viewrequest4k": "Посмотреть 4К запрос",
"components.RequestButton.requestmore4k": "Запросить больше в 4К", "components.RequestButton.requestmore4k": "Запросить больше в 4К",
@@ -570,7 +570,7 @@
"components.TvDetails.seasons": "{seasonCount, plural, one {# сезон} other {# сезонов}}", "components.TvDetails.seasons": "{seasonCount, plural, one {# сезон} other {# сезонов}}",
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Этому пользователю необходимо иметь по крайней мере <strong>{seasons}</strong> {seasons, plural, one {запрос на сезоны} other {запроса(ов) на сезоны}} для того, чтобы отправить запрос на этот сериал.", "components.RequestModal.QuotaDisplay.requiredquotaUser": "Этому пользователю необходимо иметь по крайней мере <strong>{seasons}</strong> {seasons, plural, one {запрос на сезоны} other {запроса(ов) на сезоны}} для того, чтобы отправить запрос на этот сериал.",
"components.RequestModal.QuotaDisplay.requiredquota": "Вам необходимо иметь по крайней мере <strong>{seasons}</strong> {seasons, plural, one {запрос на сезоны} other {запроса(ов) на сезоны}} для того, чтобы отправить запрос на этот сериал.", "components.RequestModal.QuotaDisplay.requiredquota": "Вам необходимо иметь по крайней мере <strong>{seasons}</strong> {seasons, plural, one {запрос на сезоны} other {запроса(ов) на сезоны}} для того, чтобы отправить запрос на этот сериал.",
"components.RequestModal.pending4krequest": "", "components.RequestModal.pending4krequest": "Ожидающий 4K запрос",
"components.RequestModal.autoapproval": "Автоматическое одобрение", "components.RequestModal.autoapproval": "Автоматическое одобрение",
"i18n.usersettings": "Настройки пользователя", "i18n.usersettings": "Настройки пользователя",
"i18n.showingresults": "Показываются результаты с <strong>{from}</strong> по <strong>{to}</strong> из <strong>{total}</strong>", "i18n.showingresults": "Показываются результаты с <strong>{from}</strong> по <strong>{to}</strong> из <strong>{total}</strong>",
@@ -790,7 +790,7 @@
"components.UserList.importfromplexerror": "Что-то пошло не так при импорте пользователей из Plex.", "components.UserList.importfromplexerror": "Что-то пошло не так при импорте пользователей из Plex.",
"components.UserList.importfrommediaserver": "Импортировать пользователей из {mediaServerName}", "components.UserList.importfrommediaserver": "Импортировать пользователей из {mediaServerName}",
"components.UserList.importfromplex": "Импортировать пользователей из Plex", "components.UserList.importfromplex": "Импортировать пользователей из Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из Plex!", "components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из Plex!",
"components.UserList.edituser": "Изменить разрешения пользователя", "components.UserList.edituser": "Изменить разрешения пользователя",
"components.UserList.displayName": "Отображаемое имя", "components.UserList.displayName": "Отображаемое имя",
"components.UserList.deleteconfirm": "Вы уверены, что хотите удалить этого пользователя? Все данные о его запросах будут удалены без возможности восстановления.", "components.UserList.deleteconfirm": "Вы уверены, что хотите удалить этого пользователя? Все данные о его запросах будут удалены без возможности восстановления.",
@@ -1018,7 +1018,7 @@
"components.Discover.CreateSlider.addcustomslider": "Создать слайдер", "components.Discover.CreateSlider.addcustomslider": "Создать слайдер",
"components.Discover.CreateSlider.nooptions": "Нет результатов.", "components.Discover.CreateSlider.nooptions": "Нет результатов.",
"components.Discover.CreateSlider.providetmdbgenreid": "Введите TMDB ID жанра", "components.Discover.CreateSlider.providetmdbgenreid": "Введите TMDB ID жанра",
"components.Discover.CreateSlider.needresults": "Должен быть хотя бы 1 результат.", "components.Discover.CreateSlider.needresults": "Должен быть хотя-бы 1 результат.",
"components.Discover.CreateSlider.providetmdbkeywordid": "Введите TMDB ID ключевого слова", "components.Discover.CreateSlider.providetmdbkeywordid": "Введите TMDB ID ключевого слова",
"components.Discover.CreateSlider.providetmdbnetwork": "Введите TMDB ID сети", "components.Discover.CreateSlider.providetmdbnetwork": "Введите TMDB ID сети",
"components.Discover.CreateSlider.providetmdbsearch": "Введите поисковой запрос", "components.Discover.CreateSlider.providetmdbsearch": "Введите поисковой запрос",
@@ -1027,7 +1027,7 @@
"components.Discover.CreateSlider.searchKeywords": "Поиск ключевых слов…", "components.Discover.CreateSlider.searchKeywords": "Поиск ключевых слов…",
"components.Discover.CreateSlider.searchStudios": "Поиск студий…", "components.Discover.CreateSlider.searchStudios": "Поиск студий…",
"components.Discover.CreateSlider.slidernameplaceholder": "Название слайдера", "components.Discover.CreateSlider.slidernameplaceholder": "Название слайдера",
"components.Discover.CreateSlider.starttyping": "Начните писать для поиска.", "components.Discover.CreateSlider.starttyping": "Начините писать для поиска.",
"components.Discover.CreateSlider.validationDatarequired": "Вы должны ввести дату.", "components.Discover.CreateSlider.validationDatarequired": "Вы должны ввести дату.",
"components.Discover.CreateSlider.validationTitlerequired": "Вы должны ввести заголовок.", "components.Discover.CreateSlider.validationTitlerequired": "Вы должны ввести заголовок.",
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Фильмы по ключевому слову \"{keywordTitle}\"", "components.Discover.DiscoverMovieKeyword.keywordMovies": "Фильмы по ключевому слову \"{keywordTitle}\"",
@@ -1059,7 +1059,7 @@
"components.Discover.DiscoverSliderEdit.deletesuccess": "Слайдер успешно удален.", "components.Discover.DiscoverSliderEdit.deletesuccess": "Слайдер успешно удален.",
"components.Discover.DiscoverSliderEdit.enable": "Изменить видимость", "components.Discover.DiscoverSliderEdit.enable": "Изменить видимость",
"components.Discover.DiscoverSliderEdit.remove": "Удалить", "components.Discover.DiscoverSliderEdit.remove": "Удалить",
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Активен фильтр} other {# Активные фильтры }}", "components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Активный фильтр} other {# Активные фильтры}}",
"components.Discover.DiscoverTv.discovertv": "Сериалы", "components.Discover.DiscoverTv.discovertv": "Сериалы",
"components.Discover.DiscoverTv.sortPopularityAsc": "Популярность по возрастанию", "components.Discover.DiscoverTv.sortPopularityAsc": "Популярность по возрастанию",
"components.Discover.DiscoverTv.sortPopularityDesc": "Популярность по убыванию", "components.Discover.DiscoverTv.sortPopularityDesc": "Популярность по убыванию",
@@ -1117,7 +1117,7 @@
"components.MovieDetails.digitalrelease": "Цифровой релиз", "components.MovieDetails.digitalrelease": "Цифровой релиз",
"components.MovieDetails.physicalrelease": "Физический релиз", "components.MovieDetails.physicalrelease": "Физический релиз",
"components.Settings.SettingsMain.toastSettingsFailure": "Что-то пошло не так при сохранении настроек.", "components.Settings.SettingsMain.toastSettingsFailure": "Что-то пошло не так при сохранении настроек.",
"components.Settings.SettingsMain.trustProxyTip": "Разрешить Overserr правильно регистрировать IP-адреса клиентов за прокси-сервером", "components.Settings.SettingsMain.trustProxyTip": "Разрешить Jellyseerr правильно регистрировать IP-адреса клиентов за прокси-сервером",
"components.Settings.experimentalTooltip": "Включение этого параметра может привести к неожиданному поведению приложения", "components.Settings.experimentalTooltip": "Включение этого параметра может привести к неожиданному поведению приложения",
"components.Settings.advancedTooltip": "Неправильная настройка этого параметра может привести к нарушению функциональности", "components.Settings.advancedTooltip": "Неправильная настройка этого параметра может привести к нарушению функциональности",
"components.Settings.externalUrl": "Внешний URL-адрес", "components.Settings.externalUrl": "Внешний URL-адрес",
@@ -1131,7 +1131,7 @@
"components.Settings.validationApiKey": "Вы должны предоставить ключ API", "components.Settings.validationApiKey": "Вы должны предоставить ключ API",
"components.TitleCard.mediaerror": "{mediaType} не найдено", "components.TitleCard.mediaerror": "{mediaType} не найдено",
"components.TitleCard.tmdbid": "TMDB ID", "components.TitleCard.tmdbid": "TMDB ID",
"components.Settings.restartrequiredTooltip": "Чтобы изменения этого параметра вступили в силу, необходимо перезапустить Overserr", "components.Settings.restartrequiredTooltip": "Чтобы изменения этого параметра вступили в силу, необходимо перезапустить Jellyseerr",
"components.ManageSlideOver.alltime": "Все время", "components.ManageSlideOver.alltime": "Все время",
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {просмотр} other {просмотров}}", "components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {просмотр} other {просмотров}}",
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Включить агент", "components.Settings.Notifications.NotificationsGotify.agentenabled": "Включить агент",
@@ -1214,13 +1214,13 @@
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Отправка тестового уведомления Gotify…", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Отправка тестового уведомления Gotify…",
"components.Settings.SettingsMain.cacheImages": "Включить кэширование изображений", "components.Settings.SettingsMain.cacheImages": "Включить кэширование изображений",
"components.Settings.SettingsMain.generalsettings": "Общие настройки", "components.Settings.SettingsMain.generalsettings": "Общие настройки",
"components.Settings.SettingsMain.generalsettingsDescription": "Настройте глобальные параметры и параметры по умолчанию для Overserr.", "components.Settings.SettingsMain.generalsettingsDescription": "Настройте глобальные параметры и параметры по умолчанию для Jellyseerr.",
"components.Settings.SettingsMain.hideAvailable": "Скрыть доступные медиа", "components.Settings.SettingsMain.hideAvailable": "Скрыть доступные медиа",
"components.Settings.SettingsMain.regionTip": "Фильтровать контент по региональной доступности", "components.Settings.SettingsMain.regionTip": "Фильтровать контент по региональной доступности",
"components.Settings.SettingsMain.toastApiKeyFailure": "Что-то пошло не так при создании нового ключа API.", "components.Settings.SettingsMain.toastApiKeyFailure": "Что-то пошло не так при создании нового ключа API.",
"components.Settings.SettingsMain.locale": "Язык приложения", "components.Settings.SettingsMain.locale": "Язык приложения",
"components.Settings.SettingsMain.originallanguage": "Регион поиска", "components.Settings.SettingsMain.originallanguage": "Регион поиска",
"components.Settings.tautulliSettingsDescription": "При желании настройте параметры для вашего сервера Tautulli. Overserr извлекает данные истории просмотров Plex из Tautulli.", "components.Settings.tautulliSettingsDescription": "При желании настройте параметры для вашего сервера Tautulli. Jellyseerr извлекает данные истории просмотров Plex из Tautulli.",
"components.Settings.toastTautulliSettingsFailure": "Что-то пошло не так при сохранении настроек Tautulli.", "components.Settings.toastTautulliSettingsFailure": "Что-то пошло не так при сохранении настроек Tautulli.",
"components.Settings.toastTautulliSettingsSuccess": "Настройки Tautulli успешно сохранены!", "components.Settings.toastTautulliSettingsSuccess": "Настройки Tautulli успешно сохранены!",
"components.Settings.validationUrlBaseTrailingSlash": "База URL не должна заканчиваться косой чертой", "components.Settings.validationUrlBaseTrailingSlash": "База URL не должна заканчиваться косой чертой",
@@ -1241,7 +1241,7 @@
"components.Settings.Notifications.NotificationsGotify.validationTypes": "Вы должны выбрать как минимум один тип уведомления", "components.Settings.Notifications.NotificationsGotify.validationTypes": "Вы должны выбрать как минимум один тип уведомления",
"components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "Вы должны указать действующий URL", "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "Вы должны указать действующий URL",
"components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "URL не должен заканчиваться слешем", "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "URL не должен заканчиваться слешем",
"components.Settings.SettingsJobsCache.imagecacheDescription": "Если включено, Overserr будет проксировать и кэшировать изображения из предварительно настроенных внешних источников. Кэшированные изображения сохраняются в папку конфигурации. Вы можете найти файлы в <code>{appDataPath}/cache/images</code>.", "components.Settings.SettingsJobsCache.imagecacheDescription": "Если включено, Jellyseerr будет проксировать и кэшировать изображения из предварительно настроенных внешних источников. Кэшированные изображения сохраняются в папку конфигурации. Вы можете найти файлы в <code>{appDataPath}/cache/images</code>.",
"components.Settings.tautulliSettings": "Настройки Tautulli", "components.Settings.tautulliSettings": "Настройки Tautulli",
"components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}", "components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}",
"components.Discover.customizediscover": "Настроить Обнаружение", "components.Discover.customizediscover": "Настроить Обнаружение",
@@ -1257,21 +1257,92 @@
"components.Selector.showmore": "Показать больше", "components.Selector.showmore": "Показать больше",
"components.Settings.SettingsJobsCache.imagecachesize": "Размер кэша", "components.Settings.SettingsJobsCache.imagecachesize": "Размер кэша",
"components.Settings.validationUrlBaseLeadingSlash": "Базовый URL должен начинаться с косой черты", "components.Settings.validationUrlBaseLeadingSlash": "Базовый URL должен начинаться с косой черты",
"components.Discover.FilterSlideover.tmdbuservotecount": "Количество голосов пользователей TMDB", "components.Layout.UserWarnings.emailRequired": "Требуется указать email адрес.",
"components.Discover.FilterSlideover.voteCount": "Количество голосов между {minValue} и {maxValue}", "components.Layout.UserWarnings.passwordRequired": "Требуется указать пароль.",
"components.Discover.tmdbmoviestreamingservices": "Стриминговые сервисы фильмов TMDB", "components.Login.emailtooltip": "Адрес не обязательно должен быть связан с вашим {mediaServerName} сервером.",
"components.Discover.tmdbtvstreamingservices": "Стриминговые сервисы сериалов TMDB", "components.Login.initialsignin": "Подключиться",
"components.Settings.RadarrModal.tagRequestsInfo": "Автоматически добавлять дополнительный тег с ID и именем запросившего пользователя", "components.Login.initialsigningin": "Подключение…",
"components.Settings.RadarrModal.tagRequests": "Теги запросов", "components.Login.save": "Добавить",
"components.Login.saving": "Добавление…",
"components.Login.signinwithjellyfin": "Используйте свой {mediaServerName} аккаунт",
"components.Login.host": "{mediaServerName} URL",
"components.Login.username": "Имя пользователя",
"components.Login.validationEmailFormat": "Неверный email",
"components.Login.validationemailformat": "Необходим корректный email",
"components.Login.validationhostformat": "Необходим корректный URL",
"components.Login.validationhostrequired": "Необходим {mediaServerName} URL",
"components.Login.validationusernamerequired": "Необходимо имя пользователя",
"components.ManageSlideOver.removearr": "Удалить из {arr}",
"components.ManageSlideOver.removearr4k": "Удалить из 4К {arr}",
"components.MovieDetails.imdbuserscore": "Оценка пользователей IMDB", "components.MovieDetails.imdbuserscore": "Оценка пользователей IMDB",
"components.Settings.SettingsJobsCache.availability-sync": "Синхронизация доступности медиа", "components.MovieDetails.openradarr": "Открыть фильм в Radarr",
"components.Settings.SonarrModal.tagRequests": "Теги запросов", "components.MovieDetails.play": "Запустить на {mediaServerName}",
"components.Settings.SonarrModal.tagRequestsInfo": "Автоматически добавлять тег с ID и именем запросившего пользователя", "components.MovieDetails.play4k": "Запустить 4К на {mediaServerName}",
"i18n.collection": "Коллекция", "components.Settings.RadarrModal.tagRequests": "Тег запросов",
"components.Settings.Notifications.NotificationsPushover.sound": "Звук уведомления", "components.Layout.UserWarnings.emailInvalid": "Неверный email адрес.",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Устройство по умолчанию",
"components.Settings.SonarrModal.animeSeriesType": "Тип аниме-сериала",
"components.Settings.SonarrModal.seriesType": "Тип сериала", "components.Settings.SonarrModal.seriesType": "Тип сериала",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Звук уведомления", "components.Discover.FilterSlideover.voteCount": "Количество голосов от {minValue} до {maxValue}",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Устройство по умолчанию" "components.Login.validationEmailRequired": "Вы должны указать адрес электронной почты",
"components.Settings.Notifications.userEmailRequired": "Требуется email пользователя",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Сканировать недавно добавленное в Jellyfin",
"components.Discover.tmdbmoviestreamingservices": "Сервисы потоковой передачи фильмов TMDB",
"components.Discover.tmdbtvstreamingservices": "Сервисы потоковой передачи сериалов TMDB",
"components.Login.description": "Поскольку вы впервые входите в систему {ApplicationName}, вам необходимо добавить адрес электронной почты.",
"components.Settings.Notifications.NotificationsPushover.sound": "Звук уведомлений",
"components.Settings.RadarrModal.tagRequestsInfo": "Автодобавление тега с именем и ID пользователя, отправившего запрос",
"components.Settings.SonarrModal.animeSeriesType": "Тип аниме",
"components.Discover.FilterSlideover.tmdbuservotecount": "Количество голосов от пользователей TMDB",
"components.Login.credentialerror": "Введено неверное имя пользователя или пароль.",
"components.Login.title": "Добавить email",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Это приведет к необратимому удалению {MediaType} из {arr}, включая все файлы.",
"components.Settings.SettingsAbout.supportjellyseerr": "Поддержать Jellyseerr",
"components.Settings.SonarrModal.tagRequests": "Тег запросов",
"components.MovieDetails.downloadstatus": "Статус загрузки",
"components.MovieDetails.openradarr4k": "Открыть фильм в 4К Radarr",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Устройство по умолчанию",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Сканировать всю библиотеку Jellyfin",
"components.Settings.SettingsJobsCache.availability-sync": "Синхронизировать доступность медиа",
"components.Settings.jellyfinSettingsFailure": "Что-то пошло не так во время сохранения настроек {mediaServerName}.",
"components.Settings.jellyfinSettingsSuccess": "Настройки {mediaServerName} успешно сохранены!",
"components.Settings.jellyfinlibraries": "Библиотеки {mediaServerName}",
"components.Settings.jellyfinlibrariesDescription": "Библиотеки {mediaServerName} проверяются на наличие заголовков. Нажмите кнопку ниже, если в списке не хватает библиотек.",
"components.Settings.jellyfinsettings": "Настройки {mediaServerName}",
"components.Settings.internalUrl": "Внутренний URL-адрес",
"components.Settings.SonarrModal.tagRequestsInfo": "Автодобавление тега с именем и ID пользователя, отправившего запрос",
"components.Settings.jellyfinSettings": "Настройки {mediaServerName}",
"components.Settings.jellyfinSettingsDescription": "Необязательно настраивать внутреннюю и внешнюю конечные точки для вашего сервера {mediaServerName}. В большинстве случаев внешний URL-адрес отличается от внутреннего. Пользовательский URL-адрес для сброса пароля также может быть задан для входа в систему {mediaServerName}, на случай, если вы хотите перенаправить на другую страницу для сброса пароля.",
"components.Settings.jellyfinsettingsDescription": "Настройте свой {mediaServerName} сервер. {mediaServerName} отсканирует ваши библиотеки, чтобы увидеть, какие библиотеки доступны.",
"components.Settings.manualscanJellyfin": "Сканировать библиотеки вручную",
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
"components.Settings.syncJellyfin": "Синхронизировать библиотеки",
"components.Settings.syncing": "Синхронизация",
"components.Settings.timeout": "Время ожидания",
"components.Setup.signin": "Войти",
"components.Setup.signinWithPlex": "Используйте свой Plex аккаунт",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> успешно удален из списка наблюдения!",
"components.TitleCard.watchlistError": "Что-то пошло не так, попробуйте еще раз.",
"components.TvDetails.play": "Запустить в {mediaServerName}",
"components.TvDetails.play4k": "Запустить 4К в {mediaServerName}",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из {mediaServerName}!",
"components.UserList.importfromJellyfin": "Добавить пользователей из {mediaServerName}",
"components.UserList.noJellyfinuserstoimport": "Нет пользователей {mediaServerName} для импорта.",
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Пользователь {mediaServerName}",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Сохранение…",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Звук уведомлений",
"i18n.collection": "Коллекция",
"components.Setup.configuremediaserver": "Настройте медиасервер",
"components.UserList.importfromJellyfinerror": "Что-то пошло не так при импорте пользователей из {mediaServerName}.",
"components.UserList.newJellyfinsigninenabled": "Параметр <strong>Включить новый вход в {mediaServerName}</strong> в настоящее время включен. Пользователей {mediaServerName} с доступом к библиотеке не нужно импортировать для входа.",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Электронная почта",
"components.UserProfile.localWatchlist": "Список наблюдения {username}",
"components.Settings.manualscanDescriptionJellyfin": "Обычно это выполняется только раз в 24 часа. Jellyseerr будет более настойчиво проверять недавно добавленный сервер {mediaServerName}. Если вы впервые настраиваете Jellyseerr, то рекомендуем однократное полное сканирование библиотеки!",
"components.TitleCard.watchlistCancel": "наблюдение за <strong>{title}</strong> отменено.",
"components.Settings.saving": "Сохранение…",
"components.TitleCard.addToWatchList": "Добавить в список наблюдения",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> успешно добавлен в список наблюдения!",
"components.Settings.save": "Сохранить изменения",
"components.Setup.signinWithJellyfin": "Используйте свой {mediaServerName} аккаунт",
"components.UserList.mediaServerUser": "Пользователь {mediaServerName}",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Сохранить изменения",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Устройство по умолчанию"
} }

59
src/i18n/locale/sl.json Normal file
View File

@@ -0,0 +1,59 @@
{
"components.Discover.CreateSlider.editsuccess": "Urejen drsnik in shranjene nastavitve prilagajanja odkrivanja.",
"components.CollectionDetails.numberofmovies": "{count} film/ov",
"components.Discover.CreateSlider.slidernameplaceholder": "Ime drsnika",
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Premiera ↓",
"components.AppDataWarning.dockerVolumeMissingDescription": "Pripenjanje nosilca <code>{appDataPath}</code> ni bilo pravilno konfigurirano. Vsi podatki bodo izbrisani, ko se vsebnik zaustavi ali znova zažene.",
"components.Discover.DiscoverMovies.sortPopularityDesc": "Priljubljenost ↑",
"components.AirDateBadge.airsrelative": "Predvajanje {relativeTime}",
"components.CollectionDetails.overview": "Pregled",
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Active Filter} drugo {# Active Filters}}",
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Ocena TMDB ↓",
"components.AirDateBadge.airedrelative": "Predvajano {relativeTime}",
"components.Discover.CreateSlider.searchStudios": "Iskanje studiev …",
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Datum izdaje ↑",
"components.Discover.CreateSlider.providetmdbnetwork": "Navedite ID omrežja TMDB",
"components.Discover.CreateSlider.addfail": "Novega drsnika ni bilo mogoče ustvariti.",
"components.CollectionDetails.requestcollection": "Zahtevaj zbirko",
"components.Discover.DiscoverMovieGenre.genreMovies": "Filmi: {genre}",
"components.Discover.DiscoverMovieLanguage.languageMovies": "Filmi: {language}",
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.DiscoverMovies.sortPopularityAsc": "Priljubljenost ↓",
"components.Discover.CreateSlider.needresults": "Imeti morate vsaj 1 rezultat.",
"components.Discover.CreateSlider.addcustomslider": "Ustvari drsnik po meri",
"components.Discover.DiscoverTv.sortPopularityAsc": "Priljubljenost ↓",
"components.Discover.CreateSlider.editSlider": "Uredi drsnik",
"components.Discover.DiscoverTv.sortTitleAsc": "Naslov (a-ž) ↓",
"components.Discover.CreateSlider.validationDatarequired": "Navesti morate vrednost podatkov.",
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Premiera ↑",
"components.Discover.DiscoverTv.discovertv": "Serije",
"components.Discover.DiscoverSliderEdit.deletefail": "Drsnika ni bilo mogoče izbrisati.",
"components.Discover.CreateSlider.providetmdbstudio": "Navedite ID studia v TMDB",
"components.Discover.DiscoverMovies.sortTitleDesc": "Naslov (a-ž) ↑",
"components.Discover.DiscoverStudio.studioMovies": "{studio} filmi",
"components.Discover.DiscoverTv.sortPopularityDesc": "Priljubljenost ↑",
"components.Discover.CreateSlider.searchGenres": "Išči žanre …",
"components.Discover.CreateSlider.editfail": "Drsnika ni bilo mogoče urediti.",
"components.Discover.CreateSlider.starttyping": "Tipkajte za iskanje.",
"components.Discover.DiscoverSliderEdit.enable": "Preklopi vidnost",
"components.Discover.CreateSlider.addSlider": "Dodaj drsnik",
"components.CollectionDetails.requestcollection4k": "Zahtevaj zbirko 4K",
"components.Discover.CreateSlider.providetmdbsearch": "Vnesite iskalno poizvedbo",
"components.Discover.DiscoverNetwork.networkSeries": "{network} serije",
"components.Discover.CreateSlider.providetmdbkeywordid": "Navedite ID ključne besede TMDB",
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Filmi: {keywordTitle}",
"components.Discover.CreateSlider.validationTitlerequired": "Navesti morate naslov.",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Datum izdaje ↓",
"components.Discover.CreateSlider.nooptions": "Ni zadetkov.",
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Ocena TMDB ↑",
"components.Discover.CreateSlider.searchKeywords": "Iskanje po ključnih besedah …",
"components.Discover.CreateSlider.addsuccess": "Ustvarjen nov drsnik in shranjene nastavitve prilagajanja odkrivanja.",
"components.Discover.DiscoverSliderEdit.deletesuccess": "Drsnik je bil uspešno izbrisan.",
"components.Discover.DiscoverMovies.discovermovies": "Filmi",
"components.Discover.DiscoverMovies.sortTitleAsc": "Naslov (a-ž) ↓",
"components.Discover.CreateSlider.providetmdbgenreid": "Navedite ID žanra TMDB",
"components.Discover.DiscoverTv.sortTitleDesc": "Naslov (a-ž) ↑",
"components.Discover.DiscoverSliderEdit.remove": "Odstrani",
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB ↓",
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series"
}

File diff suppressed because it is too large Load Diff

1308
src/i18n/locale/tr.json Normal file

File diff suppressed because it is too large Load Diff

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