Compare commits

...

139 Commits

Author SHA1 Message Date
semantic-release-bot
299f65c597 chore(release): 1.3.0 2023-01-02 00:14:11 +00:00
Fallenbagel
6021d1e336 Merge pull request #285 from Fallenbagel/prepare-for-next-version
Merge origin/develop to prepare for next release
2023-01-02 05:11:37 +05:00
Fallenbagel
3ce1ef350e docs: merge CHANGELOG.md of develop with main to fix format issue 2023-01-02 05:01:13 +05:00
Fallenbagel
06c91744f3 Merge branch 'develop' 2023-01-02 04:44:05 +05:00
Fallenbagel
d18e3d185f Merge pull request #277 from Fallenbagel/updatereadme
docs: update current features and add emphasis on the pre-requisites
2022-12-17 17:40:31 +05:00
Fallenbagel
e222463a63 docs: update current features and add emphasis on the pre-requisites [skip ci] 2022-12-17 06:02:06 +05:00
Fallenbagel
03b9bda287 Merge pull request #276 from Fallenbagel/fix-issue-#254
fix(ui): adds mediaServerName to statusBadge and manageSlideOver
2022-12-17 05:38:21 +05:00
Fallenbagel
7e20c7cb78 fix(locale): fix the duplicated wording in the Clear Media Warning message
Fixes the duplicated wording in the clear media warning message of manageSlideOver that was
introduced in previous commit
2022-12-17 05:10:11 +05:00
Fallenbagel
d0cdce9e90 fix(ui): adds mediaServerName to statusBadge and manageSlideOver
Adds mediaServerName to statusBadge and manageSlideOver to indicate the type of mediaServer that is
connected to jellyseerr

fix #254
2022-12-17 05:02:47 +05:00
Fallenbagel
113b09bf2b Merge pull request #275 from Fallenbagel/support-mixed-libraries
feat(api): adds support for Mixed Libraries
2022-12-17 04:27:46 +05:00
Fallenbagel
b16f192b92 Merge pull request #274 from Fallenbagel/pr269
Merge upstream "origin/develop"
2022-12-16 22:26:21 +05:00
Fallenbagel
d9ca3c6e52 fix(api): ignore Music,Books,Photos,MusicVideo libraries
Ignores libraries other than tvshows,movies,others
2022-12-16 20:19:03 +05:00
Fallenbagel
ba82ecec5c feat(api): adds support for Mixed Libraries
Adds support for mixed libraries with movies and show types

fix #95
2022-12-16 16:23:32 +05:00
Fallenbagel
c052a2455c Merge remote-tracking branch 'origin/develop' into pr269 2022-12-16 13:01:07 +05:00
Fallenbagel
2d99a8b03c fix character length of summary for snaps 2022-12-16 12:34:59 +05:00
Fallenbagel
7434c0cf2f fix formatting snapcraft 2022-12-16 12:22:22 +05:00
notfakie
afcb096f49 Merge remote-tracking branch 'overseerr/develop' into develop 2022-12-16 19:58:33 +13:00
Fallenbagel
9dc11cedbf Merge pull request #272 from Fallenbagel/prepare-snap-builds [skip ci]
Prepare snap builds [skip ci]
2022-12-16 03:01:31 +05:00
Fallenbagel
22aab783d4 Prepare snap builds [skip ci] 2022-12-16 02:41:05 +05:00
Fallenbagel
a2babb83ad Merge pull request #263 from darmiel/fix/combined-episodes
fix: count combined episodes
2022-12-06 17:35:02 +05:00
allcontributors[bot]
76a7ceb758 docs: add s0up4200 as a contributor for doc (#3153) [skip ci]
* docs: update README.md

* docs: update .all-contributorsrc

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-12-06 12:49:19 +04:00
soup
9688acaa87 chore(docs): fix typo in fail2ban article (#3139) [skip ci] 2022-12-06 12:35:09 +04:00
darmiel
64339e5f03 fix: count combined episodes
Jellyfin allows combined episodes, like `S01E01-E02`,
but seasons containing such episodes are only recognized
as `Partially Available`. This commit should fix that.
2022-12-04 00:36:01 +01:00
Fallenbagel
1ceea3dcca Merge pull request #262 from Fallenbagel/change-cypress-projectid
test(cypress): change cypress projectId
2022-12-03 17:10:42 +05:00
Fallenbagel
e3c3283603 test(cypress): change cypress projectId
Change cypress projectId to jellyseerr's project id
2022-12-03 17:07:36 +05:00
Fallenbagel
4ac02d3aac docs(readme): fixed the formatting of README.md
Fixed the formatting of README.md that was causing issues with the formatting check workflow
2022-12-03 16:15:14 +05:00
Fallenbagel
8eacfe045f Add information about unsupported types
Added information about unsupported libraries and automatic grouping
2022-11-24 06:06:35 +05:00
Ryan Cohen
15e246929b fix(api): handle auth for accounts where the plex id may have been set to null (#3125)
also made some changes to hopefully alleviate this issue from happening at all in the future
2022-11-20 19:07:32 +09:00
Fallenbagel
c1424634fb fix the git clone url 2022-11-02 03:31:47 +05:00
Brandon Cohen
07ec3efbca fix: improved PTR scrolling performance (#3095) 2022-11-01 14:24:10 +09:00
Fallenbagel
9b07b10901 style(readme): fix formatting of README.md 2022-10-26 08:43:08 +05:00
Fallenbagel
b1e9cdbea2 add in minimum version needed for nodejs 2022-10-25 03:53:50 +05:00
Fallenbagel
9aee630392 update instructions to include steps for stable version 2022-10-25 03:47:08 +05:00
Fallenbagel
6b50f77624 update windows instructions
Replaced `yarn` with `npm` in the installation of `win-node-env`
2022-10-25 03:45:07 +05:00
Fallenbagel
16f1c286c4 Add detailed native installation instructions 2022-10-25 03:42:46 +05:00
TheCatLady
64aab6dd82 feat(lang): add Croatian display language (#3041) 2022-10-19 00:40:03 +00:00
allcontributors[bot]
144bb84bdc docs: add Eclipseop as a contributor for code (#3087) [skip ci]
* docs: update README.md

* docs: update .all-contributorsrc

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-10-19 09:29:07 +09:00
Mackenzie
76260f9b22 build: update semantic-release to use proper arg for git sha (#3075) 2022-10-18 23:18:48 +00:00
Ryan Cohen
500cd1f872 feat: custom image proxy (#3056) 2022-10-18 14:40:24 +09:00
semantic-release-bot
9252817b58 chore(release): 1.2.1 2022-10-18 02:24:18 +00:00
Fallenbagel
a66925067d Merge pull request #242 from Fallenbagel/develop
Merge origin/develop
2022-10-18 07:21:19 +05:00
Fallenbagel
d037d178aa Merge pull request #240 from sambartik/revert-230-fix-jellyfin-emby-links
Fix jellyfin external url basepath being ignored
2022-10-16 04:24:25 +05:00
Fallenbagel
ab09664d41 fix(backend): fix jellyfinHost to not be undefined
Fix jellyfinHost so its not being treated as null or undefined

fix #237
2022-10-16 03:50:22 +05:00
Brandon Cohen
bfe56c3470 fix: added deep links to issues and status badges (#3065) 2022-10-15 05:39:33 +00:00
TheCatLady
1dfa9431a9 fix: update API docs to allow 'all' seasons value (#3073) 2022-10-15 08:31:50 +09:00
Samuel Bartík
0faae20bac Fix jellyfin external url basepath being ignored 2022-10-13 23:24:09 +02:00
Samuel Bartík
5b10da4073 Revert "fix(backend): fixes Jellyfin/Emby links if server is initially setup with a trailing /" 2022-10-13 23:08:20 +02:00
semantic-release-bot
6049edffca chore(release): 1.2.0 2022-10-12 13:34:54 +00:00
Fallenbagel
f27200c8c1 Merge pull request #235 from Fallenbagel/prepare-for-next-version
Merge origin/develop
2022-10-12 18:27:14 +05:00
Fallenbagel
613ebb95d2 Merge origin/develop 2022-10-12 00:15:50 +05:00
Fallenbagel
15c79e03a5 Merge pull request #234 from Fallenbagel/merge-upstream
Merge upstream develop
2022-10-12 00:07:57 +05:00
Fallenbagel
ed95b0af25 Merge upstream develop 2022-10-11 23:13:31 +05:00
Danshil Kokil Mungur
f5c2fc1c20 fix(ui): minor fixes (#3036)
* fix(ui): hide available media on person page

* fix(ui): set correct label for image cache settings

* fix(ui): disable status badge tooltip for collections

* fix(ui): replace empty space when no episodes in season

* fix: suggested changes

* fix(jobs): set watchlist sync to short interval

* chore: run i18n:extract

* fix: suggested changes
2022-10-04 12:03:24 +09:00
Fallenbagel
3ba69f9a74 Merge pull request #230 from Fallenbagel/fix-jellyfin-emby-links
fix(backend): fixes Jellyfin/Emby links if server is initially setup with a trailing /
2022-09-29 08:34:26 +05:00
Fallenbagel
66357019f0 fix(backend): fixes Jellyfin/Emby links if server is initially setup with a trailing /
Fixes #168 and #220
2022-09-26 10:17:18 +05:00
Brandon Cohen
21d20fdfd6 fix: sidebar close button placement when using PWA (#3045) 2022-09-23 17:54:17 +09:00
JonnyWong16
cf96db90ad docs(proxy): update sub_filter for subfolder (#3046) [skip ci]
The updated `next.js` dependency has a new regex match in the code for `/^\/_next\/data\//`.

This `sub_filter` creates invalid regex `/^\/overseerr/_next\/data\//`
```nginx
    sub_filter '/_next' '/$app/_next';
```
It needs to be updated to substitute the correct the regex `/^\/overseerr\/_next\/data\//`
```nginx
    sub_filter '\/_next' '\/$app\/_next';
    sub_filter '/_next' '/$app/_next';
```
2022-09-22 18:16:08 +00:00
Ryan Cohen
430b1ab871 fix: remove backdrop-blur class from warning buttons (#3037) 2022-09-20 18:18:25 +09:00
Danshil Kokil Mungur
7404d68143 fix(ui): hide null dates in episodes list (#3035) 2022-09-19 16:11:16 +00:00
Weblate (bot)
16cb53f703 feat(lang): translations update from Hosted Weblate (#3026)
* feat(lang): translated using Weblate (Swedish)

Currently translated at 91.8% (1028 of 1119 strings)

Co-authored-by: Johan Ruda <johan.ruda@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 89.7% (1004 of 1119 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nandor Rusz <nandor.rusz@vodafone.de>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1119 of 1119 strings)

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

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

Currently translated at 23.3% (261 of 1119 strings)

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

Co-authored-by: Johan Ruda <johan.ruda@gmail.com>
Co-authored-by: Nandor Rusz <nandor.rusz@vodafone.de>
Co-authored-by: Kobe <kobaubarr@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2022-09-18 21:07:58 -07:00
Brandon Cohen
407af32d32 fix: new status indicators added to series list on mobile (#3024)
* fix: new status indicators added to series list

* refactor: component will render icons and has updated props
2022-09-17 14:52:51 +09:00
aedelbro
5c01313cc4 fix(ui): remove 'all' badge from request cards (#2992)
if all seasons are requested for a TV show, show each indivdual season badge. This prevents the
admin from needing to open a second tab / navigate to see how many seasons / what seasons have been
requested.
2022-09-16 21:27:45 +00:00
Ryan Cohen
d8da5cbe9d fix(plex): add container-size header to recently added api call (#3023) 2022-09-16 02:01:29 +00:00
Weblate (bot)
3d458dd2fd feat(lang): translations update from Hosted Weblate (#3014)
* feat(lang): translated using Weblate (Catalan)

Currently translated at 100.0% (1119 of 1119 strings)

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

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

Currently translated at 95.6% (1070 of 1119 strings)

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

Currently translated at 95.6% (1070 of 1119 strings)

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

Currently translated at 92.3% (1033 of 1119 strings)

Co-authored-by: Eric <hamburger1024@mailbox.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Linyue-GitHub <592746995@qq.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/
Translation: Overseerr/Overseerr Frontend

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

Currently translated at 100.0% (1119 of 1119 strings)

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

Currently translated at 100.0% (1119 of 1119 strings)

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

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

Currently translated at 100.0% (1119 of 1119 strings)

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

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

Currently translated at 23.3% (261 of 1119 strings)

feat(lang): translated using Weblate (Croatian)

Currently translated at 18.1% (203 of 1119 strings)

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

Co-authored-by: Maite Guix <maite.guix@gmail.com>
Co-authored-by: Eric <hamburger1024@mailbox.org>
Co-authored-by: Linyue-GitHub <592746995@qq.com>
Co-authored-by: TheCatLady <o40yoym9@anonaddy.me>
Co-authored-by: exentler <gurandsrud@gmail.com>
Co-authored-by: lpispek <lpispek@gmail.com>
2022-09-13 23:27:24 +00:00
Brandon Cohen
e486623310 fix: compatibility issue with safari (#3019) 2022-09-14 08:16:55 +09:00
Fallenbagel
8feb20ff52 Merge pull request #157 from Fallenbagel/staging-for-1.1.1
chore(release): 1.1.1
2022-06-20 20:15:44 +05:00
Fallenbagel
f2c659c6f3 chore(release): 1.1.1 2022-06-20 20:11:05 +05:00
semantic-release-bot
99f1a4e4f3 chore(release): 1.2.0 2022-06-20 14:29:16 +00:00
Fallenbagel
fea9457dad Merge pull request #156 from Fallenbagel/develop
Merge branch 'develop' to fix the release workflow
2022-06-20 19:27:42 +05:00
Fallenbagel
23c9595933 Merge pull request #153 from Fallenbagel/develop
Merge branch 'develop'
2022-06-20 18:30:20 +05:00
semantic-release-bot
eceedbbaad chore(release): 1.1.0 2022-05-21 01:58:03 +00:00
Fallenbagel
29f06a965c Merge branch 'develop' 2022-05-21 06:43:52 +05:00
Fallenbagel
9ec05d3ba4 Fixed the link for the jellyseerr logo 2022-04-24 13:22:47 +05:00
semantic-release-bot
ee14ff5a51 chore(release): 1.0.2 2022-04-20 00:06:57 +00:00
Fallenbagel
6b62d4b862 Merge pull request #82 from Fallenbagel/workFlowfix
ci: adds GITHUB_TOKEN as an env
2022-04-20 05:02:17 +05:00
Fallenbagel
706fea0e97 ci: adds GITHUB_TOKEN as an env
adds GITHUB_TOKEN as an env to fix the github_token missing error during release workflow
2022-04-20 05:00:29 +05:00
Fallenbagel
80956d1a83 Merge pull request #81 from Fallenbagel/fixMediaServerType
fix: fix usertype from local user to mediaServerType
2022-04-20 04:58:20 +05:00
Fallenbagel
6d530d9028 fix: fix usertype from local user to mediaServerType
Fixes usertype from appearing as local user even if the mediaServerType is jellyfin
2022-04-20 04:52:39 +05:00
Fallenbagel
f12237565f Merge pull request #80 from Fallenbagel/packagejsonChanges
update tags and the branch to jellyseerr
2022-04-20 03:49:16 +05:00
Fallenbagel
11f5594ed4 update tags and the branch to jellyseerr 2022-04-20 03:47:46 +05:00
Fallenbagel
e4e58bee05 Merge pull request #79 from Fallenbagel/githubChanges
update workflows and discord locations for jellyseerr
2022-04-20 03:32:46 +05:00
Fallenbagel
13ee3a836c update workflows and discord locations for jellyseerr 2022-04-20 03:29:19 +05:00
Fallenbagel
3f16a353f5 Merge pull request #78 from Fallenbagel/urlValidationFix
fix: relax jellyfin url validation to allow local domains
2022-04-20 03:25:41 +05:00
Fallenbagel
9c43ba95e6 fix: relax jellyfin url validation to allow local domains
Relaxes jellyfin url validation so that http://localhost:8096 and http://jellyfin:8096 urls are
accepted in addition to full urls like https://example.com

fix #123
2022-04-20 03:12:01 +05:00
Fallenbagel
13fb6fd1a7 Updated the docker tags
Updated the docker tags to point to fallenbagel docker repo
2022-04-18 07:27:21 +05:00
Fallenbagel
16e8e3a38e update workflow to test for jellyseerr
update workflow and discord locations to test the docker pipeline
2022-04-18 07:17:16 +05:00
Fallenbagel
6fecdf094d Merge pull request #76 from Fallenbagel/updatePackagejson
Update package.json to reflect the jellyseerr version. This helps fix the version issue.
2022-04-15 14:57:27 +05:00
Fallenbagel
69b271b018 Chore(release):v1.0.1 2022-04-15 14:55:53 +05:00
Fallenbagel
d6ebd9a9b9 Chore(release):v1.0.1 2022-04-15 14:54:30 +05:00
Fallenbagel
70dad332fc Merge pull request #74 from Fallenbagel/versionStatusFix
fix: fix for the jellyseerr out of date even though it is up-to-date
2022-04-15 14:30:41 +05:00
Fallenbagel
a65e430c60 fix: fix for the jellyseerr out of date even though it is up-to-date
Reverting back the changes for the quick jellyseerr version fix for a better implementation
2022-04-15 14:03:03 +05:00
Fallenbagel
18f4b67b72 Merge pull request #73 from Fallenbagel/avatarfix
fix: fix default avatar missing
2022-04-15 12:12:13 +05:00
Fallenbagel
506c31562a fix: fix default avatar missing
Fix the default avatar missing because one of the os_logo_square.png file was missing
2022-04-15 12:07:12 +05:00
Fallenbagel
7a9d7a4834 Merge pull request #71 from jsl9208/feat-emby-mediaurl
feat: add emby detail url support
2022-04-15 11:21:49 +05:00
Fallenbagel
902a033b8a Merge pull request #72 from Fallenbagel/unknownjobfix
fix: replaced Unkown job with jellyfin in jobsandcache
2022-04-15 11:18:46 +05:00
Fallenbagel
00eb20aa5e fix: replaced Unkown job with jellyfin in jobsandcache
Replaced unknown job with jellyfin in jobsandcache and fixed the translations to reflect it as well
2022-04-15 10:46:09 +05:00
Shilong Jiang
a2c27cfa95 feat: add emby detail url support 2022-04-14 20:10:57 +08:00
Fallenbagel
7122b4d08b Replaced arm tags with latest
Replaced `:arm` and `:armv7` tags with `latest` as they are now deprecated.
2022-04-14 00:03:57 +05:00
Fallenbagel
b03b9b1dbb fix: fixed request card not displaying the requested season and episodes
When requested, the request card shows as {seasonCount, plural, one {Season}} and does not display
which season or episode was requested because it was still using the alpha request cards. This fixed
that issue
2022-04-13 17:24:47 +05:00
Fallenbagel
73672e29f8 fix: fixed jellyseerr out of date on stable version
When jellyseerr latest version or the stable version was deployed, the version was shown as out of
date with a message to up date to the latest version even though it was the latest version. This
fixed that issue
2022-04-13 17:21:03 +05:00
Fallenbagel
cc5192209f fixed logo_full.svg render 2022-04-13 13:17:54 +05:00
Fallenbagel
278dcf4b44 Update .all-contributorsrc 2022-04-13 13:17:54 +05:00
Fallenbagel
36e092f225 Update .all-contributorsrc 2022-04-13 13:17:54 +05:00
Fallenbagel
46d5c737a2 chore: github update 2022-04-13 13:17:54 +05:00
Fallenbagel
cba4878db3 feat: update zh_Hans.json
Update zh_Hans.json
2022-04-13 13:17:53 +05:00
Fallenbagel
57cc48a699 style: replaced Overseerr with jellyseerr 2022-04-13 13:17:53 +05:00
Fallenbagel
84f488be06 fix: database migration fix
Fixed the database migration issue fixing the error "SQLITE+ERROR: no such column:
User.jellyfinUsername
2022-04-13 13:17:53 +05:00
Fallenbagel
f885f2a0f3 ci: remove DEPENDABOT 2022-04-13 13:17:53 +05:00
Fallenbagel
eef3e5ea4c docs: added preview 2022-04-13 13:17:53 +05:00
Fallenbagel
8db821c1c1 docs: added new logo
Added new jellyseerr logo
2022-04-13 13:17:53 +05:00
Fallenbagel
a39b882f09 docs: added new logo
Added new jellyseerr logo
2022-04-13 13:17:53 +05:00
Fallenbagel
754dccc4bf first commit 2022-04-13 13:17:53 +05:00
Juan D. Jara
f97ee11430 fix(jellyfin): get jellyfin integration working with the last develop version
re #288
2021-09-27 02:56:02 +02:00
Juan D. Jara
54868fd486 style: fix linter and add types 2021-09-27 02:35:10 +02:00
Juan D. Jara
eea389879f Merge branch 'develop' of https://github.com/sct/overseerr into jellyfin-support 2021-09-27 02:24:30 +02:00
Aiden Vigue
5c917f95b4 fix(backend): use different device ids for jellyfin users 2021-06-17 13:42:08 -04:00
Aiden Vigue
dd4d42fd31 fix(backend): force same device id 2021-06-14 16:33:17 -04:00
Aiden Vigue
e5c6b9cd74 fix(backend): update jellyfin.ts for 10.8.0 2021-06-14 12:27:07 -04:00
Aiden Vigue
508fccae4e fix(build): fix build errors 2021-02-27 22:41:35 +00:00
Aiden Vigue
f77573c838 fix(frontend): revert mpaa change 2021-02-27 22:17:51 +00:00
Aiden Vigue
7dfe38001e fix(backend): fix Jellyfin scan for recently added items 2021-02-27 22:15:32 +00:00
Aiden Vigue
48f55da43e style(frontend): fix padding on MPAA rating 2021-02-27 22:15:32 +00:00
Aiden Vigue
1e97503802 fix(db): add migration 2021-02-27 22:13:53 +00:00
Aiden Vigue
42ff34bb3d fix(backend): remove console statement 2021-02-27 22:13:53 +00:00
Aiden Vigue
107b766c44 fix(frontend): add Jellyfin logo to ExternalLinkBlock 2021-02-27 22:13:23 +00:00
Aiden Vigue
fb51ce5570 feat(rebase): rebase 2021-02-27 22:12:55 +00:00
Aiden Vigue
3357343d98 feat(rebase): rebase 2021-02-27 22:12:54 +00:00
Aiden Vigue
9d61092f37 feat(rebase): rebase 2021-02-27 22:12:54 +00:00
Aiden Vigue
29274614c3 feat(rebase): rebase 2021-02-27 22:12:54 +00:00
Aiden Vigue
19b51592ea feat(rebase): rebase 2021-02-27 22:11:47 +00:00
Aiden Vigue
757c0fc29e feat(rebase): rebase 2021-02-27 22:11:27 +00:00
Aiden Vigue
3eb48abc14 feat(rebase): rebase 2021-02-27 22:11:27 +00:00
Aiden Vigue
01cd9d3872 feat(rebase): rebasse 2021-02-27 22:10:25 +00:00
Aiden Vigue
9582196e1f feat: rebase 2021-02-27 22:09:43 +00:00
Aiden Vigue
3743edab8d feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
d81e7cdbab feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
6e1d7f7075 feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
91cf2de33a feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
a6ec2d5220 feat(all): add initial Jellyfin/Emby support 2021-02-27 22:09:02 +00:00
77 changed files with 1573 additions and 360 deletions

View File

@@ -737,6 +737,24 @@
"contributions": [
"translation"
]
},
{
"login": "Eclipseop",
"name": "Mackenzie",
"avatar_url": "https://avatars.githubusercontent.com/u/5846213?v=4",
"profile": "https://github.com/Eclipseop",
"contributions": [
"code"
]
},
{
"login": "s0up4200",
"name": "soup",
"avatar_url": "https://avatars.githubusercontent.com/u/18177310?v=4",
"profile": "https://github.com/s0up4200",
"contributions": [
"doc"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
@@ -745,5 +763,6 @@
"projectOwner": "sct",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": false
"skipCi": false,
"commitConvention": "angular"
}

7
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,7 @@
# Global code ownership
- @Fallenbagel
# i18n locale files
src/i18n/locale/ @Fallenbagel

View File

@@ -76,7 +76,7 @@ jobs:
- name: Upload Snap Package
uses: actions/upload-artifact@v2
with:
name: overseerr-snap-package-${{ matrix.architecture }}
name: jellyseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1

View File

@@ -49,7 +49,7 @@ jobs:
- name: Upload Snap Package
uses: actions/upload-artifact@v3
with:
name: overseerr-snap-package-${{ matrix.architecture }}
name: jellyseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1

3
.gitignore vendored
View File

@@ -67,3 +67,6 @@ tsconfig.tsbuildinfo
# Webstorm
.idea
# Config Cache Directory
config/cache

View File

@@ -1,3 +1,24 @@
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
### Bug Fixes
* added deep links to issues and status badges ([#3065](https://github.com/fallenbagel/jellyseerr/issues/3065)) ([bfe56c3](https://github.com/fallenbagel/jellyseerr/commit/bfe56c347073001795b1c3e917eb7a5afcc4462c))
* **api:** handle auth for accounts where the plex id may have been set to null ([#3125](https://github.com/fallenbagel/jellyseerr/issues/3125)) ([15e2469](https://github.com/fallenbagel/jellyseerr/commit/15e246929bdbc2b7b5bdab7a84bd7882b79d5cb1))
* **api:** ignore Music,Books,Photos,MusicVideo libraries ([d9ca3c6](https://github.com/fallenbagel/jellyseerr/commit/d9ca3c6e52c118698ca71021217f6ca409e71974))
* count combined episodes ([64339e5](https://github.com/fallenbagel/jellyseerr/commit/64339e5f0374f8490e685e5c086e088bb7fd737e))
* improved PTR scrolling performance ([#3095](https://github.com/fallenbagel/jellyseerr/issues/3095)) ([07ec3ef](https://github.com/fallenbagel/jellyseerr/commit/07ec3efbcaf669de7ccde4421c1112bfd23675d6))
* **locale:** fix the duplicated wording in the Clear Media Warning message ([7e20c7c](https://github.com/fallenbagel/jellyseerr/commit/7e20c7cb78a44c32ab8a5f21203e285f23f402ab))
* **ui:** adds mediaServerName to statusBadge and manageSlideOver ([d0cdce9](https://github.com/fallenbagel/jellyseerr/commit/d0cdce9e90fba642d2bf934a4266e1421424bc73)), closes [#254](https://github.com/fallenbagel/jellyseerr/issues/254)
* update API docs to allow 'all' seasons value ([#3073](https://github.com/fallenbagel/jellyseerr/issues/3073)) ([1dfa943](https://github.com/fallenbagel/jellyseerr/commit/1dfa9431a95e7e2a1843746c2473d8a06f03e184))
### Features
* **api:** adds support for Mixed Libraries ([ba82ece](https://github.com/fallenbagel/jellyseerr/commit/ba82ecec5c994e79d7c9b658372041522b58a120)), closes [#95](https://github.com/fallenbagel/jellyseerr/issues/95)
* custom image proxy ([#3056](https://github.com/fallenbagel/jellyseerr/issues/3056)) ([500cd1f](https://github.com/fallenbagel/jellyseerr/commit/500cd1f872942923d2b9c3b835e6329e335d4a3f))
* **lang:** add Croatian display language ([#3041](https://github.com/fallenbagel/jellyseerr/issues/3041)) ([64aab6d](https://github.com/fallenbagel/jellyseerr/commit/64aab6dd8240e191026512733b34cc046b6e508a))
## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06)
### Bug Fixes

View File

@@ -13,37 +13,105 @@ _The original Overseerr team have been busy and Jellyfin/Emby support aren't on
## Current Features
- Jellyfin Support
- Emby Support
(Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!)
Along with all the existing Overseerr features:
- Full Plex integration. Authenticate and manage user access with Plex!
- Full Jellyfin/Emby/Plex integration. Authenticate and manage user access with Jellyfin/Emby/Plex!
- Supports Movies, Shows, Mixed Libraries!
- Ability to change email addresses for smtp purposes
- Ability to import all jellyfin/emby users
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
- Plex library scan, to keep track of the titles which are already available.
- Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available.
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
- Granular permission system.
- Support for various notification agents.
- Mobile-friendly design, for when you need to approve requests on the go!
(Upcoming Features include: Multiple Server Instances, Music Support, and much more!)
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
## Getting Started
#### Pre-requisite (Important)
_*On Jellyfin/Emby, ensure the `settings > Home > Automatically group content from the following folders into views such as 'Movies', 'Music' and 'TV'` is turned off*_
### Launching Jellyseerr using Docker
Check out our dockerhub for instructions on how to install and run Jellyseerr:
https://hub.docker.com/r/fallenbagel/jellyseerr
### Launching Jellyseerr manually:
#### Windows
Pre-requisites:
- Nodejs (atleast LTS version)
- Yarn
- Download the source code from the github (Either develop branch or main for stable)
```bash
npm i -g win-node-env
yarn install
yarn run build
yarn start
```
#### Linux
Pre-requisites:
- Nodejs (atleast LTS version)
- Yarn
- Git
```bash
git clone https://github.com/Fallenbagel/jellyseerr.git && cd jellyseerr
git checkout main #if you want to run stable instead of develop
yarn install
yarn run build
yarn start
```
_Systemd-service:_
- assuming jellyseerr was cloned to `/opt/`
and the environmentfile is located at `/etc/jellyseerr`
service:
```
[Unit]
Description=Jellyseerr Service
Wants=network-online.target
After=network-online.target
[Service]
EnvironmentFile=/etc/jellyseerr/jellyseerr.conf
Environment=NODE_ENV=production
Type=exec
Restart=on-failure
WorkingDirectory=/opt/jellyseerr
ExecStart=/root/.nvm/versions/node/v18.7.0/bin/node dist/index.js
[Install]
WantedBy=multi-user.target
```
Environmentfile:
```
# Jellyseerr's default port is 5055, if you want to use both, change this.
# specify on which port to listen
PORT=5055
# specify on which interface to listen, by default jellyseerr listens on all interfaces
#HOST=127.0.0.1
# Uncomment if your media server is emby instead of jellyfin.
# JELLYFIN_TYPE=emby
```
### Packages:
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'cypress';
export default defineConfig({
projectId: 'onnqy3',
projectId: 'xkm1b4',
e2e: {
baseUrl: 'http://localhost:5055',
experimentalSessionAndOrigin: true,

View File

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

View File

@@ -138,6 +138,7 @@ location ^~ /overseerr {
sub_filter 'href="/"' 'href="/$app"';
sub_filter 'href="/login"' 'href="/$app/login"';
sub_filter 'href:"/"' 'href:"/$app"';
sub_filter '\/_next' '\/$app\/_next';
sub_filter '/_next' '/$app/_next';
sub_filter '/api/v1' '/$app/api/v1';
sub_filter '/login/plex/loading' '/$app/login/plex/loading';

View File

@@ -40,6 +40,14 @@ If you enable this setting and find yourself unable to access Overseerr, you can
This setting is **disabled** by default.
### Enable Image Caching
When enabled, Overseerr will proxy and cache images from pre-configured sources (such as TMDB). This can use a significant amount of disk space.
Images are saved in the `config/cache/images` and stale images are cleared out every 24 hours.
You should enable this if you are having issues with loading images directly from TMDB in your browser.
### Display Language
Set the default display language for Overseerr. Users can override this setting in their user settings.

View File

@@ -2667,29 +2667,44 @@ paths:
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
example: cache-id
name:
type: string
example: cache name
stats:
type: object
properties:
imageCache:
type: object
properties:
tmdb:
type: object
properties:
size:
type: number
example: 123456
imageCount:
type: number
example: 123
apiCaches:
type: array
items:
type: object
properties:
hits:
type: number
misses:
type: number
keys:
type: number
ksize:
type: number
vsize:
type: number
id:
type: string
example: cache-id
name:
type: string
example: cache name
stats:
type: object
properties:
hits:
type: number
misses:
type: number
keys:
type: number
ksize:
type: number
vsize:
type: number
/settings/cache/{cacheId}/flush:
post:
summary: Flush a specific cache
@@ -4838,9 +4853,13 @@ paths:
type: number
example: 123
seasons:
type: array
items:
type: number
oneOf:
- type: array
items:
type: number
minimum: 1
- type: string
enum: [all]
is4k:
type: boolean
example: false
@@ -4919,7 +4938,7 @@ paths:
$ref: '#/components/schemas/MediaRequest'
put:
summary: Update MediaRequest
description: Updates a specific media request and returns the request in a JSON object.. Requires the `MANAGE_REQUESTS` permission.
description: Updates a specific media request and returns the request in a JSON object. Requires the `MANAGE_REQUESTS` permission.
tags:
- request
parameters:
@@ -4930,6 +4949,37 @@ paths:
example: '1'
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
mediaType:
type: string
enum: [movie, tv]
seasons:
type: array
items:
type: number
minimum: 1
is4k:
type: boolean
example: false
serverId:
type: number
profileId:
type: number
rootFolder:
type: string
languageProfileId:
type: number
userId:
type: number
nullable: true
required:
- mediaType
responses:
'200':
description: Succesfully updated request

View File

@@ -1,6 +1,6 @@
{
"name": "jellyseerr",
"version": "0.1.0",
"version": "1.3.0",
"private": true,
"scripts": {
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
@@ -225,7 +225,7 @@
{
"path": "semantic-release-docker-buildx",
"buildArgs": {
"COMMIT_TAG": "$GITHUB_SHA"
"COMMIT_TAG": "$GIT_SHA"
},
"imageNames": [
"fallenbagel/jellyseerr"

View File

@@ -0,0 +1,3 @@
[ZoneTransfer]
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
ZoneId=3

View File

@@ -38,6 +38,7 @@ export interface JellyfinLibraryItem {
SeasonId?: string;
SeasonName?: string;
IndexNumber?: number;
IndexNumberEnd?: number;
ParentIndexNumber?: number;
MediaType: string;
}
@@ -178,8 +179,10 @@ class JellyfinAPI {
(Item: any) => {
return (
Item.Type === 'CollectionFolder' &&
(Item.CollectionType === 'tvshows' ||
Item.CollectionType === 'movies')
Item.CollectionType !== 'music' &&
Item.CollectionType !== 'books' &&
Item.CollectionType !== 'musicvideos' &&
Item.CollectionType !== 'homevideos'
);
}
).map((Item: any) => {
@@ -204,7 +207,7 @@ class JellyfinAPI {
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try {
const contents = await this.axios.get<any>(
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}`
);
return contents.data.Items.filter(

View File

@@ -232,6 +232,10 @@ class PlexAPI {
uri: `/library/sections/${id}/all?sort=addedAt%3Adesc&addedAt>>=${Math.floor(
options.addedAt / 1000
)}`,
extraHeaders: {
'X-Plex-Container-Start': `0`,
'X-Plex-Container-Size': `500`,
},
});
return response.MediaContainer.Metadata;

View File

@@ -191,7 +191,7 @@ export interface TmdbVideo {
export interface TmdbTvEpisodeResult {
id: number;
air_date: string;
air_date: string | null;
episode_number: number;
name: string;
overview: string;
@@ -372,7 +372,8 @@ export interface TmdbPersonCombinedCredits {
crew: TmdbPersonCreditCrew[];
}
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
export interface TmdbSeasonWithEpisodes
extends Omit<TmdbTvSeasonResult, 'episode_count'> {
episodes: TmdbTvEpisodeResult[];
external_ids: TmdbExternalIds;
}

View File

@@ -200,15 +200,20 @@ class Media {
const pageName =
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
const { serverId, hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
let jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
jellyfinHost = jellyfinHost.endsWith('/')
? jellyfinHost.slice(0, -1)
: jellyfinHost;
if (this.jellyfinMediaId) {
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
}
if (this.jellyfinMediaId4k) {
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
}
}
}

View File

@@ -39,7 +39,7 @@ export class User {
return users.map((u) => u.filter(showFiltered));
}
static readonly filteredFields: string[] = ['email'];
static readonly filteredFields: string[] = ['email', 'plexId'];
public displayName: string;
@@ -76,7 +76,7 @@ export class User {
@Column({ type: 'integer', default: UserType.PLEX })
public userType: UserType;
@Column({ nullable: true })
@Column({ nullable: true, select: true })
public plexId?: number;
@Column({ nullable: true })

View File

@@ -17,6 +17,7 @@ import WebPushAgent from '@server/lib/notifications/agents/webpush';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import routes from '@server/routes';
import imageproxy from '@server/routes/imageproxy';
import { getAppVersion } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
@@ -186,6 +187,9 @@ app
next();
});
server.use('/api/v1', routes);
server.use('/imageproxy', imageproxy);
server.get('*', (req, res) => handle(req, res));
server.use(
(

View File

@@ -54,6 +54,11 @@ export interface CacheItem {
};
}
export interface CacheResponse {
apiCaches: CacheItem[];
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
}
export interface StatusResponse {
version: string;
commitTag: string;

View File

@@ -257,8 +257,19 @@ class JobJellyfinSync {
//use for loop to make sure this loop _completes_ in full
//before the next section
for (const episode of episodes) {
let episodeCount = 1;
// count number of combined episodes
if (
episode.IndexNumber !== undefined &&
episode.IndexNumberEnd !== undefined
) {
episodeCount =
episode.IndexNumberEnd - episode.IndexNumber + 1;
}
if (!this.enable4kShow) {
totalStandard++;
totalStandard += episodeCount;
} else {
const ExtendedEpisodeData = await this.jfClient.getItemData(
episode.Id
@@ -268,10 +279,10 @@ class JobJellyfinSync {
return MediaSource.MediaStreams.some((MediaStream) => {
if (MediaStream.Type === 'Video') {
if (MediaStream.Width ?? 0 < 2000) {
totalStandard++;
totalStandard += episodeCount;
}
} else {
total4k++;
total4k += episodeCount;
}
});
});

View File

@@ -1,5 +1,6 @@
import { MediaServerType } from '@server/constants/server';
import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy';
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
import { radarrScanner } from '@server/lib/scanners/radarr';
import { sonarrScanner } from '@server/lib/scanners/sonarr';
@@ -111,7 +112,7 @@ export const startJobs = (): void => {
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
interval: 'long',
interval: 'short',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
@@ -181,5 +182,21 @@ export const startJobs = (): void => {
}),
});
// Run image cache cleanup every 5 minutes
scheduledJobs.push({
id: 'image-cache-cleanup',
name: 'Image Cache Cleanup',
type: 'process',
interval: 'long',
cronSchedule: jobs['image-cache-cleanup'].schedule,
job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => {
logger.info('Starting scheduled job: Image Cache Cleanup', {
label: 'Jobs',
});
// Clean TMDB image cache
ImageProxy.clearCache('tmdb');
}),
});
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
};

268
server/lib/imageproxy.ts Normal file
View File

@@ -0,0 +1,268 @@
import logger from '@server/logger';
import axios from 'axios';
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit';
import { createHash } from 'crypto';
import { promises } from 'fs';
import path, { join } from 'path';
type ImageResponse = {
meta: {
revalidateAfter: number;
curRevalidate: number;
isStale: boolean;
etag: string;
extension: string;
cacheKey: string;
cacheMiss: boolean;
};
imageBuffer: Buffer;
};
class ImageProxy {
public static async clearCache(key: string) {
let deletedImages = 0;
const cacheDirectory = path.join(
__dirname,
'../../config/cache/images/',
key
);
const files = await promises.readdir(cacheDirectory);
for (const file of files) {
const filePath = path.join(cacheDirectory, file);
const stat = await promises.lstat(filePath);
if (stat.isDirectory()) {
const imageFiles = await promises.readdir(filePath);
for (const imageFile of imageFiles) {
const [, expireAtSt] = imageFile.split('.');
const expireAt = Number(expireAtSt);
const now = Date.now();
if (now > expireAt) {
await promises.rm(path.join(filePath, imageFile));
deletedImages += 1;
}
}
}
}
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, {
label: 'Image Cache',
});
}
public static async getImageStats(
key: string
): Promise<{ size: number; imageCount: number }> {
const cacheDirectory = path.join(
__dirname,
'../../config/cache/images/',
key
);
const imageTotalSize = await ImageProxy.getDirectorySize(cacheDirectory);
const imageCount = await ImageProxy.getImageCount(cacheDirectory);
return {
size: imageTotalSize,
imageCount,
};
}
private static async getDirectorySize(dir: string): Promise<number> {
const files = await promises.readdir(dir, {
withFileTypes: true,
});
const paths = files.map(async (file) => {
const path = join(dir, file.name);
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
if (file.isFile()) {
const { size } = await promises.stat(path);
return size;
}
return 0;
});
return (await Promise.all(paths))
.flat(Infinity)
.reduce((i, size) => i + size, 0);
}
private static async getImageCount(dir: string) {
const files = await promises.readdir(dir);
return files.length;
}
private axios;
private cacheVersion;
private key;
constructor(
key: string,
baseUrl: string,
options: {
cacheVersion?: number;
rateLimitOptions?: rateLimitOptions;
} = {}
) {
this.cacheVersion = options.cacheVersion ?? 1;
this.key = key;
this.axios = axios.create({
baseURL: baseUrl,
});
if (options.rateLimitOptions) {
this.axios = rateLimit(this.axios, options.rateLimitOptions);
}
}
public async getImage(path: string): Promise<ImageResponse> {
const cacheKey = this.getCacheKey(path);
const imageResponse = await this.get(cacheKey);
if (!imageResponse) {
const newImage = await this.set(path, cacheKey);
if (!newImage) {
throw new Error('Failed to load image');
}
return newImage;
}
// If the image is stale, we will revalidate it in the background.
if (imageResponse.meta.isStale) {
this.set(path, cacheKey);
}
return imageResponse;
}
private async get(cacheKey: string): Promise<ImageResponse | null> {
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const files = await promises.readdir(directory);
const now = Date.now();
for (const file of files) {
const [maxAgeSt, expireAtSt, etag, extension] = file.split('.');
const buffer = await promises.readFile(join(directory, file));
const expireAt = Number(expireAtSt);
const maxAge = Number(maxAgeSt);
return {
meta: {
curRevalidate: maxAge,
revalidateAfter: maxAge * 1000 + now,
isStale: now > expireAt,
etag,
extension,
cacheKey,
cacheMiss: false,
},
imageBuffer: buffer,
};
}
} catch (e) {
// No files. Treat as empty cache.
}
return null;
}
private async set(
path: string,
cacheKey: string
): Promise<ImageResponse | null> {
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const response = await this.axios.get(path, {
responseType: 'arraybuffer',
});
const buffer = Buffer.from(response.data, 'binary');
const extension = path.split('.').pop() ?? '';
const maxAge = Number(response.headers['cache-control'].split('=')[1]);
const expireAt = Date.now() + maxAge * 1000;
const etag = response.headers.etag.replace(/"/g, '');
await this.writeToCacheDir(
directory,
extension,
maxAge,
expireAt,
buffer,
etag
);
return {
meta: {
curRevalidate: maxAge,
revalidateAfter: expireAt,
isStale: false,
etag,
extension,
cacheKey,
cacheMiss: true,
},
imageBuffer: buffer,
};
} catch (e) {
logger.debug('Something went wrong caching image.', {
label: 'Image Cache',
errorMessage: e.message,
});
return null;
}
}
private async writeToCacheDir(
dir: string,
extension: string,
maxAge: number,
expireAt: number,
buffer: Buffer,
etag: string
) {
const filename = join(dir, `${maxAge}.${expireAt}.${etag}.${extension}`);
await promises.rm(dir, { force: true, recursive: true }).catch(() => {
// do nothing
});
await promises.mkdir(dir, { recursive: true });
await promises.writeFile(filename, buffer);
}
private getCacheKey(path: string) {
return this.getHash([this.key, this.cacheVersion, path]);
}
private getHash(items: (string | number | Buffer)[]) {
const hash = createHash('sha256');
for (const item of items) {
if (typeof item === 'number') hash.update(String(item));
else {
hash.update(item);
}
}
// See https://en.wikipedia.org/wiki/Base64#Filenames
return hash.digest('base64').replace(/\//g, '-');
}
private getCacheDirectory() {
return path.join(__dirname, '../../config/cache/images/', this.key);
}
}
export default ImageProxy;

View File

@@ -38,7 +38,7 @@ export interface PlexSettings {
export interface JellyfinSettings {
name: string;
hostname?: string;
hostname: string;
externalHostname?: string;
libraries: Library[];
serverId: string;
@@ -263,7 +263,8 @@ export type JobId =
| 'download-sync'
| 'download-sync-reset'
| 'jellyfin-recently-added-sync'
| 'jellyfin-full-sync';
| 'jellyfin-full-sync'
| 'image-cache-cleanup';
interface AllSettings {
clientId: string;
@@ -446,6 +447,9 @@ class Settings {
'jellyfin-full-sync': {
schedule: '0 0 3 * * *',
},
'image-cache-cleanup': {
schedule: '0 0 5 * * *',
},
},
};
if (initialSettings) {

View File

@@ -29,7 +29,7 @@ import type { Video } from './Movie';
interface Episode {
id: number;
name: string;
airDate: string;
airDate: string | null;
episodeNumber: number;
overview: string;
productionCode: string;
@@ -50,7 +50,7 @@ interface Season {
seasonNumber: number;
}
export interface SeasonWithEpisodes extends Season {
export interface SeasonWithEpisodes extends Omit<Season, 'episodeCount'> {
episodes: Episode[];
externalIds: ExternalIds;
}
@@ -141,7 +141,6 @@ export const mapSeasonWithEpisodes = (
season: TmdbSeasonWithEpisodes
): SeasonWithEpisodes => ({
airDate: season.air_date,
episodeCount: season.episode_count,
episodes: season.episodes.map(mapEpisodeResult),
externalIds: mapExternalIds(season.external_ids),
id: season.id,

View File

@@ -89,13 +89,28 @@ authRoutes.post('/plex', async (req, res, next) => {
await userRepository.save(user);
} else {
const mainUser = await userRepository.findOneOrFail({
select: { id: true, plexToken: true, plexId: true },
select: { id: true, plexToken: true, plexId: true, email: true },
where: { id: 1 },
});
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
if (!account.id) {
logger.error('Plex ID was missing from Plex.tv response', {
label: 'API',
ip: req.ip,
email: account.email,
plexUsername: account.username,
});
return next({
status: 500,
message: 'Something went wrong. Try again.',
});
}
if (
account.id === mainUser.plexId ||
(account.email === mainUser.email && !mainUser.plexId) ||
(await mainPlexTv.checkUserAccess(account.id))
) {
if (user) {
@@ -226,7 +241,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
const hostname =
settings.jellyfin.hostname !== ''
? settings.jellyfin.hostname
: body.hostname;
: body.hostname ?? '';
const { externalHostname } = getSettings().jellyfin;
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
@@ -244,11 +259,15 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
}
// First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
const jellyfinHost =
let jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
jellyfinHost = jellyfinHost.endsWith('/')
? jellyfinHost.slice(0, -1)
: jellyfinHost;
const account = await jellyfinserver.login(body.username, body.password);
// Next let's see if the user already exists
user = await userRepository.findOne({

View File

@@ -0,0 +1,39 @@
import ImageProxy from '@server/lib/imageproxy';
import logger from '@server/logger';
import { Router } from 'express';
const router = Router();
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
/**
* Image Proxy
*/
router.get('/*', async (req, res) => {
const imagePath = req.path.replace('/image', '');
try {
const imageData = await tmdbImageProxy.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 image', {
imagePath,
errorMessage: e.message,
});
res.status(500).send();
}
});
export default router;

View File

@@ -1,5 +1,7 @@
import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus } from '@server/constants/media';
import Media from '@server/entity/Media';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import {
mapCastCredits,
@@ -34,6 +36,7 @@ personRoutes.get('/:id', async (req, res, next) => {
personRoutes.get('/:id/combined_credits', async (req, res, next) => {
const tmdb = new TheMovieDb();
const settings = getSettings();
try {
const combinedCredits = await tmdb.getPersonCombinedCredits({
@@ -41,14 +44,30 @@ personRoutes.get('/:id/combined_credits', async (req, res, next) => {
language: req.locale ?? (req.query.language as string),
});
const castMedia = await Media.getRelatedMedia(
let castMedia = await Media.getRelatedMedia(
combinedCredits.cast.map((result) => result.id)
);
const crewMedia = await Media.getRelatedMedia(
let crewMedia = await Media.getRelatedMedia(
combinedCredits.crew.map((result) => result.id)
);
if (settings.main.hideAvailable) {
castMedia = castMedia.filter(
(media) =>
(media.mediaType === 'movie' || media.mediaType === 'tv') &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.PARTIALLY_AVAILABLE
);
crewMedia = crewMedia.filter(
(media) =>
(media.mediaType === 'movie' || media.mediaType === 'tv') &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
return res.status(200).json({
cast: combinedCredits.cast
.map((result) =>

View File

@@ -16,9 +16,10 @@ import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
import { scheduledJobs } from '@server/job/schedule';
import type { AvailableCacheIds } from '@server/lib/cache';
import cacheManager from '@server/lib/cache';
import ImageProxy from '@server/lib/imageproxy';
import { Permission } from '@server/lib/permissions';
import { plexFullScanner } from '@server/lib/scanners/plex';
import type { Library, MainSettings } from '@server/lib/settings';
import type { JobId, Library, MainSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
@@ -307,11 +308,14 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
settingsRoutes.get('/jellyfin/users', async (req, res) => {
const settings = getSettings();
const { hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
let jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
jellyfinHost = jellyfinHost.endsWith('/')
? jellyfinHost.slice(0, -1)
: jellyfinHost;
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
@@ -601,7 +605,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
});
});
settingsRoutes.post<{ jobId: string }>(
settingsRoutes.post<{ jobId: JobId }>(
'/jobs/:jobId/cancel',
(req, res, next) => {
const scheduledJob = scheduledJobs.find(
@@ -628,7 +632,7 @@ settingsRoutes.post<{ jobId: string }>(
}
);
settingsRoutes.post<{ jobId: string }>(
settingsRoutes.post<{ jobId: JobId }>(
'/jobs/:jobId/schedule',
(req, res, next) => {
const scheduledJob = scheduledJobs.find(
@@ -663,16 +667,23 @@ settingsRoutes.post<{ jobId: string }>(
}
);
settingsRoutes.get('/cache', (req, res) => {
const caches = cacheManager.getAllCaches();
settingsRoutes.get('/cache', async (_req, res) => {
const cacheManagerCaches = cacheManager.getAllCaches();
return res.status(200).json(
Object.values(caches).map((cache) => ({
id: cache.id,
name: cache.name,
stats: cache.getStats(),
}))
);
const apiCaches = Object.values(cacheManagerCaches).map((cache) => ({
id: cache.id,
name: cache.name,
stats: cache.getStats(),
}));
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
return res.status(200).json({
apiCaches,
imageCache: {
tmdb: tmdbImageCache,
},
});
});
settingsRoutes.post<{ cacheId: AvailableCacheIds }>(

View File

@@ -497,11 +497,14 @@ router.post(
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
const createdUsers: User[] = [];
const { hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
let jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
jellyfinHost = jellyfinHost.endsWith('/')
? jellyfinHost.slice(0, -1)
: jellyfinHost;
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const jellyfinUsers = await jellyfinClient.getUsers();

View File

@@ -1,10 +1,11 @@
name: overseerr
adopt-info: overseerr
name: jellyseerr
adopt-info: jellyseerr
license: MIT
summary: Request management and media discovery tool for the Plex ecosystem.
summary: Request management and media discovery tool for media servers
description: >
Overseerr is a free and open source software application for managing requests for your media library.
It integrates with your existing services such as Sonarr, Radarr and Plex!
Jellyseerr is a free and open source software application for managing requests for your media library.
It is a a fork of Overseerr built to bring support for & focusing mainly on Jellyfin & Emby media servers!
It integrates with your existing services such as Sonarr, Radarr, and Jellyfin/Emby/Plex.
base: core18
confinement: strict
@@ -14,7 +15,7 @@ architectures:
- build-on: armhf
parts:
overseerr:
jellyseerr:
plugin: nodejs
nodejs-version: '16.17.0'
nodejs-package-manager: 'yarn'
@@ -36,7 +37,7 @@ parts:
override-pull: |
snapcraftctl pull
# Get information to determine snap grade and version
git config --global --add safe.directory /data/parts/overseerr/src
git config --global --add safe.directory /data/parts/jellyyseerr/src
#setup yarn.rc
echo "--install.frozen-lockfile\n--install.network-timeout 1000000" > .yarnrc
BRANCH=$(git rev-parse --abbrev-ref HEAD)

View File

@@ -61,7 +61,7 @@ function Button<P extends ElementTypes = 'button'>(
break;
case 'warning':
buttonStyle.push(
'text-white border border-yellow-500 backdrop-blur bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700'
'text-white border border-yellow-500 bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700'
);
break;
case 'success':

View File

@@ -1,18 +1,27 @@
import useSettings from '@app/hooks/useSettings';
import type { ImageProps } from 'next/image';
import type { ImageLoader, ImageProps } from 'next/image';
import Image from 'next/image';
const imageLoader: ImageLoader = ({ src }) => src;
/**
* The CachedImage component should be used wherever
* we want to offer the option to locally cache images.
*
* It uses the `next/image` Image component but overrides
* the `unoptimized` prop based on the application setting `cacheImages`.
**/
const CachedImage = (props: ImageProps) => {
const CachedImage = ({ src, ...props }: ImageProps) => {
const { currentSettings } = useSettings();
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
let imageUrl = src;
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
const parsedUrl = new URL(imageUrl);
if (parsedUrl.host === 'image.tmdb.org' && currentSettings.cacheImages) {
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
}
}
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;
};
export default CachedImage;

View File

@@ -0,0 +1,45 @@
import {
BellIcon,
CheckIcon,
ClockIcon,
MinusSmIcon,
} from '@heroicons/react/solid';
import { MediaStatus } from '@server/constants/media';
interface StatusBadgeMiniProps {
status: MediaStatus;
is4k?: boolean;
}
const StatusBadgeMini = ({ status, is4k = false }: StatusBadgeMiniProps) => {
const badgeStyle = ['w-5 rounded-full p-0.5 text-white ring-1'];
let indicatorIcon: React.ReactNode;
switch (status) {
case MediaStatus.PROCESSING:
badgeStyle.push('bg-indigo-500 ring-indigo-400');
indicatorIcon = <ClockIcon />;
break;
case MediaStatus.AVAILABLE:
badgeStyle.push('bg-green-500 ring-green-400');
indicatorIcon = <CheckIcon />;
break;
case MediaStatus.PENDING:
badgeStyle.push('bg-yellow-500 ring-yellow-400');
indicatorIcon = <BellIcon />;
break;
case MediaStatus.PARTIALLY_AVAILABLE:
badgeStyle.push('bg-green-500 ring-green-400');
indicatorIcon = <MinusSmIcon />;
break;
}
return (
<div className="inline-flex whitespace-nowrap rounded-full text-xs font-semibold leading-5 ring-1 ring-gray-700">
<div className={badgeStyle.join(' ')}>{indicatorIcon}</div>
{is4k && <span className="pl-1 pr-2 text-gray-200">4K</span>}
</div>
);
};
export default StatusBadgeMini;

View File

@@ -20,7 +20,7 @@ const Tooltip = ({ children, content, tooltipConfig }: TooltipProps) => {
return (
<>
{React.cloneElement(children, { ref: setTriggerRef })}
{visible && (
{visible && content && (
<div
ref={setTooltipRef}
{...getTooltipProps({

View File

@@ -1,3 +1,4 @@
import CachedImage from '@app/components/Common/CachedImage';
import Link from 'next/link';
import { useState } from 'react';
@@ -30,11 +31,15 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
role="link"
tabIndex={0}
>
<img
src={image}
alt={name}
className="relative z-40 max-h-full max-w-full"
/>
<div className="relative h-full w-full">
<CachedImage
src={image}
alt={name}
className="relative z-40 h-full w-full"
layout="fill"
objectFit="contain"
/>
</div>
<div
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
isHovered ? 'from-gray-800' : 'from-gray-900'

View File

@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import IssueComment from '@app/components/IssueDetails/IssueComment';
import IssueDescription from '@app/components/IssueDetails/IssueDescription';
import { issueOptions } from '@app/components/IssueModal/constants';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
@@ -91,6 +92,13 @@ const IssueDetails = () => {
: null
);
const { mediaUrl, mediaUrl4k } = useDeepLinks({
mediaUrl: data?.mediaInfo?.mediaUrl,
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
const CommentSchema = Yup.object().shape({
message: Yup.string().required(),
});
@@ -359,7 +367,7 @@ const IssueDetails = () => {
{issueData?.media.mediaUrl && (
<Button
as="a"
href={issueData?.media.mediaUrl}
href={mediaUrl}
target="_blank"
rel="noreferrer"
className="w-full"
@@ -405,7 +413,7 @@ const IssueDetails = () => {
{issueData?.media.mediaUrl4k && (
<Button
as="a"
href={issueData?.media.mediaUrl4k}
href={mediaUrl4k}
target="_blank"
rel="noreferrer"
className="w-full"
@@ -621,7 +629,7 @@ const IssueDetails = () => {
{issueData?.media.mediaUrl && (
<Button
as="a"
href={issueData?.media.mediaUrl}
href={mediaUrl}
target="_blank"
rel="noreferrer"
className="w-full"
@@ -667,7 +675,7 @@ const IssueDetails = () => {
{issueData?.media.mediaUrl4k && (
<Button
as="a"
href={issueData?.media.mediaUrl4k}
href={mediaUrl4k}
target="_blank"
rel="noreferrer"
className="w-full"

View File

@@ -121,7 +121,7 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
>
<>
<div className="sidebar relative flex h-full w-full max-w-xs flex-1 flex-col bg-gray-800">
<div className="sidebar-close-button absolute top-0 right-0 -mr-14 p-1">
<div className="sidebar-close-button absolute right-0 -mr-14 p-1">
<button
className="flex h-12 w-12 items-center justify-center rounded-full focus:bg-gray-600 focus:outline-none"
aria-label="Close sidebar"

View File

@@ -16,6 +16,7 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import getConfig from 'next/config';
import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@@ -30,7 +31,7 @@ const messages = defineMessages({
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear Data',
manageModalClearMediaWarning:
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your {mediaServerName} library, the media information will be recreated during the next scan.',
openarr: 'Open in {arr}',
openarr4k: 'Open in 4K {arr}',
downloadstatus: 'Downloads',
@@ -79,6 +80,7 @@ const ManageSlideOver = ({
const { user: currentUser, hasPermission } = useUser();
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { data: watchData } = useSWR<MediaWatchDataResponse>(
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
data.mediaInfo &&
@@ -505,6 +507,13 @@ const ManageSlideOver = ({
mediaType: intl.formatMessage(
mediaType === 'movie' ? messages.movie : messages.tvshow
),
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
</div>
</div>

View File

@@ -1,6 +1,8 @@
import TitleCard from '@app/components/TitleCard';
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
@@ -15,6 +17,18 @@ interface ShowMoreCardProps {
const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
const intl = useIntl();
const [isHovered, setHovered] = useState(false);
const { ref, inView } = useInView({
triggerOnce: true,
});
if (!inView) {
return (
<div ref={ref}>
<TitleCard.Placeholder />
</div>
);
}
return (
<Link href={url}>
<a

View File

@@ -18,6 +18,7 @@ import PersonCard from '@app/components/PersonCard';
import RequestButton from '@app/components/RequestButton';
import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -129,31 +130,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]);
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl);
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k);
useEffect(() => {
if (data) {
if (
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
) {
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
} else {
setPlexUrl(data.mediaInfo?.mediaUrl);
setPlexUrl4k(data.mediaInfo?.mediaUrl4k);
}
}
}, [
data,
data?.mediaInfo?.iOSPlexUrl,
data?.mediaInfo?.iOSPlexUrl4k,
data?.mediaInfo?.mediaUrl,
data?.mediaInfo?.mediaUrl4k,
settings.currentSettings.mediaServerType,
]);
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: data?.mediaInfo?.mediaUrl,
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
if (!data && !error) {
return <LoadingSpinner />;
@@ -378,7 +360,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
}
tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie"
plexUrl={plexUrl}
plexUrl={plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k}
/>
)}

View File

@@ -1,34 +1,43 @@
import { RefreshIcon } from '@heroicons/react/outline';
import Router from 'next/router';
import { useRouter } from 'next/router';
import PR from 'pulltorefreshjs';
import { useEffect } from 'react';
import ReactDOMServer from 'react-dom/server';
const PullToRefresh: React.FC = () => {
const PullToRefresh = () => {
const router = useRouter();
useEffect(() => {
PR.init({
mainElement: '#pull-to-refresh',
onRefresh() {
Router.reload();
router.reload();
},
iconArrow: ReactDOMServer.renderToString(
<RefreshIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
<div className="p-2">
<RefreshIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
</div>
),
iconRefreshing: ReactDOMServer.renderToString(
<RefreshIcon
className="z-50 m-auto h-9 w-9 animate-spin rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700"
<div
className="animate-spin p-2"
style={{ animationDirection: 'reverse' }}
/>
>
<RefreshIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
</div>
),
instructionsPullToRefresh: ReactDOMServer.renderToString(<div />),
instructionsReleaseToRefresh: ReactDOMServer.renderToString(<div />),
instructionsRefreshing: ReactDOMServer.renderToString(<div />),
distReload: 55,
distReload: 60,
distIgnore: 15,
shouldPullToRefresh: () =>
!window.scrollY && document.body.style.overflow !== 'hidden',
});
return () => {
PR.destroyAll();
};
}, []);
}, [router]);
return <div id="pull-to-refresh"></div>;
};

View File

@@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { withProperties } from '@app/utils/typeHelpers';
@@ -61,6 +62,13 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
const { hasPermission } = useUser();
const intl = useIntl();
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: requestData?.media?.mediaUrl,
mediaUrl4k: requestData?.media?.mediaUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
@@ -138,11 +146,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
).length > 0
}
is4k={requestData.is4k}
plexUrl={
requestData.is4k
? requestData.media.mediaUrl4k
: requestData.media.mediaUrl
}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k
@@ -217,6 +221,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
fallbackData: request,
});
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: requestData?.media?.mediaUrl,
mediaUrl4k: requestData?.media?.mediaUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
@@ -357,20 +368,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
: request.seasons.length,
})}
</span>
{title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
</span>
) : (
<div className="hide-scrollbar overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
<div className="hide-scrollbar overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
</div>
)}
<div className="mt-2 flex items-center text-sm sm:mt-1">
@@ -403,11 +407,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
plexUrl={
requestData.is4k
? requestData.media.mediaUrl4k
: requestData.media.mediaUrl
}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k

View File

@@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import {
@@ -61,6 +62,13 @@ const RequestItemError = ({
revalidateList();
};
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: requestData?.media?.mediaUrl,
mediaUrl4k: requestData?.media?.mediaUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
return (
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
@@ -130,11 +138,7 @@ const RequestItemError = ({
).length > 0
}
is4k={requestData.is4k}
plexUrl={
requestData.is4k
? requestData.media.mediaUrl4k
: requestData.media.mediaUrl
}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k
@@ -316,6 +320,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
}
};
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: requestData?.media?.mediaUrl,
mediaUrl4k: requestData?.media?.mediaUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
if (!title && !error) {
return (
<div
@@ -420,20 +431,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
: request.seasons.length,
})}
</span>
{title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
</span>
) : (
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
</div>
)}
</div>
@@ -469,11 +473,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
plexUrl={
requestData.is4k
? requestData.media.mediaUrl4k
: requestData.media.mediaUrl
}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k

View File

@@ -13,7 +13,10 @@ import { Transition } from '@headlessui/react';
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
import { PencilIcon } from '@heroicons/react/solid';
import { MediaServerType } from '@server/constants/server';
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
import type {
CacheItem,
CacheResponse,
} from '@server/interfaces/api/settingsInterfaces';
import type { JobId } from '@server/lib/settings';
import axios from 'axios';
import cronstrue from 'cronstrue/i18n';
@@ -58,6 +61,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'sonarr-scan': 'Sonarr Scan',
'download-sync': 'Download Sync',
'download-sync-reset': 'Download Sync Reset',
'image-cache-cleanup': 'Image Cache Cleanup',
editJobSchedule: 'Modify Job',
jobScheduleEditSaved: 'Job edited successfully!',
jobScheduleEditFailed: 'Something went wrong while saving the job.',
@@ -67,6 +71,11 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
editJobScheduleSelectorMinutes:
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
imagecache: 'Image Cache',
imagecacheDescription:
'When enabled in settings, Overseerr 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',
imagecachesize: 'Total Cache Size',
});
interface Job {
@@ -132,7 +141,8 @@ const SettingsJobs = () => {
} = useSWR<Job[]>('/api/v1/settings/jobs', {
refreshInterval: 5000,
});
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheItem[]>(
const { data: appData } = useSWR('/api/v1/status/appdata');
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheResponse>(
'/api/v1/settings/cache',
{
refreshInterval: 10000,
@@ -435,7 +445,7 @@ const SettingsJobs = () => {
</tr>
</thead>
<Table.TBody>
{cacheData
{cacheData?.apiCaches
?.filter(
(cache) =>
!(
@@ -465,6 +475,41 @@ const SettingsJobs = () => {
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
<p className="description">
{intl.formatMessage(messages.imagecacheDescription, {
code: (msg: React.ReactNode) => (
<code className="bg-opacity-50">{msg}</code>
),
appDataPath: appData ? appData.appDataPath : '/app/config',
})}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.cachename)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.imagecachecount)}
</Table.TH>
<Table.TH>{intl.formatMessage(messages.imagecachesize)}</Table.TH>
</tr>
</thead>
<Table.TBody>
<tr>
<Table.TD>The Movie Database (tmdb)</Table.TD>
<Table.TD>
{intl.formatNumber(cacheData?.imageCache.tmdb.imageCount ?? 0)}
</Table.TD>
<Table.TD>
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
</Table.TD>
</tr>
</Table.TBody>
</Table>
</div>
</>
);
};

View File

@@ -46,7 +46,7 @@ const messages = defineMessages({
'Do NOT enable this setting unless you understand what you are doing!',
cacheImages: 'Enable Image Caching',
cacheImagesTip:
'Cache and serve optimized images (requires a significant amount of disk space)',
'Cache externally sourced images (requires a significant amount of disk space)',
trustProxy: 'Enable Proxy Support',
trustProxyTip:
'Allow Overseerr to correctly register client IP addresses behind a proxy',
@@ -309,7 +309,7 @@ const SettingsMain = () => {
</div>
</div>
<div className="form-row">
<label htmlFor="csrfProtection" className="checkbox-label">
<label htmlFor="cacheImages" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.cacheImages)}
</span>

View File

@@ -5,12 +5,14 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
status: '{status}',
status4k: '4K {status}',
playonplex: 'Play on Plex',
playonplex: 'Play on {mediaServerName}',
openinarr: 'Open in {arr}',
managemedia: 'Manage {mediaType}',
});
@@ -37,6 +39,7 @@ const StatusBadge = ({
const intl = useIntl();
const { hasPermission } = useUser();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
let mediaLink: string | undefined;
let mediaLinkDescription: string | undefined;
@@ -68,7 +71,14 @@ const StatusBadge = ({
: settings.currentSettings.series4kEnabled))
) {
mediaLink = plexUrl;
mediaLinkDescription = intl.formatMessage(messages.playonplex);
mediaLinkDescription = intl.formatMessage(messages.playonplex, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType === MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
});
} else if (hasPermission(Permission.MANAGE_REQUESTS)) {
if (mediaType && tmdbId) {
mediaLink = `/${mediaType}/${tmdbId}?manage=1`;
@@ -77,7 +87,7 @@ const StatusBadge = ({
mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow
),
});
} else if (hasPermission(Permission.ADMIN)) {
} else if (hasPermission(Permission.ADMIN) && serviceUrl) {
mediaLink = serviceUrl;
mediaLinkDescription = intl.formatMessage(messages.openinarr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',

View File

@@ -6,6 +6,7 @@ import useSWR from 'swr';
const messages = defineMessages({
somethingwentwrong: 'Something went wrong while retrieving season data.',
noepisodes: 'Episode list unavailable.',
});
type SeasonProps = {
@@ -29,32 +30,38 @@ const Season = ({ seasonNumber, tvId }: SeasonProps) => {
return (
<div className="flex flex-col justify-center divide-y divide-gray-700">
{data.episodes
.slice()
.reverse()
.map((episode) => {
return (
<div
className="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
key={`season-${seasonNumber}-episode-${episode.episodeNumber}`}
>
<div className="flex-1">
<div className="flex flex-col space-y-2 xl:flex-row xl:items-center xl:space-y-0 xl:space-x-2">
<h3 className="text-lg">{episode.name}</h3>
<AirDateBadge airDate={episode.airDate} />
{data.episodes.length === 0 ? (
<p>{intl.formatMessage(messages.noepisodes)}</p>
) : (
data.episodes
.slice()
.reverse()
.map((episode) => {
return (
<div
className="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
key={`season-${seasonNumber}-episode-${episode.episodeNumber}`}
>
<div className="flex-1">
<div className="flex flex-col space-y-2 xl:flex-row xl:items-center xl:space-y-0 xl:space-x-2">
<h3 className="text-lg">{episode.name}</h3>
{episode.airDate && (
<AirDateBadge airDate={episode.airDate} />
)}
</div>
{episode.overview && <p>{episode.overview}</p>}
</div>
{episode.overview && <p>{episode.overview}</p>}
{episode.stillPath && (
<img
className="h-auto w-full rounded-lg xl:h-32 xl:w-auto"
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
alt=""
/>
)}
</div>
{episode.stillPath && (
<img
className="h-auto w-full rounded-lg xl:h-32 xl:w-auto"
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
alt=""
/>
)}
</div>
);
})}
);
})
)}
</div>
);
};

View File

@@ -10,6 +10,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import type { PlayButtonLink } from '@app/components/Common/PlayButton';
import PlayButton from '@app/components/Common/PlayButton';
import StatusBadgeMini from '@app/components/Common/StatusBadgeMini';
import Tooltip from '@app/components/Common/Tooltip';
import ExternalLinkBlock from '@app/components/ExternalLinkBlock';
import IssueModal from '@app/components/IssueModal';
@@ -21,6 +22,7 @@ import RequestModal from '@app/components/RequestModal';
import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge';
import Season from '@app/components/TvDetails/Season';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -124,31 +126,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]);
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl);
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k);
useEffect(() => {
if (data) {
if (
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
) {
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
} else {
setPlexUrl(data.mediaInfo?.mediaUrl);
setPlexUrl4k(data.mediaInfo?.mediaUrl4k);
}
}
}, [
data,
data?.mediaInfo?.iOSPlexUrl,
data?.mediaInfo?.iOSPlexUrl4k,
data?.mediaInfo?.mediaUrl,
data?.mediaInfo?.mediaUrl4k,
settings.currentSettings.mediaServerType,
]);
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
mediaUrl: data?.mediaInfo?.mediaUrl,
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
if (!data && !error) {
return <LoadingSpinner />;
@@ -595,75 +578,149 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
{((!mSeason &&
request?.status === MediaRequestStatus.APPROVED) ||
mSeason?.status === MediaStatus.PROCESSING) && (
<Badge badgeType="primary">
{intl.formatMessage(globalMessages.requested)}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="primary">
{intl.formatMessage(globalMessages.requested)}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.PROCESSING}
/>
</div>
</>
)}
{((!mSeason &&
request?.status === MediaRequestStatus.PENDING) ||
mSeason?.status === MediaStatus.PENDING) && (
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini status={MediaStatus.PENDING} />
</div>
</>
)}
{mSeason?.status ===
MediaStatus.PARTIALLY_AVAILABLE && (
<Badge badgeType="success">
{intl.formatMessage(
globalMessages.partiallyavailable
)}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="success">
{intl.formatMessage(
globalMessages.partiallyavailable
)}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.PARTIALLY_AVAILABLE}
/>
</div>
</>
)}
{mSeason?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.AVAILABLE}
/>
</div>
</>
)}
{((!mSeason4k &&
request4k?.status ===
MediaRequestStatus.APPROVED) ||
mSeason4k?.status4k === MediaStatus.PROCESSING) &&
show4k && (
<Badge badgeType="primary">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.requested
),
})}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="primary">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.requested
),
})}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.PROCESSING}
is4k={true}
/>
</div>
</>
)}
{((!mSeason4k &&
request4k?.status === MediaRequestStatus.PENDING) ||
mSeason?.status4k === MediaStatus.PENDING) &&
show4k && (
<Badge badgeType="warning">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.pending
),
})}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="warning">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.pending
),
})}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.PENDING}
is4k={true}
/>
</div>
</>
)}
{mSeason4k?.status4k ===
MediaStatus.PARTIALLY_AVAILABLE &&
show4k && (
<Badge badgeType="success">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.partiallyavailable
),
})}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="success">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.partiallyavailable
),
})}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.PARTIALLY_AVAILABLE}
is4k={true}
/>
</div>
</>
)}
{mSeason4k?.status4k === MediaStatus.AVAILABLE &&
show4k && (
<Badge badgeType="success">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.available
),
})}
</Badge>
<>
<div className="hidden md:flex">
<Badge badgeType="success">
{intl.formatMessage(messages.status4k, {
status: intl.formatMessage(
globalMessages.available
),
})}
</Badge>
</div>
<div className="flex md:hidden">
<StatusBadgeMini
status={MediaStatus.AVAILABLE}
is4k={true}
/>
</div>
</>
)}
<ChevronUpIcon
className={`${
@@ -788,6 +845,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</div>
)}
{data.nextEpisodeToAir &&
data.nextEpisodeToAir.airDate &&
data.nextEpisodeToAir.airDate !== data.firstAirDate && (
<div className="media-fact">
<span>{intl.formatMessage(messages.nextAirDate)}</span>
@@ -908,9 +966,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
tvdbId={data.externalIds.tvdbId}
imdbId={data.externalIds.imdbId}
rtUrl={ratingData?.url}
mediaUrl={
data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k
}
mediaUrl={plexUrl ?? plexUrl4k}
/>
</div>
</div>

View File

@@ -10,6 +10,7 @@ export type AvailableLocale =
| 'el'
| 'es'
| 'fr'
| 'hr'
| 'hu'
| 'it'
| 'ja'
@@ -60,6 +61,10 @@ export const availableLanguages: AvailableLanguageObject = {
code: 'fr',
display: 'Français',
},
hr: {
code: 'hr',
display: 'Hrvatski',
},
it: {
code: 'it',
display: 'Italiano',

45
src/hooks/useDeepLinks.ts Normal file
View File

@@ -0,0 +1,45 @@
import useSettings from '@app/hooks/useSettings';
import { MediaServerType } from '@server/constants/server';
import { useEffect, useState } from 'react';
interface useDeepLinksProps {
mediaUrl?: string;
mediaUrl4k?: string;
iOSPlexUrl?: string;
iOSPlexUrl4k?: string;
}
const useDeepLinks = ({
mediaUrl,
mediaUrl4k,
iOSPlexUrl,
iOSPlexUrl4k,
}: useDeepLinksProps) => {
const [returnedMediaUrl, setReturnedMediaUrl] = useState(mediaUrl);
const [returnedMediaUrl4k, setReturnedMediaUrl4k] = useState(mediaUrl4k);
const settings = useSettings();
useEffect(() => {
if (
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
) {
setReturnedMediaUrl(iOSPlexUrl);
setReturnedMediaUrl4k(iOSPlexUrl4k);
} else {
setReturnedMediaUrl(mediaUrl);
setReturnedMediaUrl4k(mediaUrl4k);
}
}, [
iOSPlexUrl,
iOSPlexUrl4k,
mediaUrl,
mediaUrl4k,
settings.currentSettings.mediaServerType,
]);
return { mediaUrl: returnedMediaUrl, mediaUrl4k: returnedMediaUrl4k };
};
export default useDeepLinks;

View File

@@ -37,7 +37,7 @@
"components.ManageSlideOver.alltime": "جميع الأوقات",
"components.ManageSlideOver.downloadstatus": "التنزيلات",
"components.ManageSlideOver.manageModalAdvanced": "متقدم",
"components.ManageSlideOver.manageModalClearMediaWarning": "* سيتم حذف جميع البيانات بشكل نهائي لـ {mediaType},متضمنا جميع الطلبات.إذا كان هذا المحتوى متوفر في مكتبة بليكس، سيتم إعادة تفاصيل المحتوى في عملية الفحص القادمة.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* سيتم حذف جميع البيانات بشكل نهائي لـ {mediaType},متضمنا جميع الطلبات.إذا كان هذا المحتوى متوفر في مكتبة {mediaServerName}، سيتم إعادة تفاصيل المحتوى في عملية الفحص القادمة.",
"components.ManageSlideOver.manageModalRequests": "الطلبات",
"components.ManageSlideOver.manageModalTitle": "إدارة {mediaType}",
"components.ManageSlideOver.manageModalIssues": "المشاكل المفتوحة",

View File

@@ -887,7 +887,7 @@
"components.IssueModal.CreateIssueModal.whatswrong": "Què passa?",
"components.IssueModal.issueAudio": "Àudio",
"components.IssueModal.issueOther": "Altre",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Això eliminarà de manera irreversible totes les dades de {mediaType}, incloses les sol·licituds. Si aquest element existeix a la vostra biblioteca Plex, la informació dels continguts es recrearà durant la següent exploració.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Això eliminarà de manera irreversible totes les dades de {mediaType}, incloses les sol·licituds. Si aquest element existeix a la vostra biblioteca {mediaServerName}, la informació dels continguts es recrearà durant la següent exploració.",
"components.ManageSlideOver.downloadstatus": "Descàrregues",
"components.IssueDetails.toasteditdescriptionsuccess": "La descripció de l'incidència s'ha editat correctament!",
"components.IssueList.IssueItem.issuetype": "Tipus",
@@ -924,7 +924,7 @@
"components.NotificationTypeSelector.adminissuecommentDescription": "Notifica'm quan altres usuaris facin comentaris sobre incidències.",
"components.ManageSlideOver.tvshow": "sèries",
"components.Settings.SettingsJobsCache.editJobSchedule": "Modifica la tasca programada",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Freqüència",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Freqüència nova",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Cada {jobScheduleHours, plural, one {hora} other {{jobScheduleHours} hores}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Cada {jobScheduleMinutes, plural, one {minut} other {{jobScheduleMinutes} minuts}}",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "<ApplicationRegistrationLink>Registreu una aplicació</ApplicationRegistrationLink> per utilitzar-la amb {applicationTitle}",
@@ -1104,7 +1104,7 @@
"components.RequestBlock.delete": "Suprimeix la sol·licitud",
"components.RequestBlock.edit": "Edita la sol·licitud",
"components.RequestBlock.lastmodifiedby": "Última modificació per",
"components.StatusBadge.playonplex": "Reprodueix a Plex",
"components.StatusBadge.playonplex": "Reprodueix a {mediaServerName}",
"components.RequestCard.declinerequest": "Rebutja la sol·licitud",
"components.StatusBadge.openinarr": "Obre a {arr}",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Sincronització de la llista de seguiment de Plex",
@@ -1121,5 +1121,6 @@
"components.RequestModal.requestcollectiontitle": "Sol·licitud de col·lecció",
"components.RequestModal.requestmovie4ktitle": "Sol·licitud de pel·lícula en 4K",
"components.RequestModal.requestmovietitle": "Sol·licitud de pel·lícula",
"components.RequestModal.requestseriestitle": "Sol·licitud de sèries"
"components.RequestModal.requestseriestitle": "Sol·licitud de sèries",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Freqüència actual"
}

View File

@@ -548,7 +548,7 @@
"components.ManageSlideOver.manageModalClearMedia": "Vyčistit data",
"components.ManageSlideOver.alltime": "Pořád",
"components.ManageSlideOver.manageModalAdvanced": "Pokročilý",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tímto nenávratně odstraníte všechna data pro tento {mediaType}, včetně všech požadavků. Pokud tato položka existuje ve vaší knihovně Plex, informace o médiích budou znovu vytvořeny během příštího skenování.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tímto nenávratně odstraníte všechna data pro tento {mediaType}, včetně všech požadavků. Pokud tato položka existuje ve vaší knihovně {mediaServerName}, informace o médiích budou znovu vytvořeny během příštího skenování.",
"components.ManageSlideOver.manageModalMedia": "Média",
"components.ManageSlideOver.manageModalMedia4k": "4K Média",
"components.ManageSlideOver.markallseasonsavailable": "Označte všechny sezóny jako dostupné",
@@ -1087,7 +1087,7 @@
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Watchlist synchronizace",
"components.StatusBadge.managemedia": "Spravovat {mediaType}",
"components.StatusBadge.openinarr": "Otevřít v {arr}",
"components.StatusBadge.playonplex": "Přehrávání cez Plex",
"components.StatusBadge.playonplex": "Přehrávání cez {mediaServerName}",
"components.TvDetails.manageseries": "Spravovat sérii",
"components.RequestBlock.delete": "Smazat požadavek",
"components.RequestBlock.edit": "Upravit požadavek",

View File

@@ -201,7 +201,7 @@
"components.IssueModal.issueVideo": "Video",
"components.Layout.Sidebar.issues": "Problemer",
"components.ManageSlideOver.manageModalClearMedia": "Ryd Mediedata",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette alle data for denne {mediaType} uden mulighed for gendannelse, inklusiv alle forespørgsler. Hvis dette objekt findes i dit Plex bibliotek vil medieinformationen blive genskabt under næste skanning.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette alle data for denne {mediaType} uden mulighed for gendannelse, inklusiv alle forespørgsler. Hvis dette objekt findes i dit {mediaServerName} bibliotek vil medieinformationen blive genskabt under næste skanning.",
"components.IssueModal.CreateIssueModal.whatswrong": "Hvad er galt?",
"components.IssueModal.issueAudio": "Lyd",
"components.IssueModal.issueOther": "Andet",

View File

@@ -931,7 +931,7 @@
"components.Layout.Sidebar.issues": "Probleme",
"components.ManageSlideOver.downloadstatus": "Downloads",
"components.ManageSlideOver.manageModalClearMedia": "Daten löschen",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dadurch werden alle Daten für diesen {mediaType} unwiderruflich entfernt, einschließlich aller Anfragen. Wenn dieses Element in Ihrer Plex-Bibliothek existiert, werden die Medieninformationen beim nächsten Scan neu erstellt.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dadurch werden alle Daten für diesen {mediaType} unwiderruflich entfernt, einschließlich aller Anfragen. Wenn dieses Element in Ihrer {mediaServerName}-Bibliothek existiert, werden die Medieninformationen beim nächsten Scan neu erstellt.",
"components.ManageSlideOver.manageModalIssues": "Problem eröffnen",
"components.ManageSlideOver.manageModalNoRequests": "Keine Anfragen.",
"components.ManageSlideOver.manageModalRequests": "Anfragen",

View File

@@ -141,7 +141,7 @@
"components.ManageSlideOver.downloadstatus": "Downloads",
"components.ManageSlideOver.manageModalAdvanced": "Advanced",
"components.ManageSlideOver.manageModalClearMedia": "Clear Data",
"components.ManageSlideOver.manageModalClearMediaWarning": "* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your {mediaServerName} library, the media information will be recreated during the next scan.",
"components.ManageSlideOver.manageModalIssues": "Open Issues",
"components.ManageSlideOver.manageModalMedia": "Media",
"components.ManageSlideOver.manageModalMedia4k": "4K Media",
@@ -649,6 +649,11 @@
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
"components.Settings.SettingsJobsCache.jelly-recently-added-scan": "Jellyfin Recently Added Scan",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Full Library Scan",
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
"components.Settings.SettingsJobsCache.imagecache": "Image Cache",
"components.Settings.SettingsJobsCache.imagecacheDescription": "When enabled in settings, Overseerr 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>.",
"components.Settings.SettingsJobsCache.imagecachecount": "Images Cached",
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job edited successfully!",
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
@@ -759,7 +764,7 @@
"components.Settings.applicationTitle": "Application Title",
"components.Settings.applicationurl": "Application URL",
"components.Settings.cacheImages": "Enable Image Caching",
"components.Settings.cacheImagesTip": "Cache and serve optimized images (requires a significant amount of disk space)",
"components.Settings.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection",
@@ -874,7 +879,7 @@
"components.Setup.welcome": "Welcome to Jellyseerr",
"components.StatusBadge.managemedia": "Manage {mediaType}",
"components.StatusBadge.openinarr": "Open in {arr}",
"components.StatusBadge.playonplex": "Play on Plex",
"components.StatusBadge.playonplex": "Play on {mediaServerName}",
"components.StatusBadge.status": "{status}",
"components.StatusBadge.status4k": "4K {status}",
"components.StatusChacker.newversionDescription": "Jellyseerr has been updated! Please click the button below to reload the page.",
@@ -889,6 +894,7 @@
"components.TitleCard.mediaerror": "{mediaType} Not Found",
"components.TitleCard.tmdbid": "TMDB ID",
"components.TitleCard.tvdbid": "TheTVDB ID",
"components.TvDetails.Season.noepisodes": "Episode list unavailable.",
"components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.",
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",

View File

@@ -953,7 +953,7 @@
"components.IssueModal.issueAudio": "Audio",
"components.IssueModal.issueSubtitles": "Subtítulo",
"components.IssueModal.issueVideo": "Vídeo",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Esto eliminará irreversiblemente todos los datos de {mediaType}, incluyendo todas las solicitudes. Si este elemento existe en la biblioteca de Plex, la información de los contenidos se recreará en el siguiente escaneado.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Esto eliminará irreversiblemente todos los datos de {mediaType}, incluyendo todas las solicitudes. Si este elemento existe en la biblioteca de {mediaServerName}, la información de los contenidos se recreará en el siguiente escaneado.",
"components.ManageSlideOver.mark4kavailable": "Marcar como Disponible en 4K",
"components.ManageSlideOver.openarr4k": "Abrir en 4K {arr}",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Token de Acceso",

View File

@@ -877,7 +877,7 @@
"components.ManageSlideOver.manageModalNoRequests": "Aucune demande.",
"components.ManageSlideOver.manageModalRequests": "Demandes",
"components.ManageSlideOver.manageModalTitle": "Gérer {mediaType}",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ceci supprimera de manière irréversible toutes les données de ce(tte) {mediaType}, y compris les demandes éventuelles. Si cet élément existe dans votre bibliothèque Plex, les informations sur le média seront recréées lors de la prochaine analyse.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ceci supprimera de manière irréversible toutes les données de ce(tte) {mediaType}, y compris les demandes éventuelles. Si cet élément existe dans votre bibliothèque {mediaServerName}, les informations sur le média seront recréées lors de la prochaine analyse.",
"components.ManageSlideOver.tvshow": "série",
"components.NotificationTypeSelector.issuecomment": "Commentaires du problème",
"components.NotificationTypeSelector.issuecreatedDescription": "Envoyer des notifications lorsqu'un problème est signalé.",
@@ -1099,7 +1099,7 @@
"components.RequestCard.declinerequest": "Refuser la demande",
"components.StatusBadge.managemedia": "Gérer {mediaType}",
"components.StatusBadge.openinarr": "Ouvrir dans {arr}",
"components.StatusBadge.playonplex": "Lire sur Plex",
"components.StatusBadge.playonplex": "Lire sur {mediaServerName}",
"components.TvDetails.Season.somethingwentwrong": "Une erreur s'est produite lors de la récupération des données de la saison.",
"components.TvDetails.rtaudiencescore": "Note d'audience de Rotten Tomatoes",
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatomètre",

View File

@@ -51,5 +51,214 @@
"components.IssueDetails.IssueComment.postedbyedited": "Objavljeno u {relativeTime} od korisnika {username} (Uređeno)",
"components.IssueDetails.allseasons": "Sve Sezone",
"components.IssueDetails.episode": "Epizode {episodeNumber}",
"components.IssueDetails.deleteissueconfirm": "Jeste li sigurni da želite izbrisati ovaj problem?"
"components.IssueDetails.deleteissueconfirm": "Jeste li sigurni da želite izbrisati ovaj problem?",
"components.IssueDetails.lastupdated": "Zadnje ažurirano",
"components.IssueDetails.leavecomment": "Komentar",
"components.IssueDetails.nocomments": "Bez komentara.",
"components.IssueDetails.openedby": "#{issueId} otvoren u {relativeTime} od korisnka {username}",
"components.IssueDetails.openin4karr": "Otvoren u 4K {arr}",
"components.IssueDetails.openinarr": "Otvoren u {arr}",
"components.IssueDetails.toasteditdescriptionfailed": "Nešto nije u redu prilikom uređivanja opisa problema.",
"components.IssueModal.CreateIssueModal.allepisodes": "Sve epizode",
"components.IssueDetails.toastissuedeleted": "Problem je uspješno izbrisan!",
"components.IssueDetails.unknownissuetype": "Nepoznato",
"components.IssueList.issues": "Problem",
"components.IssueList.IssueItem.openeduserdate": "{date} od korinika {user}",
"components.IssueModal.CreateIssueModal.allseasons": "Sve sezone",
"components.IssueModal.issueOther": "Ostalo",
"components.IssueModal.issueAudio": "Zvuk",
"components.IssueModal.issueSubtitles": "Podnaslov",
"components.IssueModal.issueVideo": "Video",
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {Sezona} other {Sezone}}",
"components.Layout.UserDropdown.myprofile": "Profil",
"components.Layout.UserDropdown.requests": "Zahtjevi",
"components.Layout.VersionStatus.streamstable": "Overseerr Stabilan",
"components.Login.password": "Zaporka",
"components.ManageSlideOver.openarr4k": "Otvori 4K u {arr}-u",
"components.ManageSlideOver.pastdays": "Proteklih {days, number} dana",
"components.Login.signinwithplex": "Koristite svoj Plex račun",
"components.ManageSlideOver.movie": "film",
"components.Login.validationemailrequired": "Morate unijeti valjanu adresu e-pošte",
"components.ManageSlideOver.manageModalRequests": "Zahtjevi",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ovo će nepovratno ukloniti sve podatke za ovaj {mediaType}, uključujući sve zahtjeve. Ako ova stavka postoji u vašoj {mediaServerName} biblioteci, informacije o medijima ponovno će se stvoriti tijekom sljedećeg skeniranja.",
"components.ManageSlideOver.manageModalMedia4k": "4K Mediji",
"components.ManageSlideOver.manageModalNoRequests": "Nema zahtjeva.",
"components.ManageSlideOver.manageModalMedia": "Mediji",
"components.ManageSlideOver.manageModalTitle": "Upravljanje {mediaType}",
"components.ManageSlideOver.mark4kavailable": "Označi kao dostupno u 4K",
"components.MovieDetails.originaltitle": "Izvorni naslov",
"components.MovieDetails.overview": "Pregled",
"components.ManageSlideOver.openarr": "Otvori u {arr}-u",
"components.MovieDetails.cast": "Postava",
"components.MovieDetails.budget": "Proračun",
"components.ManageSlideOver.opentautulli": "Otvori u Tautulli-u",
"components.MediaSlider.ShowMoreCard.seemore": "Vidi više",
"components.MovieDetails.markavailable": "Označi kao dostupno",
"components.ManageSlideOver.tvshow": "serije",
"components.MovieDetails.productioncountries": "{countryCount, plural, one {Država produkcije} other {Države produkcije}}",
"components.MovieDetails.managemovie": "Upravljanje filmom",
"components.MovieDetails.playonplex": "Reproduciraj na Plex-u",
"components.MovieDetails.overviewunavailable": "Pregled nedostupan.",
"components.MovieDetails.reportissue": "Prijavi problem",
"components.MovieDetails.revenue": "Prihod",
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes ocjena publike",
"components.MovieDetails.showless": "Prikaži manje",
"components.MovieDetails.showmore": "Prikaži više",
"components.MovieDetails.similar": "Slični naslovi",
"components.MovieDetails.streamingproviders": "Trenutačno se prikacuje na",
"components.NotificationTypeSelector.issuecommentDescription": "Pošaljite obavijest kada problemi dobiju nove komentare.",
"components.NotificationTypeSelector.issueresolved": "Problem riješen",
"components.NotificationTypeSelector.issuereopened": "Problem ponovno otvoren",
"components.NotificationTypeSelector.issueresolvedDescription": "Pošalji obavijest kada se problem riješi.",
"components.NotificationTypeSelector.issuereopenedDescription": "Pošalji obavijest kada se problem ponovno otvori.",
"components.NotificationTypeSelector.mediaAutoApproved": "Automatsko odobravanje zahtjeva",
"components.IssueDetails.issuepagetitle": "Problem",
"components.IssueDetails.issuetype": "Tip",
"components.IssueDetails.play4konplex": "Reproduciraj u 4K na Plex-u",
"components.IssueDetails.playonplex": "Reproduciraj na Plex-u",
"components.IssueDetails.problemseason": "Zahvaćene Sezone",
"components.IssueDetails.problemepisode": "Zahvaćene Epizode",
"components.IssueDetails.reopenissue": "Ponovno otvorite problem",
"components.IssueDetails.reopenissueandcomment": "Ponovno otvori s komentarom",
"components.IssueDetails.season": "Sezona {seasonNumber}",
"components.IssueDetails.toasteditdescriptionsuccess": "Opis problema je uspješno uređen!",
"components.IssueDetails.toastissuedeletefailed": "Nešto nije u redu prilikom brisanja problema.",
"components.IssueDetails.toaststatusupdated": "Status problema je uspješno ažuriran!",
"components.IssueDetails.toaststatusupdatefailed": "Nešto nije u redu prilikom ažuriranja statusa problema.",
"components.IssueList.IssueItem.issuestatus": "Status",
"components.IssueList.IssueItem.issuetype": "Vrsta",
"components.IssueList.IssueItem.opened": "Otvoren",
"components.IssueList.IssueItem.problemepisode": "Zahvaćene Epizode",
"components.IssueList.IssueItem.unknownissuetype": "Nepoznato",
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {Epizoda} other {Epizode}}",
"components.IssueList.IssueItem.viewissue": "Pogledaj problem",
"components.IssueList.showallissues": "Prikaži sve probleme",
"components.IssueList.sortAdded": "Najnoviji",
"components.IssueList.sortModified": "Zadnje promjene",
"components.IssueModal.CreateIssueModal.episode": "Epizoda {episodeNumber}",
"components.IssueModal.CreateIssueModal.extras": "Dodaci",
"components.IssueModal.CreateIssueModal.problemepisode": "Zahvaćene epizode",
"components.IssueModal.CreateIssueModal.problemseason": "Zahvaćene sezone",
"components.IssueModal.CreateIssueModal.providedetail": "Navedite detaljno objašnjenje problema na koji ste naišli.",
"components.IssueModal.CreateIssueModal.reportissue": "Prijavite problem",
"components.IssueModal.CreateIssueModal.season": "Sezona {seasonNumber}",
"components.IssueModal.CreateIssueModal.submitissue": "Pošalji problem",
"components.IssueModal.CreateIssueModal.toastFailedCreate": "Nešto nije u redu prilikom slanja problema.",
"components.IssueModal.CreateIssueModal.toastSuccessCreate": "Problem prijavljen za <strong>{title}</strong> je uspješno predan!",
"components.IssueModal.CreateIssueModal.toastviewissue": "Pogledaj problem",
"components.IssueModal.CreateIssueModal.validationMessageRequired": "Morate unijeti opis",
"components.IssueModal.CreateIssueModal.whatswrong": "Što nije u redu?",
"components.LanguageSelector.languageServerDefault": "Default ({language})",
"components.LanguageSelector.originalLanguageDefault": "Svi jezici",
"components.Layout.LanguagePicker.displaylanguage": "Jezik prikaza",
"components.Layout.SearchInput.searchPlaceholder": "Pretražite filmove i TV",
"components.Layout.Sidebar.dashboard": "Otkrivanje",
"components.Layout.Sidebar.issues": "Problemi",
"components.Layout.Sidebar.requests": "Zahtjevi",
"components.Layout.Sidebar.settings": "Postavke",
"components.Layout.Sidebar.users": "Korisnici",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Zahtjevi za serije",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Zahtjevi za filmove",
"components.Layout.UserDropdown.settings": "Postavke",
"components.Layout.UserDropdown.signout": "Odjavi se",
"components.Layout.VersionStatus.outofdate": "Zastarjelo",
"components.Layout.VersionStatus.streamdevelop": "Overseerr Razvoj",
"components.Login.email": "Adresa e-pošte",
"components.Login.forgotpassword": "Zaboravljena lozinka?",
"components.Login.loginerror": "Nešto nije u redu prilikom pokušaja prijave.",
"components.Login.signin": "Prijavite se",
"components.Login.signingin": "Prijava…",
"components.Layout.VersionStatus.commitsbehind": "",
"components.Login.signinheader": "Prijavite se za nastavak",
"components.Login.signinwithoverseerr": "Koristite svoj {applicationTitle} račun",
"components.Login.validationpasswordrequired": "Morate unijeti lozinku",
"components.ManageSlideOver.alltime": "Cijelo vrijeme",
"components.ManageSlideOver.downloadstatus": "Preuzimanja",
"components.ManageSlideOver.manageModalAdvanced": "Napredna",
"components.ManageSlideOver.manageModalClearMedia": "Obriši podatke",
"components.ManageSlideOver.manageModalIssues": "Otvoreni problemi",
"components.ManageSlideOver.markallseasons4kavailable": "Označi sve sezone kao dostupne u 4K",
"components.ManageSlideOver.markallseasonsavailable": "Označi sve sezone kao dostupne",
"components.ManageSlideOver.markavailable": "Označi kao dostupno",
"components.ManageSlideOver.playedby": "Reproducirano od",
"components.ManageSlideOver.plays": "<strong>{playCount, broj}</strong> {playCount, plural, one {reproducirano} other {reproducirano}}",
"components.MovieDetails.MovieCast.fullcast": "Glumačka postava",
"components.MovieDetails.digitalrelease": "Digitalno izdanje",
"components.MovieDetails.mark4kavailable": "Označi kao dostupno u 4K",
"components.MovieDetails.originallanguage": "Izvorni jezik",
"components.MovieDetails.MovieCrew.fullcrew": "Filmska postava",
"components.MovieDetails.physicalrelease": "Fizičko izdanje",
"components.MovieDetails.play4konplex": "Reproduciraj u 4K na Plex-u",
"components.MovieDetails.recommendations": "Preporuke",
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Datum Izlaska} other {Datumi izlaska}}",
"components.MovieDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
"components.MovieDetails.runtime": "{minutes} minute",
"components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studiji}}",
"components.MovieDetails.theatricalrelease": "Izdanje u kinima",
"components.MovieDetails.tmdbuserscore": "Ocjena korisnika TMDB-a",
"components.MovieDetails.viewfullcrew": "Pogledajte cijelu filmsku postavu",
"components.MovieDetails.watchtrailer": "Pogledajte najavu",
"components.NotificationTypeSelector.adminissuecommentDescription": "Primite obavijest kada drugi korisnici komentiraju probleme.",
"components.NotificationTypeSelector.adminissuereopenedDescription": "Primite obavijest kada problem ponovno otvore drugi korisnici.",
"components.NotificationTypeSelector.adminissueresolvedDescription": "Primite obavijest kada drugi korisnici riješe probleme.",
"components.NotificationTypeSelector.issuecomment": "Komentiraj problem",
"components.NotificationTypeSelector.issuecreated": "Problem prijavljen",
"components.NotificationTypeSelector.issuecreatedDescription": "Pošalji obavijest kada se problem prijavi.",
"components.NotificationTypeSelector.userissueresolvedDescription": "Primite obavijest kada problemi koje ste prijavili budu riješeni.",
"components.NotificationTypeSelector.mediaavailableDescription": "Slanje obavijesti kada medijski zahtjevi postanu dostupni.",
"components.NotificationTypeSelector.mediadeclinedDescription": "Slanje obavijesti kada su medijski zahtjevi odbijeni.",
"components.NotificationTypeSelector.mediarequested": "Zahtjev čeka odobrenje",
"components.NotificationTypeSelector.mediarequestedDescription": "Slanje obavijesti kada korisnici pošalju nove medijske zahtjeve koji zahtijevaju odobrenje.",
"components.NotificationTypeSelector.mediaautorequested": "Zahtjev je automatski poslan",
"components.NotificationTypeSelector.mediaavailable": "Zahtjev dostupan",
"components.NotificationTypeSelector.mediafailedDescription": "Slanje obavijesti kada se medijski zahtjevi ne uspiju dodati u Radarr ili Sonarr.",
"components.NotificationTypeSelector.userissuecommentDescription": "Primite obavijest kada problemi koje ste prijavili dobiju nove komentare.",
"components.PermissionEdit.autoapprove4kSeries": "Automatsko odobravanje serija u 4K",
"components.NotificationTypeSelector.usermediafailedDescription": "Primite obavijest kada se medijski zahtjevi ne uspiju dodati u Radarr ili Sonarr.",
"components.NotificationTypeSelector.usermediarequestedDescription": "Primite obavijest kada drugi korisnici pošalju nove medijske zahtjeve koji zahtijevaju odobrenje.",
"components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Primite obavijest kada drugi korisnici pošalju nove medijske zahtjeve koji se automatski odobravaju.",
"components.NotificationTypeSelector.usermediadeclinedDescription": "Primite obavijest kada vaši medijski zahtjevi budu odbijeni.",
"components.PermissionEdit.adminDescription": "Potpuni administratorski pristup. Zaobilazi sve druge provjere dopuštenja.",
"components.PermissionEdit.advancedrequest": "Napredni zahtjevi",
"components.PermissionEdit.autoapprove4k": "Automatsko odobravanje 4K",
"components.PermissionEdit.autoapproveSeriesDescription": "Dozvolite automatsko odobravanje zahtjeva za serijale koji nisu u 4K.",
"components.PermissionEdit.autoapprove4kMoviesDescription": "Dozvolite automatsko odobravanje zahtjeva za filmove u 4K.",
"components.PermissionEdit.autoapprove4kSeriesDescription": "Dozvolite automatsko odobravanje zahtjeva za serije u 4K.",
"components.QuotaSelector.days": "{count, plural, one {danu} other {danu}}",
"components.QuotaSelector.movies": "{count, plural, one {film} other {filmova}}",
"components.PermissionEdit.autoapproveMoviesDescription": "Dozvolite automatsko odobravanje zahtjeva za filmove koji nisu u 4K.",
"components.RequestButton.approve4krequests": "Odobriti {requestCount, plural, one {4K Zahtjev} other {{requestCount} 4K Zahtjeve}}",
"components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {film} other {filmova}}",
"components.RequestButton.approverequests": "Odobriti {requestCount, plural, one {Zatjev} other {{requestCount} Zahtjeve}}",
"components.QuotaSelector.seasons": "{count, plural, one {sezona} other {sezone}}",
"components.RequestCard.seasons": "{seasonCount, plural, one {Sezona} other {Sezone}}",
"components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Sezona} other {Sezone}}",
"components.RequestBlock.seasons": "{seasonCount, plural, one {Sezona} other {Sezone}}",
"components.RequestButton.decline4krequests": "Odbiti {requestCount, plural, one {4K Zahtjev} other {{requestCount} 4K Zahtjeve}}",
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Ovaj korisnik treba imati još barem <strong>{seasons}</strong> {seasons, plural, one {jedan zahtjev za sezonu} other {nekoliko zahtjeva za sezone}} kako bi mogao preadti zahtjev za ovu seriju.",
"components.RequestModal.QuotaDisplay.requiredquota": "Morate imati još barem <strong>{seasons}</strong> {seasons, plural, one {jedan zahtjev za sezonu} other {nekoliko zahtjeva za sezone}} kako bi mogli preadti zahtjev za ovu seriju.",
"components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {sezona} other {sezona/e}}",
"components.RequestModal.requestmovies": "{count} {count, plural, one {Zahtjev za film} other {Zahtjevi za filmove}}",
"components.RequestModal.requestmovies4k": "{count} {count, plural, one {Zahtjev za film} other {Zahtejvi za filmove}} u 4K",
"components.NotificationTypeSelector.mediaAutoApprovedDescription": "Slanje obavijesti kada korisnici pošalju novi medijski zahtjev koji se automatski odobrava.",
"components.NotificationTypeSelector.mediaapproved": "Zahtjev odobren",
"components.NotificationTypeSelector.mediaapprovedDescription": "Slanje obavijesti kada se medijski zahtjev ručno odobri.",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Primite obavijest kada se automatski pošalje novi medijski zahtjevi za stavke na vašoj Plex listi koju pratite.",
"components.NotificationTypeSelector.mediadeclined": "Zahtjev je odbijen",
"components.NotificationTypeSelector.mediafailed": "Obrada zahtjeva nije uspjela",
"components.NotificationTypeSelector.notificationTypes": "Vrste obavijesti",
"components.NotificationTypeSelector.userissuecreatedDescription": "Primite obavijest kada drugi korisnici prijave probleme.",
"components.NotificationTypeSelector.userissuereopenedDescription": "Primite obavijest kada se problemi koje ste prijavili ponovno otvore.",
"components.NotificationTypeSelector.usermediaapprovedDescription": "Primite obavijest kada vaši zahtjevi za medije budu odobreni.",
"components.NotificationTypeSelector.usermediaavailableDescription": "Primite obavijest kada vaši medijski zahtjevi postanu dostupni.",
"components.PermissionEdit.admin": "Administrator",
"components.PermissionEdit.advancedrequestDescription": "Dodajte dozvolu za izmjenu naprednih opcija zahtjeva za medije.",
"components.PermissionEdit.autoapprove": "Automatsko odobravanje",
"components.PermissionEdit.autoapprove4kMovies": "Automatsko odobravanje 4K filmova",
"components.PermissionEdit.autoapprove4kDescription": "Dozvolite automatsko odobravanje svih zahtjeva za 4K medije.",
"components.PermissionEdit.autoapproveDescription": "Dozvolite automatsko odobravanje svih zahtjeva koji nisu u 4K mediji.",
"components.PermissionEdit.autoapproveMovies": "Automatsko odobravanje filmova",
"components.PermissionEdit.autoapproveSeries": "Automatsko odobravanje serijala",
"components.RequestButton.declinerequests": "Odbiti {requestCount, plural, one {Zahtjev} other {{requestCount} Zahtjeve}}",
"components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {No} other {<strong>#</strong>}} {type} {remaining, plural, one {zahtjev preostalo} other {zahtjeva preostala}}"
}

View File

@@ -851,7 +851,7 @@
"components.IssueModal.CreateIssueModal.toastFailedCreate": "Valami hiba történt a probléma elküldése során.",
"components.IssueDetails.play4konplex": "Lejátszás Plexen 4K-ban",
"components.IssueModal.CreateIssueModal.toastviewissue": "Probléma Megtekintése",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ez visszafordíthatatlanul eltávolítja az összes adatot ehhez a {mediaType}-hez, beleértve a kéréseket is. Ha ez az elem létezik a Plex könyvtárában, a médiainformáció a következő beolvasás során újra létrejön.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ez visszafordíthatatlanul eltávolítja az összes adatot ehhez a {mediaType}-hez, beleértve a kéréseket is. Ha ez az elem létezik a {mediaServerName} könyvtárában, a médiainformáció a következő beolvasás során újra létrejön.",
"components.IssueDetails.commentplaceholder": "Hozzászólás írása…",
"components.IssueDetails.comments": "Hozzászólások",
"components.IssueDetails.deleteissue": "Probléma Törlése",
@@ -1014,5 +1014,31 @@
"i18n.importing": "Importálás…",
"i18n.import": "Importálás",
"components.PermissionEdit.viewissues": "Problémák Megtekintése",
"components.Settings.externalUrl": "Külső URL"
"components.Settings.externalUrl": "Külső URL",
"components.MovieDetails.physicalrelease": "Fizikai kiadás",
"components.MovieDetails.digitalrelease": "Digitális kiadás",
"components.RequestCard.cancelrequest": "Kérés visszavonása",
"components.RequestCard.declinerequest": "Kérelem elutasítása",
"components.RequestCard.editrequest": "Kérelem szerkesztése",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "",
"components.PermissionEdit.autorequest": "Automatikus kérés",
"components.NotificationTypeSelector.mediaautorequested": "A kérelem automatikusan elküldve",
"components.MovieDetails.reportissue": "Probléma bejelentése",
"components.PermissionEdit.autorequestMovies": "Filmek automatikus kérése",
"components.NotificationTypeSelector.issuecomment": "Probléma Megjegyzés",
"components.PermissionEdit.autorequestSeries": "Automatikus kérés sorozatok",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Sorozatkérések",
"components.MovieDetails.managemovie": "Film kezelése",
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes közönségpontszám",
"components.MovieDetails.tmdbuserscore": "TMDB felhasználói pontszám",
"components.RequestBlock.delete": "Kérelem törlése",
"components.RequestBlock.edit": "Kérelem szerkesztése",
"components.RequestBlock.approve": "Kérelem jóváhagyása",
"components.RequestBlock.decline": "Kérelem elutasítása",
"components.RequestBlock.lastmodifiedby": "Utoljára módosította",
"components.RequestBlock.requestdate": "Igénylés dátuma",
"components.RequestCard.approverequest": "Kérelem jóváhagyása",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Filmkérések",
"components.Layout.UserDropdown.requests": "Kérések",
"components.RequestModal.requestcollectiontitle": "Gyűjtemény kérése"
}

View File

@@ -950,7 +950,7 @@
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job modificato correttamente!",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "Impossibile salvare le impostazioni Pushover.",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Impostazioni Pushover salvate con successo!",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Questo rimuoverà irreversibilmente tutti i dati per questo {mediaType}, incluse eventuali richieste. Se questo elemento esiste nella tua libreria Plex, le informazioni multimediali verranno ricreate durante la scansione successiva.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Questo rimuoverà irreversibilmente tutti i dati per questo {mediaType}, incluse eventuali richieste. Se questo elemento esiste nella tua libreria {mediaServerName}, le informazioni multimediali verranno ricreate durante la scansione successiva.",
"components.NotificationTypeSelector.issuecreated": "Problema Segnalato",
"components.NotificationTypeSelector.issuecreatedDescription": "Invia una notifica quando un problema viene segnalato.",
"components.NotificationTypeSelector.issueresolved": "Problema risolto",

View File

@@ -503,7 +503,7 @@
"components.ManageSlideOver.manageModalClearMedia": "データを消去",
"components.ManageSlideOver.manageModalRequests": "リクエスト",
"components.ManageSlideOver.openarr": "{arr} を開く",
"components.ManageSlideOver.manageModalClearMediaWarning": "※リクエストを含め、すべての詳細情報が消去されます。この操作は元に戻すことができません。この作品が Plex ライブラリに存在する場合、詳細情報は次のスキャンで再作成されます。",
"components.ManageSlideOver.manageModalClearMediaWarning": "※リクエストを含め、すべての詳細情報が消去されます。この操作は元に戻すことができません。この作品が {mediaServerName} ライブラリに存在する場合、詳細情報は次のスキャンで再作成されます。",
"components.ManageSlideOver.openarr4k": "4K {arr} を開く",
"components.ManageSlideOver.manageModalNoRequests": "リクエストが有りません。",
"components.ManageSlideOver.manageModalTitle": "{mediaType}を管理",

View File

@@ -329,7 +329,7 @@
"components.IssueModal.CreateIssueModal.problemseason": "Paveikti sezonai",
"components.IssueDetails.openedby": "#{issueId} problema atverta {relativeTime}, {username}",
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {komitas} other {komitai}} behind",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tai negyžtamai pašalins {mediaType} tipo duomenis, įskaitant rezervacijas. Plex bibliotekoje esančios medijos informacija bus atkurta kito skanavimo metu.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tai negyžtamai pašalins {mediaType} tipo duomenis, įskaitant rezervacijas. {mediaServerName} bibliotekoje esančios medijos informacija bus atkurta kito skanavimo metu.",
"components.NotificationTypeSelector.adminissuecommentDescription": "Gauti pranešimus kai kiti vartotojai komentuoja problemą.",
"components.NotificationTypeSelector.adminissueresolvedDescription": "Gauti pranešimus kai kiti vartotojai uždaro problemą.",
"components.NotificationTypeSelector.issuecomment": "Problemos komentaras",

View File

@@ -180,7 +180,7 @@
"components.UserProfile.UserSettings.UserGeneralSettings.role": "Rolle",
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Filtrer innhold basert på regiontilgjengelighet",
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Utforskelsesregion",
"components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Plexbruker",
"components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Plex-bruker",
"components.UserProfile.UserSettings.UserGeneralSettings.owner": "Eier",
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Filtrer innhold basert på originalspråk",
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Utforskelsesspråk",
@@ -337,7 +337,7 @@
"components.MediaSlider.ShowMoreCard.seemore": "Vis mer",
"components.Login.validationpasswordrequired": "Du må skrive et passord",
"components.Login.validationemailrequired": "Du må bruke en gyldig E-postadresse",
"components.Login.signinwithplex": "Bruk Plex-konto",
"components.Login.signinwithplex": "Bruk din Plex-konto",
"components.Login.signinwithoverseerr": "Bruk {applicationTitle}-konto",
"components.Login.signinheader": "Logg inn for å fortsette",
"components.Login.signingin": "Logger inn…",
@@ -753,7 +753,7 @@
"components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "Du må oppgi en applikasjon/API-nøkkel",
"i18n.next": "Neste",
"components.Settings.SettingsJobsCache.editJobSchedule": "Endre Oppgave",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Frekvens",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Ny Frekvens",
"components.TvDetails.firstAirDate": "Første gang sendt",
"i18n.deleting": "Sletter…",
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Innstillingene for E-post ble lagret!",
@@ -854,8 +854,8 @@
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Oppgaven ble endret!",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Hver {jobScheduleHours}. time",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Hvert {jobScheduleMinutes}. minutt",
"components.Settings.SettingsUsers.localLoginTip": "Tillater brukere å kunne logge inn med kun deres E-postadresse og passord istedenfor med Plex OAuth",
"components.Settings.SettingsUsers.newPlexLoginTip": "Tillater Plex brukere å logge inn uten å være importert på forhånd",
"components.Settings.SettingsUsers.localLoginTip": "Tilllat brukere å logge med kun E-postadresse og passord istedenfor med Plex OAuth",
"components.Settings.SettingsUsers.newPlexLoginTip": "Tillat Plex brukere å logge inn uten å være importert på forhånd",
"components.Settings.SonarrModal.validationApplicationUrl": "Du må oppgi en gyldig nettadresse",
"components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "Base URL kan ikke slutte med en skråstrek",
"components.Settings.locale": "Visningsspråk",
@@ -986,7 +986,7 @@
"components.Settings.SettingsJobsCache.cachevsize": "Verdistørrelse",
"components.Settings.trustProxyTip": "Tillatt Jellyseerr å registrere klienters IP addresser korrekt bak en proxy",
"components.Settings.serviceSettingsDescription": "Konfigurer dine {serverType}tjener(e) nedenfor. Du kan koble til flere forskellige {serverType}tjenere men kun to av dem kan markeres som standard (en som ikke er 4K og en 4K). Administratorer kan endre hvilken tjener som brukes før godkjennelse av nye forespørsler.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette all data for denne tittelen uten mulighet for å bli gjennopprettet, det inkluderer alle forespørsler, avvik osv. Hvis denne tittelen finnes i ditt Plex bibliotek vil medieinformasjon bli opprettet på nytt under neste skanning.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette all data for denne tittelen uten mulighet for å bli gjennopprettet, det inkluderer alle forespørsler, avvik osv. Hvis denne tittelen finnes i ditt {mediaServerName} bibliotek vil medieinformasjon bli opprettet på nytt under neste skanning.",
"components.Settings.Notifications.NotificationsWebhook.authheader": "Autorisasjonshode",
"components.Settings.SettingsJobsCache.cacheksize": "Nøkkelstørrelse",
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
@@ -1097,7 +1097,7 @@
"components.Settings.advancedTooltip": "Feil konfigurering av denne innstillingen kan føre til defekt funksjonalitet",
"components.TvDetails.Season.somethingwentwrong": "Noe gikk galt under henting av data for denne sesongen.",
"components.StatusChecker.reloadApp": "Last inn {applicationTitle} på nytt",
"components.StatusBadge.playonplex": "Spill av med Plex",
"components.StatusBadge.playonplex": "Spill av med {mediaServerName}",
"components.StatusBadge.openinarr": "Vis i {arr}",
"components.StatusBadge.managemedia": "Administrer {mediaType}",
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episode} other {# Episoder}}",
@@ -1116,5 +1116,6 @@
"components.RequestModal.requestcollectiontitle": "Forespør hele samlingen",
"components.Discover.emptywatchlist": "Matriale som du legger til via <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> vil dukke opp her.",
"components.UserProfile.emptywatchlist": "Matriale som du legger til via <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> vil dukke opp her.",
"components.RequestModal.SearchByNameModal.nomatches": "Vi klarte ikke å koble denne serien med et søkbart treff."
"components.RequestModal.SearchByNameModal.nomatches": "Vi klarte ikke å koble denne serien med et søkbart treff.",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Nåværende frekvens"
}

View File

@@ -33,7 +33,7 @@
"components.RequestModal.cancel": "Verzoek annuleren",
"components.RequestModal.extras": "Extra's",
"components.RequestModal.numberofepisodes": "Aantal afleveringen",
"components.RequestModal.pendingrequest": "",
"components.RequestModal.pendingrequest": "Verzoek in behandeling",
"components.RequestModal.requestCancel": "Verzoek voor <strong>{title}</strong> is geannuleerd.",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> is succesvol aangevraagd!",
"components.RequestModal.requestadmin": "Dit verzoek zal automatisch goedgekeurd worden.",
@@ -291,7 +291,7 @@
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON-payload",
"components.Settings.Notifications.NotificationsWebhook.authheader": "Autorisatie-header",
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Agent inschakelen",
"components.RequestModal.pending4krequest": "",
"components.RequestModal.pending4krequest": "4K-verzoek in behandeling",
"components.RequestButton.viewrequest4k": "4K-verzoek bekijken",
"components.RequestButton.viewrequest": "Verzoek bekijken",
"components.RequestButton.requestmore": "Meer aanvragen",
@@ -632,7 +632,7 @@
"components.Settings.SettingsJobsCache.jobsandcache": "Taken en cache",
"components.Settings.SettingsAbout.about": "Over",
"components.ResetPassword.passwordreset": "Wachtwoord opnieuw instellen",
"components.Settings.cacheImagesTip": "Cache en serveer geoptimaliseerde afbeeldingen (een aanzienlijke hoeveelheid schijfruimte is nodig)",
"components.Settings.cacheImagesTip": "Geoptimaliseerde afbeeldingen cachen en hosten (vereist veel schijfruimte)",
"components.Settings.cacheImages": "Afbeeldingscaching inschakelen",
"components.Settings.SettingsLogs.logDetails": "Loggegevens",
"components.Settings.SettingsLogs.extraData": "Aanvullende gegevens",
@@ -713,9 +713,9 @@
"components.RequestModal.AdvancedRequester.selecttags": "Labels selecteren",
"components.RequestModal.AdvancedRequester.notagoptions": "Geen labels.",
"components.Settings.RadarrModal.loadingTags": "Labels laden…",
"components.RequestList.RequestItem.mediaerror": "{mediaType} Niet Gevonden",
"components.RequestList.RequestItem.mediaerror": "{mediaType} niet gevonden",
"components.RequestList.RequestItem.deleterequest": "Verzoek verwijderen",
"components.RequestCard.mediaerror": "{mediaType} Niet Gevonden",
"components.RequestCard.mediaerror": "{mediaType} niet gevonden",
"components.RequestCard.deleterequest": "Verzoek verwijderen",
"components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Je moet een geldige openbare PGP-sleutel opgeven",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Instellingen Telegrammeldingen succesvol opgeslagen!",
@@ -855,7 +855,7 @@
"components.MovieDetails.streamingproviders": "Momenteel te streamen op",
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Taak succesvol bewerkt!",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Elk(e) {jobScheduleMinutes, plural, one {minuut} other {{jobScheduleMinutes} minuten}}",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Frequentie",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Nieuwe frequentie",
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Er ging iets mis bij het opslaan van de taak.",
"components.Settings.SettingsJobsCache.editJobSchedule": "Taak wijzigen",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Elk(e) {jobScheduleHours, plural, one {uur} other {{jobScheduleHours} uren}}",
@@ -893,7 +893,7 @@
"components.IssueModal.CreateIssueModal.allepisodes": "Alle afleveringen",
"components.IssueModal.issueAudio": "Audio",
"components.IssueDetails.nocomments": "Geen opmerkingen.",
"components.IssueModal.CreateIssueModal.reportissue": "Een probleem melden",
"components.IssueModal.CreateIssueModal.reportissue": "Probleem melden",
"components.IssueDetails.allepisodes": "Alle afleveringen",
"components.IssueDetails.toasteditdescriptionsuccess": "Probleembeschrijving succesvol bewerkt!",
"components.IssueDetails.toastissuedeleted": "Probleem succesvol verwijderd!",
@@ -939,7 +939,7 @@
"components.IssueModal.issueOther": "Andere",
"components.Layout.Sidebar.issues": "Problemen",
"components.ManageSlideOver.manageModalClearMedia": "Gegevens wissen",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Hiermee worden alle gegevens voor deze {mediaType} onomkeerbaar verwijderd, inclusief eventuele verzoeken. Als dit item in je Plex-bibliotheek staat, worden de mediagegevens opnieuw aangemaakt tijdens de volgende scan.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Hiermee worden alle gegevens voor deze {mediaType} onomkeerbaar verwijderd, inclusief eventuele verzoeken. Als dit item in je {mediaServerName}-bibliotheek staat, worden de mediagegevens opnieuw aangemaakt tijdens de volgende scan.",
"components.ManageSlideOver.manageModalRequests": "Verzoeken",
"components.ManageSlideOver.manageModalTitle": "{mediaType} beheren",
"components.ManageSlideOver.tvshow": "serie",
@@ -1037,5 +1037,93 @@
"components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Gebruikers-ID Discord",
"components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Het <FindDiscordIdLink>meercijferige ID-nummer</FindDiscordIdLink> van je Discord-account",
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Je moet een geldige gebruikers-ID van Discord opgeven",
"components.Settings.SettingsAbout.appDataPath": "Gegevensmap"
"components.Settings.SettingsAbout.appDataPath": "Gegevensmap",
"components.RequestBlock.languageprofile": "Taalprofiel",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Huidige frequentie",
"components.StatusBadge.managemedia": "{mediaType} beheren",
"components.StatusBadge.openinarr": "Openen in {arr}",
"components.StatusBadge.playonplex": "Afspelen op {mediaServerName}",
"components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
"components.MovieDetails.digitalrelease": "Digitale release",
"i18n.restartRequired": "Opnieuw opstarten vereist",
"components.PermissionEdit.viewrecentDescription": "Toestemming geven om de lijst met recent toegevoegde media te bekijken.",
"components.PermissionEdit.viewrecent": "Recent toegevoegd bekijken",
"components.Settings.deleteServer": "{serverType}-server verwijderen",
"components.StatusChecker.appUpdated": "{applicationTitle} bijgewerkt",
"components.RequestList.RequestItem.tmdbid": "TMDB ID",
"components.RequestList.RequestItem.tvdbid": "TheTVDB ID",
"components.StatusChecker.restartRequired": "Server opnieuw opstarten vereist",
"components.StatusChecker.restartRequiredDescription": "Start de server opnieuw op om de bijgewerkte instellingen toe te passen.",
"components.TitleCard.cleardata": "Gegevens wissen",
"components.TitleCard.mediaerror": "{mediatype} niet gevonden",
"components.TitleCard.tvdbid": "TheTVDB ID",
"components.RequestCard.tmdbid": "TMDB ID",
"components.RequestCard.declinerequest": "Verzoek weigeren",
"components.RequestCard.editrequest": "Verzoek bewerken",
"components.RequestCard.cancelrequest": "Verzoek annuleren",
"components.RequestModal.requestcollection4ktitle": "Collectie aanvragen in 4K",
"components.RequestModal.requestcollectiontitle": "Collectie aanvragen",
"components.RequestModal.requestseries4ktitle": "Serie aanvragen in 4K",
"components.RequestModal.requestmovie4ktitle": "Film aanvragen in 4K",
"components.RequestModal.requestseriestitle": "Serie aanvragen",
"components.RequestModal.requestmovietitle": "Film aanvragen",
"components.TvDetails.tmdbuserscore": "Gebruikersscore TMDB",
"components.TvDetails.rtaudiencescore": "Publieksscore Rotten Tomatoes",
"components.TvDetails.seasonnumber": "Seizoen {seasonNumber}",
"components.TvDetails.Season.somethingwentwrong": "Er ging iets mis bij het ophalen van de seizoensgegevens.",
"components.TvDetails.seasonstitle": "Seizoenen",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Je Plex Kijklijst",
"components.Discover.plexwatchlist": "Je Plex Kijklijst",
"components.MovieDetails.physicalrelease": "Fysieke release",
"components.PermissionEdit.autorequest": "Automatisch aanvragen",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Kijklijst synchroniseren",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Series automatisch aanvragen",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Automatisch series op je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> aanvragen",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Automatisch films op je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> aanvragen",
"components.PermissionEdit.autorequestDescription": "Toestemming geven om niet-4K media in je Plex Kijklijst automatisch aan te vragen.",
"components.RequestCard.tvdbid": "TheTVDB ID",
"components.Discover.DiscoverWatchlist.watchlist": "Plex Kijklijst",
"components.MovieDetails.theatricalrelease": "Bioscooprelease",
"components.NotificationTypeSelector.mediaautorequested": "Aanvraag automatisch ingediend",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Ontvang een melding wanneer er automatisch nieuwe mediaverzoeken worden ingediend voor items op je Plex Kijklijst.",
"components.PermissionEdit.autorequestSeriesDescription": "Toestemming geven om niet-4K series in je Plex Kijklijst automatisch aan te vragen.",
"components.PermissionEdit.viewwatchlists": "Plex Kijklijsten bekijken",
"components.PermissionEdit.viewwatchlistsDescription": "Toestemming verlenen om de Plex Kijklijsten van andere gebruikers te bekijken.",
"components.Settings.SettingsLogs.viewdetails": "Details bekijken",
"components.Settings.advancedTooltip": "Deze instelling onjuist configureren, kan resulteren in gebroken functionaliteit",
"components.StatusChecker.reloadApp": "{applicationTitle} opnieuw laden",
"components.TitleCard.tmdbid": "TMDB ID",
"components.StatusChecker.appUpdatedDescription": "Klik op de onderstaande knop om de toepassing opnieuw te laden.",
"components.UserProfile.plexwatchlist": "Plex Kijklijst",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Films automatisch aanvragen",
"components.TvDetails.manageseries": "Serie beheren",
"components.MovieDetails.managemovie": "Film beheren",
"components.MovieDetails.reportissue": "Probleem melden",
"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.autorequestMovies": "Films automatisch aanvragen",
"components.Settings.experimentalTooltip": "Deze instelling inschakelen, kan leiden tot onverwacht gedrag van de toepassing",
"components.Settings.restartrequiredTooltip": "Overseerr moet opnieuw worden gestart om wijzigingen in deze instelling door te voeren",
"components.AirDateBadge.airedrelative": "{relativeTime} uitgezonden",
"components.AirDateBadge.airsrelative": "Uitzending {relativeTime}",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Serieverzoeken",
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# aflevering} other {# afleveringen}}",
"components.TvDetails.status4k": "4K {status}",
"components.MovieDetails.rtaudiencescore": "Publieksscore Rotten Tomatoes",
"components.MovieDetails.rtcriticsscore": "Tomatometer Rotten Tomatoes",
"components.MovieDetails.tmdbuserscore": "Gebruikersscore TMDB",
"components.RequestBlock.approve": "Verzoek goedkeuren",
"components.TvDetails.reportissue": "Probleem melden",
"components.TvDetails.rtcriticsscore": "Tomatometer Rotten Tomatoes",
"components.RequestModal.SearchByNameModal.nomatches": "We konden geen match vinden voor deze serie.",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Filmverzoeken",
"components.Layout.UserDropdown.requests": "Verzoeken",
"components.RequestBlock.decline": "Verzoek weigeren",
"components.Discover.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
"components.RequestBlock.delete": "Verzoek verwijderen",
"components.RequestBlock.edit": "Verzoek bewerken",
"components.RequestBlock.lastmodifiedby": "Laatst gewijzigd door",
"components.RequestBlock.requestdate": "Aanvraagdatum",
"components.RequestBlock.requestedby": "Aangevraagd door",
"components.RequestCard.approverequest": "Verzoek goedkeuren"
}

View File

@@ -103,7 +103,7 @@
"components.PermissionEdit.createissues": "Zgłoś problemy",
"components.PermissionEdit.manageissues": "Zarządzaj problemami",
"components.PermissionEdit.manageissuesDescription": "Udziel uprawnień do zarządzania problemami z multimediami.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Spowoduje to nieodwracalne usunięcie wszystkich danych dla {mediaType}, w tym wszelkie prośby. Jeśli ten element istnieje w Twojej bibliotece Plex, informacje o multimediach zostaną odtworzone podczas następnego skanowania.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Spowoduje to nieodwracalne usunięcie wszystkich danych dla {mediaType}, w tym wszelkie prośby. Jeśli ten element istnieje w Twojej bibliotece {mediaServerName}, informacje o multimediach zostaną odtworzone podczas następnego skanowania.",
"components.IssueModal.CreateIssueModal.providedetail": "Podaj szczegółowe wyjaśnienie napotkanego problemu.",
"components.IssueModal.CreateIssueModal.whatswrong": "Co jest nie tak?",
"components.Discover.MovieGenreList.moviegenres": "Gatunki filmowe",

View File

@@ -924,7 +924,7 @@
"components.IssueModal.issueOther": "Outros",
"components.IssueModal.issueSubtitles": "Legenda",
"components.ManageSlideOver.manageModalClearMedia": "Limpar Dados",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Isso irá remover em definitivo todos dados desse(a) {mediaType}, incluindo quaisquer solicitações para esse item. Se este item existir in sua biblioteca do Plex, os dados de mídia serão recriados na próxima sincronia.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Isso irá remover em definitivo todos dados desse(a) {mediaType}, incluindo quaisquer solicitações para esse item. Se este item existir in sua biblioteca do {mediaServerName}, os dados de mídia serão recriados na próxima sincronia.",
"components.ManageSlideOver.manageModalIssues": "Problemas Abertos",
"components.ManageSlideOver.manageModalNoRequests": "Nenhuma solicitação.",
"components.ManageSlideOver.manageModalRequests": "Solicitações",
@@ -1098,7 +1098,7 @@
"components.RequestBlock.requestdate": "Data do pedido",
"components.RequestCard.declinerequest": "Rejeitar Pedido",
"components.RequestCard.editrequest": "Editar Pedido",
"components.StatusBadge.playonplex": "Reproduzir no Plex",
"components.StatusBadge.playonplex": "Reproduzir no {mediaServerName}",
"components.RequestBlock.decline": "Rejeitar pedido",
"components.RequestBlock.lastmodifiedby": "Última modificação por",
"components.RequestBlock.delete": "Deletar pedido",

View File

@@ -871,7 +871,7 @@
"components.IssueDetails.allseasons": "Все сезоны",
"components.IssueDetails.allepisodes": "Все эпизоды",
"components.ManageSlideOver.manageModalClearMedia": "Очистить данные",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Это приведёт к необратимому удалению всех данных для этого {mediaType}а, включая любые запросы. Если этот элемент существует в вашей библиотеке Plex, мультимедийная информация о нём будет воссоздана во время следующего сканирования.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Это приведёт к необратимому удалению всех данных для этого {mediaType}а, включая любые запросы. Если этот элемент существует в вашей библиотеке {mediaServerName}, мультимедийная информация о нём будет воссоздана во время следующего сканирования.",
"components.IssueDetails.problemepisode": "Затронутый эпизод",
"components.ManageSlideOver.manageModalRequests": "Запросы",
"components.IssueDetails.closeissue": "Закрыть проблему",

View File

@@ -5,7 +5,7 @@
"components.IssueModal.CreateIssueModal.submitissue": "Paraqit Problemin",
"components.IssueModal.CreateIssueModal.toastSuccessCreate": "Raporti i problemit për <strong>{title}</strong> u paraqit me sukses!",
"components.IssueModal.CreateIssueModal.toastviewissue": "Shiko Problemin",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Kjo do të heqë në mënyrë të pakthyeshme të gjitha të dhënat për këtë {mediaType}, duke përfshirë çdo kërkesë. Nëse ky artikull ekziston në bibliotekën tuaj Plex, informacioni i medias do të rikrijohet gjatë skanimit të ardhshëm.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Kjo do të heqë në mënyrë të pakthyeshme të gjitha të dhënat për këtë {mediaType}, duke përfshirë çdo kërkesë. Nëse ky artikull ekziston në bibliotekën tuaj {mediaServerName}, informacioni i medias do të rikrijohet gjatë skanimit të ardhshëm.",
"components.AppDataWarning.dockerVolumeMissingDescription": "Monitimi i volumit <code>{appDataPath}</code> nuk u konfigurua siç duhet. Gjithë informacioni do të fshihet kur kontenieri do të mbyllet ose të ristartohet.",
"components.Discover.StudioSlider.studios": "Studiot",
"components.Layout.UserDropdown.settings": "Cilësimet",

View File

@@ -609,7 +609,7 @@
"components.Settings.SettingsAbout.uptodate": "Najsvežiji",
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Morate da navedete važeći JSON korisni teret",
"components.Settings.Notifications.validationChatIdRequired": "Morate da navedete važeći ID za ćaskanje",
"components.StatusBadge.playonplex": "Igrajte na Plex-u",
"components.StatusBadge.playonplex": "Igrajte na {mediaServerName}-u",
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Morate da obezbedite pristupni token",
"components.UserList.userssaved": "Korisničke dozvole su uspešno sačuvane!"
}

View File

@@ -521,7 +521,7 @@
"components.Settings.scanning": "Synkar…",
"components.Settings.scan": "Skanna bibliotek",
"components.Settings.regionTip": "Filtrera innehåll efter region tillgänglighet",
"components.Settings.region": "Upptäck Region",
"components.Settings.region": "Upptäck region",
"components.Settings.originallanguageTip": "Filtrera innehåll efter originalspråk",
"components.Settings.originallanguage": "Upptäck språk",
"components.Settings.notificationAgentSettingsDescription": "Konfigurera och aktivera aviseringsagenter.",
@@ -683,8 +683,8 @@
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Serieförfrågnings gräns",
"components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Filmförfrågnings gräns",
"components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Överskrid den globala gränsen",
"components.Settings.SettingsUsers.tvRequestLimitLabel": "Global serieförfrågnings gräns",
"components.Settings.SettingsUsers.movieRequestLimitLabel": "Global filmförfrågnings gräns",
"components.Settings.SettingsUsers.tvRequestLimitLabel": "Global serieförfrågningsgräns",
"components.Settings.SettingsUsers.movieRequestLimitLabel": "Global filmförfrågningsgräns",
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Den här användaren behöver ha minst <strong>{seasons}</strong> {seasons, plural, one {säsongsförfrågan} other {säsongsförfrågningar}} kvar för att skicka in en begäran om denna serie.",
"components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {säsong} other {säsonger}}",
"components.RequestModal.QuotaDisplay.season": "säsong",
@@ -951,7 +951,7 @@
"components.NotificationTypeSelector.issuecreated": "Problem rappoterat",
"components.PermissionEdit.createissues": "Rapportera problem",
"components.PermissionEdit.viewissues": "Visa problem",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Detta tar bort all data för denna {mediaType}, inklusive eventuella begäranden, på ett oåterkalleligt sätt. Om det här objektet finns i ditt Plex-bibliotek kommer medieinformationen att återskapas vid nästa genomsökning.",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Detta tar bort all data för denna {mediaType}, inklusive eventuella begäranden, på ett oåterkalleligt sätt. Om det här objektet finns i ditt {mediaServerName}-bibliotek kommer medieinformationen att återskapas vid nästa genomsökning.",
"components.ManageSlideOver.manageModalNoRequests": "Inga förfrågningar.",
"components.NotificationTypeSelector.userissueresolvedDescription": "Få meddelande när dina rapporterade problem har blivit lösta.",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Åtkomsttoken",

View File

@@ -944,7 +944,7 @@
"components.NotificationTypeSelector.userissueresolvedDescription": "当您报告的问题解决时获取通知。",
"components.ManageSlideOver.alltime": "历史",
"components.ManageSlideOver.manageModalAdvanced": "高级",
"components.ManageSlideOver.manageModalClearMediaWarning": "* 这将会删除所有和{mediaType}相关的数据和所有请求。如果{mediaType}在您的Plex服务器存在,数据将会在媒体库扫描时重新建立。",
"components.ManageSlideOver.manageModalClearMediaWarning": "* 这将会删除所有和{mediaType}相关的数据和所有请求。如果{mediaType}在您的{mediaServerName}服务器存在,数据将会在媒体库扫描时重新建立。",
"components.ManageSlideOver.manageModalIssues": "未解决问题",
"components.ManageSlideOver.manageModalMedia": "媒体",
"components.ManageSlideOver.manageModalMedia4k": "4K 媒体",
@@ -986,7 +986,7 @@
"components.Settings.Notifications.NotificationsPushbullet.channelTag": "频道标签",
"components.Settings.RadarrModal.announced": "已公布",
"components.Settings.RadarrModal.released": "已发布",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "频率",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "频率",
"components.Settings.externalUrl": "外部网址",
"components.Settings.tautulliApiKey": "API Key",
"components.Settings.toastTautulliSettingsFailure": "保存 Tautulli 设置时出现问题。",
@@ -1031,5 +1031,42 @@
"i18n.import": "导入",
"i18n.importing": "导入中…",
"components.RequestBlock.languageprofile": "语言配置文件",
"components.TitleCard.mediaerror": "未找到{mediaType}"
"components.TitleCard.mediaerror": "未找到{mediaType}",
"components.MovieDetails.digitalrelease": "数字发行",
"components.MovieDetails.physicalrelease": "物理释放",
"components.MovieDetails.theatricalrelease": "剧场版",
"components.PermissionEdit.viewrecent": "查看最近添加的内容",
"components.PermissionEdit.viewrecentDescription": "授予查看最近添加的媒体列表的权限。",
"components.StatusChecker.appUpdated": "{applicationTitle} 已更新",
"components.StatusChecker.restartRequired": "需要重启服务器",
"components.StatusChecker.appUpdatedDescription": "请点击下面的按钮,重新加载应用程序。",
"components.StatusChecker.reloadApp": "重新加载 {applicationTitle}",
"i18n.restartRequired": "需要重新启动",
"components.Settings.deleteServer": "删除 {serverType} 服务器",
"components.StatusChecker.restartRequiredDescription": "请重新启动服务器以应用更新的设置。",
"components.RequestList.RequestItem.tmdbid": "TMDB ID",
"components.Discover.DiscoverWatchlist.watchlist": "Plex 关注列表",
"components.MovieDetails.managemovie": "管理电影",
"components.MovieDetails.reportissue": "报告问题",
"components.NotificationTypeSelector.mediaautorequested": "自动提交的请求",
"components.PermissionEdit.viewwatchlistsDescription": "授权查看其他用户的Plex关注列表。",
"components.RequestList.RequestItem.tvdbid": "TheTVDB ID",
"components.Settings.advancedTooltip": "错误配置此设置可能会导致功能不可用",
"components.Settings.experimentalTooltip": "启用此设置可能会导致意外的应用程序行为",
"components.TvDetails.reportissue": "报告问题",
"components.RequestCard.tmdbid": "TMDB ID",
"components.Settings.SettingsLogs.viewdetails": "查看详情",
"components.Layout.UserDropdown.requests": "请求",
"components.Settings.restartrequiredTooltip": "必须重新启动 Overseerr 才能使更改的设置生效",
"components.TvDetails.manageseries": "管理电视节目",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "自动请求您的 <PlexWatchlistSupportLink>Plex 关注列表</PlexWatchlistSupportLink>的媒体",
"components.AirDateBadge.airedrelative": "播出{relativeTime}",
"components.AirDateBadge.airsrelative": "播出{relativeTime}",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "电影请求",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "电视节目请求",
"components.NotificationTypeSelector.mediaautorequestedDescription": "当 Plex 关注列表中的项目自动提交新媒体请求时,会收到通知。",
"components.PermissionEdit.viewwatchlists": "查看 Plex 关注列表",
"components.TvDetails.Season.somethingwentwrong": "在检索季元数据时出了问题。",
"components.UserProfile.plexwatchlist": "Plex 关注列表",
"components.RequestCard.tvdbid": "TheTVDB ID"
}

View File

@@ -847,7 +847,7 @@
"components.MovieDetails.streamingproviders": "目前的流媒體服務",
"components.Settings.SettingsJobsCache.editJobSchedule": "編輯作業",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "每 {jobScheduleHours} 小時",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "頻率",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "新的頻率",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "每 {jobScheduleMinutes} 分鐘",
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "儲存作業設定時出了點問題。",
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "作業編輯成功!",
@@ -884,7 +884,7 @@
"components.IssueModal.issueAudio": "音訊",
"components.ManageSlideOver.downloadstatus": "下載狀態",
"components.IssueModal.CreateIssueModal.allepisodes": "所有集數",
"components.ManageSlideOver.manageModalClearMediaWarning": "※這將會刪除包括使用者請求在內所有有關此{mediaType}的資料。如果這{mediaType}存在於您的 Plex 伺服器,資料將會在媒體庫掃描時重新建立。",
"components.ManageSlideOver.manageModalClearMediaWarning": "※這將會刪除包括使用者請求在內所有有關此{mediaType}的資料。如果這{mediaType}存在於您的 {mediaServerName} 伺服器,資料將會在媒體庫掃描時重新建立。",
"components.ManageSlideOver.mark4kavailable": "標記 4K 版為可觀看",
"components.IssueModal.issueSubtitles": "字幕",
"components.IssueModal.issueOther": "其他",
@@ -1051,29 +1051,29 @@
"components.TitleCard.tmdbid": "TMDB ID",
"components.RequestCard.tmdbid": "TMDB ID",
"components.RequestList.RequestItem.tvdbid": "TheTVDB ID",
"components.Discover.plexwatchlist": "您的 Plex Watchlist",
"components.Discover.plexwatchlist": "您的 Plex 關注列表",
"components.PermissionEdit.autorequestMovies": "自動提出電影請求",
"components.PermissionEdit.autorequestSeries": "自動提出影集請求",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Watchlist 同步",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex 關注列表同步",
"components.PermissionEdit.autorequest": "自動提出請求",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "您的 Plex Watchlist",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "您的 Plex 關注列表",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "自動提出電影請求",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "自動提出影集請求",
"components.NotificationTypeSelector.mediaautorequested": "請求自動提出",
"components.PermissionEdit.autorequestMoviesDescription": "授予從 Plex Watchlist 中自動提出非 4K 電影請求的權限。",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "從您的 <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> 中自動提出電影請求",
"components.NotificationTypeSelector.mediaautorequestedDescription": "當您的 Plex Watchlist 中的媒體自動提出請求時取得通知。",
"components.PermissionEdit.autorequestDescription": "授予從 Plex Watchlist 中自動提出非 4K 媒體請求的權限。",
"components.PermissionEdit.autorequestSeriesDescription": "授予從 Plex Watchlist 中自動提出非 4K 影集請求的權限。",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "從您的 <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> 中自動提出影集請求",
"components.PermissionEdit.autorequestMoviesDescription": "授予從 Plex 關注列表中自動提出非 4K 電影請求的權限。",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "從您的 <PlexWatchlistSupportLink>Plex 關注列表</PlexWatchlistSupportLink>中自動提出電影請求",
"components.NotificationTypeSelector.mediaautorequestedDescription": "當您的 Plex 關注列表中的媒體自動提出請求時取得通知。",
"components.PermissionEdit.autorequestDescription": "授予從 Plex 關注列表中自動提出非 4K 媒體請求的權限。",
"components.PermissionEdit.autorequestSeriesDescription": "授予從 Plex 關注列表中自動提出非 4K 影集請求的權限。",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "從您的 <PlexWatchlistSupportLink>Plex 關注列表</PlexWatchlistSupportLink>中自動提出影集請求",
"components.Settings.SettingsLogs.viewdetails": "查看詳細信息",
"components.TvDetails.reportissue": "報告問題",
"components.MovieDetails.managemovie": "管理電影",
"components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist",
"components.UserProfile.plexwatchlist": "Plex Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Plex 關注列表",
"components.UserProfile.plexwatchlist": "Plex 關注列表",
"components.MovieDetails.reportissue": "報告問題",
"components.PermissionEdit.viewwatchlists": "查看 Plex Watchlists",
"components.PermissionEdit.viewwatchlistsDescription": "授予查看其他使用者的 Plex Watchlists 的權限。",
"components.PermissionEdit.viewwatchlists": "查看 Plex 關注列表",
"components.PermissionEdit.viewwatchlistsDescription": "授予查看其他使用者的 Plex 關注列表的權限。",
"components.TvDetails.manageseries": "管理影集",
"components.Settings.restartrequiredTooltip": "Jellyseerr 必須重新啟動才能應用設定的變更",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "電影請求",
@@ -1092,7 +1092,7 @@
"components.RequestBlock.delete": "刪除請求",
"components.RequestCard.editrequest": "編輯請求",
"components.RequestBlock.requestedby": "請求者",
"components.StatusBadge.playonplex": "在 Plex 上觀看",
"components.StatusBadge.playonplex": "在 {mediaServerName} 上觀看",
"components.StatusBadge.managemedia": "管理{mediaType}",
"components.StatusBadge.openinarr": "開啟 {arr} 伺服器",
"components.TvDetails.status4k": "4K 版{status}",
@@ -1113,8 +1113,9 @@
"components.RequestModal.requestseries4ktitle": "提出 4K 影集請求",
"components.RequestModal.requestcollectiontitle": "提出電影系列請求",
"components.RequestModal.SearchByNameModal.nomatches": "找不到此影集的數據。",
"components.UserProfile.emptywatchlist": "您的 <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> 中的媒體會顯示在這裡。",
"components.Discover.emptywatchlist": "您的 <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> 中的媒體會顯示在這裡。",
"components.UserProfile.emptywatchlist": "您的 <PlexWatchlistSupportLink>Plex 關注列表</PlexWatchlistSupportLink>中的媒體會顯示在這裡。",
"components.Discover.emptywatchlist": "您的 <PlexWatchlistSupportLink>Plex 關注列表</PlexWatchlistSupportLink>中的媒體會顯示在這裡。",
"components.Settings.advancedTooltip": "錯誤的設定可能會破壞應用程式功能",
"components.Settings.experimentalTooltip": "啟用此設定可能會出現意外的應用程式行為"
"components.Settings.experimentalTooltip": "啟用此設定可能會出現意外的應用程式行為",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "目前的頻率"
}

View File

@@ -43,6 +43,8 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
return import('../i18n/locale/es.json');
case 'fr':
return import('../i18n/locale/fr.json');
case 'hr':
return import('../i18n/locale/hr.json');
case 'hu':
return import('../i18n/locale/hu.json');
case 'it':

View File

@@ -470,6 +470,6 @@
z-index: 30 !important;
}
.ptr--ptr .ptr--box {
.ptr--box {
margin-bottom: -13px !important;
}