Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a3d7c5915 | ||
|
|
912c63e562 | ||
|
|
f6865ca0c1 | ||
|
|
c9fe6cb0b7 | ||
|
|
3778ad829c | ||
|
|
b1786245c7 | ||
|
|
b7f5739035 | ||
|
|
c128898206 | ||
|
|
ff2ab29491 | ||
|
|
108dfc4afd | ||
|
|
6f2de8577e | ||
|
|
1bd0e646e3 | ||
|
|
077513151e | ||
|
|
191f39e8bd | ||
|
|
fb883e8043 | ||
|
|
4435468561 | ||
|
|
e821d0a02b | ||
|
|
5f71fb7ee2 | ||
|
|
a0c5608aa0 | ||
|
|
816fec1a83 | ||
|
|
2a4dd52275 | ||
|
|
3601d442db | ||
|
|
53bede692d | ||
|
|
fe46895289 | ||
|
|
a0024a0cbe | ||
|
|
8408e19568 | ||
|
|
ed49e7c28e | ||
|
|
549567a7e9 | ||
|
|
51f99a2e7b | ||
|
|
048b6c1d90 | ||
|
|
a6a065ac0b | ||
|
|
5e5ba40505 | ||
|
|
abb0b2d763 | ||
|
|
d2668d3f49 | ||
|
|
36b2b4fba4 | ||
|
|
6beac736ef | ||
|
|
6933b661ca | ||
|
|
4b6ad8a387 | ||
|
|
992a23222a | ||
|
|
e75b592bb2 | ||
|
|
baa62b286d | ||
|
|
908f63557c | ||
|
|
f88c4a6d4a | ||
|
|
0972f40a4e | ||
|
|
1f0486eba2 | ||
|
|
086183b563 | ||
|
|
df4ef1fbe5 | ||
|
|
727fa06c18 | ||
|
|
bee6eb288f | ||
|
|
3473aed27e | ||
|
|
a9144a2167 | ||
|
|
59822d6e42 | ||
|
|
ee84f74f8a | ||
|
|
18925decaf | ||
|
|
67146c33ef | ||
|
|
d5eb4d8d43 | ||
|
|
fc12ab84d9 | ||
|
|
0d088e085e | ||
|
|
2fe53ec5a8 | ||
|
|
ce0266f74e | ||
|
|
c21fa5b535 | ||
|
|
647d5efb09 | ||
|
|
bb89f679b6 | ||
|
|
3210ac29d0 | ||
|
|
e9c899ce41 | ||
|
|
0bd1bee1fa | ||
|
|
46326f9d16 | ||
|
|
ad18a40969 | ||
|
|
ae2ede8952 | ||
|
|
675060bcdf | ||
|
|
f8f388db6d | ||
|
|
2948f9360e | ||
|
|
df4ac8361f | ||
|
|
92a874e36d | ||
|
|
6d7907e844 | ||
|
|
d9788c4aa9 | ||
|
|
db0a5c44f6 | ||
|
|
c545ec16e7 | ||
|
|
961d110720 | ||
|
|
e8776fd336 | ||
|
|
7c08809501 | ||
|
|
7fd43c9529 | ||
|
|
33f8831e88 | ||
|
|
20b119c1ac | ||
|
|
6cd20491d2 | ||
|
|
cb8a50bd66 | ||
|
|
fddbb3cdfe | ||
|
|
01c179f762 | ||
|
|
e08fa35548 | ||
|
|
d96d65b8af | ||
|
|
54b96cc451 | ||
|
|
7d2a187865 | ||
|
|
900827be97 | ||
|
|
b9faa6486b | ||
|
|
3f2a04c881 | ||
|
|
ff618956b5 | ||
|
|
b5988f9a5f | ||
|
|
3ba09d07eb | ||
|
|
f998873fc5 | ||
|
|
33b54c08b5 | ||
|
|
34136ef617 | ||
|
|
17df5dd072 | ||
|
|
4b48a36ad3 | ||
|
|
2d3359a7e3 | ||
|
|
6d08b10820 | ||
|
|
78af1a3e6d | ||
|
|
16bd3e9fd5 | ||
|
|
6360c82898 | ||
|
|
67a8f7fbfe | ||
|
|
e17c637483 | ||
|
|
15013d6c5d | ||
|
|
8cb05c413a | ||
|
|
2098a2d3d2 | ||
|
|
6c1ee830a1 | ||
|
|
0658b7943e | ||
|
|
f1df9eeb2f | ||
|
|
edbbccf3ae | ||
|
|
a3973791fb | ||
|
|
cb5c0403c1 | ||
|
|
7bf924f7e9 | ||
|
|
c53dc3b15d | ||
|
|
b96bd33982 | ||
|
|
92c9001c9d |
@@ -70,6 +70,125 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jvennik",
|
||||
"name": "jvennik",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/6672637?v=4",
|
||||
"profile": "https://github.com/jvennik",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "darknessgp",
|
||||
"name": "darknessgp",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1521243?v=4",
|
||||
"profile": "https://github.com/darknessgp",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "saltydk",
|
||||
"name": "salty",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6587950?v=4",
|
||||
"profile": "https://github.com/saltydk",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Shutruk",
|
||||
"name": "Shutruk",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/9198633?v=4",
|
||||
"profile": "https://github.com/Shutruk",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "krystiancharubin",
|
||||
"name": "Krystian Charubin",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/17775600?v=4",
|
||||
"profile": "https://github.com/krystiancharubin",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kieron",
|
||||
"name": "Kieron Boswell",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/8655212?v=4",
|
||||
"profile": "https://github.com/kieron",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "samwiseg0",
|
||||
"name": "samwiseg0",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2241731?v=4",
|
||||
"profile": "https://github.com/samwiseg0",
|
||||
"contributions": [
|
||||
"question",
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ecelebi29",
|
||||
"name": "ecelebi29",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/8337120?v=4",
|
||||
"profile": "https://github.com/ecelebi29",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mmozeiko",
|
||||
"name": "Mārtiņš Možeiko",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/1665010?v=4",
|
||||
"profile": "https://github.com/mmozeiko",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mazzetta86",
|
||||
"name": "mazzetta86",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/45591560?v=4",
|
||||
"profile": "https://github.com/mazzetta86",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Panzer1119",
|
||||
"name": "Paul Hagedorn",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/23016343?v=4",
|
||||
"profile": "https://github.com/Panzer1119",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Shagon94",
|
||||
"name": "Shagon94",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/9140783?v=4",
|
||||
"profile": "https://github.com/Shagon94",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sebstrgg",
|
||||
"name": "sebstrgg",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/27026694?v=4",
|
||||
"profile": "https://github.com/sebstrgg",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [sct]
|
||||
patreon: overseerr
|
||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,6 +9,11 @@ assignees: ''
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**What version of Overseerr are you running?**
|
||||
Please fill in the version you are currently running.
|
||||
|
||||
You can find it under: Settings -> About -> Version
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -29,12 +29,33 @@ jobs:
|
||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, 'skip ci')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: sctx/overseerr
|
||||
tags: develop
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.CR_PAT }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
tags: |
|
||||
sctx/overseerr:develop
|
||||
sctx/overseerr:${{ github.sha }}
|
||||
ghcr.io/sct/overseerr:develop
|
||||
ghcr.io/sct/overseerr:${{ github.sha }}
|
||||
|
||||
19
.github/workflows/invalid_template.yml
vendored
Normal file
19
.github/workflows/invalid_template.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: 'Invalid Template'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'invalid:template-incomplete'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, please edit your issue and follow the template provided.
|
||||
close-issue: false
|
||||
lock-issue: false
|
||||
issue-lock-reason: 'resolved'
|
||||
151
CHANGELOG.md
151
CHANGELOG.md
@@ -1,3 +1,154 @@
|
||||
# [1.11.0](https://github.com/sct/overseerr/compare/v1.10.0...v1.11.0) (2020-12-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add language picker to setup/login ([ff2ab29](https://github.com/sct/overseerr/commit/ff2ab29491a80c421525b9a394d6fbbf54914dc2))
|
||||
* **frontend:** add support overseerr block to about page ([c128898](https://github.com/sct/overseerr/commit/c128898206d6cbb482de4d8dca53f70b87e4911a))
|
||||
* **frontend:** releases added to about page ([b7f5739](https://github.com/sct/overseerr/commit/b7f573903500cc8a62e39afd787bc1da8c09d88b)), closes [#303](https://github.com/sct/overseerr/issues/303)
|
||||
* **lang:** add support for Italian, Portuguese (Brazil) and Serbian ([108dfc4](https://github.com/sct/overseerr/commit/108dfc4afd31388cb6c9e07deccd168ade8b1574))
|
||||
* **lang:** add support for swedish language ([c9fe6cb](https://github.com/sct/overseerr/commit/c9fe6cb0b7ea984d8e4e1cb3f284935c9da7cc2b))
|
||||
* **lang:** translations update from Weblate ([#400](https://github.com/sct/overseerr/issues/400)) ([1bd0e64](https://github.com/sct/overseerr/commit/1bd0e646e313ddf77ef331e818e03401fbf64a72))
|
||||
* **lang:** translations update from Weblate ([#403](https://github.com/sct/overseerr/issues/403)) ([3778ad8](https://github.com/sct/overseerr/commit/3778ad829c0897de178212b3bde4c0d3b5089161))
|
||||
|
||||
# [1.10.0](https://github.com/sct/overseerr/compare/v1.9.1...v1.10.0) (2020-12-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **email:** fix link to Overseerr in email templates ([816fec1](https://github.com/sct/overseerr/commit/816fec1a83a53edb3b65c3e5e7d0e6e1bd49726d)), closes [#392](https://github.com/sct/overseerr/issues/392)
|
||||
* **frontend:** adjust padding of search box so placeholder text fits on mobile ([3601d44](https://github.com/sct/overseerr/commit/3601d442db32d3f98f7b050365c11ea8ef9bc4ae)), closes [#393](https://github.com/sct/overseerr/issues/393)
|
||||
* **frontend:** changed request block for slideover on mobile UI ([#387](https://github.com/sct/overseerr/issues/387)) ([549567a](https://github.com/sct/overseerr/commit/549567a7e9db01933546d9970fc06f17218dfab1))
|
||||
* **frontend:** hide Request More button if all current seasons are available ([2a4dd52](https://github.com/sct/overseerr/commit/2a4dd52275007e48f946c3b9e29f1d78da57bdaa)), closes [#343](https://github.com/sct/overseerr/issues/343)
|
||||
* **frontend:** try not to render broken rottentomatoes data ([a0c5608](https://github.com/sct/overseerr/commit/a0c5608aa0b6c7a4294300589efa9a662163ce48))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** translations update from Weblate ([#391](https://github.com/sct/overseerr/issues/391)) ([5f71fb7](https://github.com/sct/overseerr/commit/5f71fb7ee280714275d2ac045c472fcdddd5a2ea))
|
||||
* add missing tzdata package to image ([53bede6](https://github.com/sct/overseerr/commit/53bede692d4f0e940dededa63015fe1908129914)), closes [#394](https://github.com/sct/overseerr/issues/394)
|
||||
* **frontend:** add external links to movie and tv detail pages ([a0024a0](https://github.com/sct/overseerr/commit/a0024a0cbe717d78f53413bb78644c829f143c4d))
|
||||
* **lang:** translations update from Weblate ([#380](https://github.com/sct/overseerr/issues/380)) ([8408e19](https://github.com/sct/overseerr/commit/8408e19568b2f239c57e11e2946c75f193d1c22e))
|
||||
|
||||
## [1.9.1](https://github.com/sct/overseerr/compare/v1.9.0...v1.9.1) (2020-12-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* change default internal port to 5055 ([#389](https://github.com/sct/overseerr/issues/389)) ([5e5ba40](https://github.com/sct/overseerr/commit/5e5ba4050563f07bff367d2fb31ed7e7fca4291e))
|
||||
|
||||
# [1.9.0](https://github.com/sct/overseerr/compare/v1.8.0...v1.9.0) (2020-12-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* api key regeneration ([6beac73](https://github.com/sct/overseerr/commit/6beac736efcf7b9102e02e43b75d91a9a158cd22))
|
||||
* **api:** add movie keyword search ([f88c4a6](https://github.com/sct/overseerr/commit/f88c4a6d4a49f8f3451ba6c85153677f33b7f5f6))
|
||||
* **frontend:** add studio/networks to movie/tv details ([4b6ad8a](https://github.com/sct/overseerr/commit/4b6ad8a3871957db4192b603abf38404250cea5d)), closes [#370](https://github.com/sct/overseerr/issues/370)
|
||||
* **frontend:** added user deletion to the user list ([727fa06](https://github.com/sct/overseerr/commit/727fa06c18febb2a97ca219cc6bf0277ff462acd)), closes [#348](https://github.com/sct/overseerr/issues/348)
|
||||
* **holiday:** special seasonal slider added to discover :) ([908f635](https://github.com/sct/overseerr/commit/908f63557ca03a1da8b16809ffa2c3acd782d94e))
|
||||
* allow to listen server on specific host interface ([#381](https://github.com/sct/overseerr/issues/381)) ([086183b](https://github.com/sct/overseerr/commit/086183b5636aa8d075d01fe59492c3eab0d1345b)), closes [#273](https://github.com/sct/overseerr/issues/273)
|
||||
* anime profile support ([#384](https://github.com/sct/overseerr/issues/384)) ([0972f40](https://github.com/sct/overseerr/commit/0972f40a4e1fb3b5f02b07ae46b997d71aab9bfb)), closes [#266](https://github.com/sct/overseerr/issues/266)
|
||||
|
||||
# [1.8.0](https://github.com/sct/overseerr/compare/v1.7.0...v1.8.0) (2020-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** translations update from Weblate ([#336](https://github.com/sct/overseerr/issues/336)) ([ee84f74](https://github.com/sct/overseerr/commit/ee84f74f8a3558875b41daa539f42d00b949898a))
|
||||
|
||||
# [1.7.0](https://github.com/sct/overseerr/compare/v1.6.0...v1.7.0) (2020-12-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **email:** do not pass auth object to transport if no auth data present ([d5eb4d8](https://github.com/sct/overseerr/commit/d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef)), closes [#312](https://github.com/sct/overseerr/issues/312)
|
||||
* **frontend:** add http/https prefix to hostname fields for plex/radarr/sonarr ([ce0266f](https://github.com/sct/overseerr/commit/ce0266f74ea3979b291ff962271a928682892788)), closes [#357](https://github.com/sct/overseerr/issues/357)
|
||||
* **frontend:** clarify that radarr/sonnarr servers must be tested before profiles/folders appear ([fc12ab8](https://github.com/sct/overseerr/commit/fc12ab84d9482eb3a11f117f8cab6fd48a9401cd)), closes [#326](https://github.com/sct/overseerr/issues/326) [#328](https://github.com/sct/overseerr/issues/328)
|
||||
* **frontend:** correctly show an unauthorized error when a user fails to login ([18925de](https://github.com/sct/overseerr/commit/18925decafdac518f52a354c594cc378d2529022)), closes [#322](https://github.com/sct/overseerr/issues/322)
|
||||
* **frontend:** fix tv shows failing to open when firstAirDate is undefined ([c21fa5b](https://github.com/sct/overseerr/commit/c21fa5b5350abdd8e03c077fde7246fa398e176e)), closes [#347](https://github.com/sct/overseerr/issues/347)
|
||||
* **frontend:** make minimum availability required for Radarr servers ([2fe53ec](https://github.com/sct/overseerr/commit/2fe53ec5a8534e75c7d0cef31a8b46065111e0a7)), closes [#345](https://github.com/sct/overseerr/issues/345)
|
||||
* **plex-sync:** bundle duplicate ratingKeys to speed up recently added sync ([67146c3](https://github.com/sct/overseerr/commit/67146c33ef7f28d520ba2c50b32673d43f4525c8)), closes [#360](https://github.com/sct/overseerr/issues/360)
|
||||
* **sonarr.ts, mediarequest.ts:** add missing seasonFolder option ([#358](https://github.com/sct/overseerr/issues/358)) ([e9c899c](https://github.com/sct/overseerr/commit/e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** show alert when there are no default radarr/sonarr servers ([0d088e0](https://github.com/sct/overseerr/commit/0d088e085e68d39455fda21d1fd08ebcaef2c06b)), closes [#344](https://github.com/sct/overseerr/issues/344)
|
||||
|
||||
# [1.6.0](https://github.com/sct/overseerr/compare/v1.5.0...v1.6.0) (2020-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** accept the api key to perform actions on the api with X-API-Key header ([33f8831](https://github.com/sct/overseerr/commit/33f8831e880dc7fd3f69d951246cada5c6c0ffe7))
|
||||
* **api:** filter out libraries that do not have any metadata agent or are not movie/show ([01c179f](https://github.com/sct/overseerr/commit/01c179f762e686a1e5a3d4dab3a5bea53425b575))
|
||||
* **api:** only run recently added sync on enabled libraries ([e08fa35](https://github.com/sct/overseerr/commit/e08fa35548bb8644afa8df3124e6f9cc3a2c8f4a)), closes [#259](https://github.com/sct/overseerr/issues/259)
|
||||
* **api:** set plex libraries to disabled if the name changes ([675060b](https://github.com/sct/overseerr/commit/675060bcdf23acbfd4de2900a65f95e74f4966a5)), closes [#324](https://github.com/sct/overseerr/issues/324)
|
||||
* **frontend:** adds a tip to plex setup to clarify that syncing runs in the background ([df4ac83](https://github.com/sct/overseerr/commit/df4ac8361f82971ee845f3be217408a9123a0bf3)), closes [#325](https://github.com/sct/overseerr/issues/325)
|
||||
* **frontend:** aligned movie and tv details ([#331](https://github.com/sct/overseerr/issues/331)) ([db0a5c4](https://github.com/sct/overseerr/commit/db0a5c44f678e76eee7f5582381016306d1f46a2))
|
||||
* **frontend:** close sidebar when clicking outside ([#333](https://github.com/sct/overseerr/issues/333)) ([6d7907e](https://github.com/sct/overseerr/commit/6d7907e844a909993d185759d660632f55aeaa35))
|
||||
* spelling mistake on the word 'requested' fixed ([#319](https://github.com/sct/overseerr/issues/319)) ([961d110](https://github.com/sct/overseerr/commit/961d1107208069a6fc820a1ba97ffda7336677cb))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add version to startup logs ([2948f93](https://github.com/sct/overseerr/commit/2948f9360eb484d1d6c0740a840135ca97e7240a))
|
||||
* **frontend:** temporary logs page to clear up confusion about it 404ing ([d9788c4](https://github.com/sct/overseerr/commit/d9788c4aa9f87e2eda3f7e3f1adc985f16039552)), closes [#272](https://github.com/sct/overseerr/issues/272)
|
||||
* **lang:** add support for Spanish language ([6cd2049](https://github.com/sct/overseerr/commit/6cd20491d2a0ceb995c4744eeb92a6e2f57a4893))
|
||||
* **lang:** Translations update from Weblate ([#291](https://github.com/sct/overseerr/issues/291)) ([fddbb3c](https://github.com/sct/overseerr/commit/fddbb3cdfe3d50b2835c248556139c769dc2b805))
|
||||
|
||||
# [1.5.0](https://github.com/sct/overseerr/compare/v1.4.0...v1.5.0) (2020-12-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** require package.json directly so typescript doesnt compile it into dist folder ([b9faa64](https://github.com/sct/overseerr/commit/b9faa6486b35aa865019aa8af9d307531054bc1d))
|
||||
* **frontend:** add validation for Radarr/Sonarr server name ([b5988f9](https://github.com/sct/overseerr/commit/b5988f9a5ff274e97f208c2726abe76c22c858ee))
|
||||
* **frontend:** only show alpha notice to admins ([ff61895](https://github.com/sct/overseerr/commit/ff618956b5d9cf933d867ea979b612c3d8a6f30b))
|
||||
* add support for ssl when connecting to plex ([3ba09d0](https://github.com/sct/overseerr/commit/3ba09d07eb0367c41603cd55e7ff41c66fb641c4)), closes [#275](https://github.com/sct/overseerr/issues/275)
|
||||
* **services:** improve logging for when Radarr movie already exists ([#285](https://github.com/sct/overseerr/issues/285)) ([f998873](https://github.com/sct/overseerr/commit/f998873fc5669a547901f2733c9c785d744d27ca)), closes [#260](https://github.com/sct/overseerr/issues/260)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** add i18n strings for new about page ([900827b](https://github.com/sct/overseerr/commit/900827be97845688e4bea72a8c5d9611a3e9d069))
|
||||
* about page initial version ([3f2a04c](https://github.com/sct/overseerr/commit/3f2a04c881bf06b73a952181fa463af84454b0dd))
|
||||
|
||||
# [1.4.0](https://github.com/sct/overseerr/compare/v1.3.2...v1.4.0) (2020-12-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* changing parameter name to use correct 'port' [#276](https://github.com/sct/overseerr/issues/276) ([#277](https://github.com/sct/overseerr/issues/277)) ([6d08b10](https://github.com/sct/overseerr/commit/6d08b108200177ca3068c852e60a0df75ce2232a))
|
||||
* **services:** include radarr/sonarr baseUrl when adding media ([78af1a3](https://github.com/sct/overseerr/commit/78af1a3e6d00a5645a05e7bf3cf56a59439b6cc9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** Translations update from Weblate ([#240](https://github.com/sct/overseerr/issues/240)) ([e17c637](https://github.com/sct/overseerr/commit/e17c63748362b6a480693e003ef5eec614dcec43))
|
||||
|
||||
## [1.3.2](https://github.com/sct/overseerr/compare/v1.3.1...v1.3.2) (2020-12-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** convert plex port to a number before posting to the api ([8cb05c4](https://github.com/sct/overseerr/commit/8cb05c413a15a4b74e37ece5e24367d115995b32))
|
||||
* **frontend:** converts email smtp port to a number before posting to the api ([2098a2d](https://github.com/sct/overseerr/commit/2098a2d3d2981fd2ae54392aec3ef81327f2858e)), closes [#251](https://github.com/sct/overseerr/issues/251)
|
||||
* **frontend:** encode special characters in search input to prevent crashing router ([15013d6](https://github.com/sct/overseerr/commit/15013d6c5dbff15704c7c30d261d68a265e7f2d7)), closes [#252](https://github.com/sct/overseerr/issues/252)
|
||||
* **plex sync:** catch errors that occur during processMovie ([edbbccf](https://github.com/sct/overseerr/commit/edbbccf3ae623430294f1a5c3fd2728dbd42e555)), closes [#244](https://github.com/sct/overseerr/issues/244) [#246](https://github.com/sct/overseerr/issues/246) [#250](https://github.com/sct/overseerr/issues/250)
|
||||
* **services:** improve logging for adding movies to Radarr ([6c1ee83](https://github.com/sct/overseerr/commit/6c1ee830a183f89bb1fe96a181a7d61684e23b22))
|
||||
* **services:** radarr/sonarr will use the correct default server ([0658b79](https://github.com/sct/overseerr/commit/0658b7943e1ab25816db9da34d4c9ea808d9203d))
|
||||
|
||||
## [1.3.1](https://github.com/sct/overseerr/compare/v1.3.0...v1.3.1) (2020-12-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** also convert activeProfileId to a number for radarr/sonarr submissions ([7bf924f](https://github.com/sct/overseerr/commit/7bf924f7e94a0e0834f41b4ec067ed277c652766))
|
||||
* **frontend:** also convert ports to numbers when saving radarr/sonarr servers ([c53dc3b](https://github.com/sct/overseerr/commit/c53dc3b15da522c6e6ab76bbc9d15008a8a9fb9d))
|
||||
* **frontend:** new radarr/sonarr ports will be converted to a number before posting ([92c9001](https://github.com/sct/overseerr/commit/92c9001c9d1f2cbd272a5897ea1157d2cadbce2d))
|
||||
|
||||
# [1.3.0](https://github.com/sct/overseerr/compare/v1.2.0...v1.3.0) (2020-12-14)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ You can also run the development environment in [Docker](https://www.docker.com/
|
||||
- PRs with commits not following this standard will not be merged.
|
||||
- Please make meaningful commits, or squash them
|
||||
- Always rebase your commit to the latest `develop` branch. Do not merge develop into your branch.
|
||||
- It is your responsbility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch.
|
||||
- It is your responsibility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch.
|
||||
- You can create a Draft pull request early to get feedback on your work.
|
||||
- Your code must be formatted correctly or the tests will fail.
|
||||
- We use Prettier to format our codebase. It should auto run with a git hook, but its recommended to have a Prettier extension installed in your editor and have it format on save.
|
||||
|
||||
@@ -12,6 +12,11 @@ RUN yarn cache clean
|
||||
|
||||
FROM node:12.18-alpine
|
||||
|
||||
ARG COMMIT_TAG
|
||||
ENV COMMIT_TAG=${COMMIT_TAG}
|
||||
|
||||
RUN apk add tzdata
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
@@ -21,3 +26,5 @@ COPY --from=BUILD_IMAGE /app/.next ./.next
|
||||
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
|
||||
|
||||
CMD yarn start
|
||||
|
||||
EXPOSE 5055
|
||||
|
||||
38
README.md
38
README.md
@@ -16,7 +16,7 @@
|
||||
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr">
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-7-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-20-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
</p>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
- More notification types (Slack/Telegram/etc.).
|
||||
- Issues system. This will allow users to report issues with content on your media server.
|
||||
- Local user system (for those who don't use Plex).
|
||||
- Compatiblity APIs (to work with existing tools in your system).
|
||||
- Compatibility APIs (to work with existing tools in your system).
|
||||
|
||||
## Running Overseerr
|
||||
|
||||
@@ -54,7 +54,7 @@ Currently, Overseerr is only distributed through Docker images. If you have Dock
|
||||
docker run -d \
|
||||
-e LOG_LEVEL=info \
|
||||
-e TZ=Asia/Tokyo \
|
||||
-p 5055:3000 \
|
||||
-p 5055:5055 \
|
||||
-v /path/to/appdata/config:/app/config \
|
||||
--restart unless-stopped \
|
||||
sctx/overseerr
|
||||
@@ -76,7 +76,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
|
||||
|
||||
## API Documentation
|
||||
|
||||
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:3000/api-docs
|
||||
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:5055/api-docs
|
||||
|
||||
## Community
|
||||
|
||||
@@ -86,7 +86,13 @@ If you would like to chat with community members you can join the [Overseerr Dis
|
||||
|
||||
Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Overseerr community channels.
|
||||
|
||||
## Contributors
|
||||
## Contributing
|
||||
|
||||
You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
@@ -101,13 +107,25 @@ Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_COND
|
||||
<td align="center"><a href="https://github.com/GiyomuWeb"><img src="https://avatars0.githubusercontent.com/u/62489209?v=4" width="100px;" alt=""/><br /><sub><b>GiyomuWeb</b></sub></a><br /><a href="#translation-GiyomuWeb" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/angrycuban13"><img src="https://avatars3.githubusercontent.com/u/39564898?v=4" width="100px;" alt=""/><br /><sub><b>Angry Cuban</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=angrycuban13" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4" width="100px;" alt=""/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4" width="100px;" alt=""/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/saltydk"><img src="https://avatars1.githubusercontent.com/u/6587950?v=4" width="100px;" alt=""/><br /><sub><b>salty</b></sub></a><br /><a href="#infra-saltydk" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/Shutruk"><img src="https://avatars2.githubusercontent.com/u/9198633?v=4" width="100px;" alt=""/><br /><sub><b>Shutruk</b></sub></a><br /><a href="#translation-Shutruk" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/krystiancharubin"><img src="https://avatars2.githubusercontent.com/u/17775600?v=4" width="100px;" alt=""/><br /><sub><b>Krystian Charubin</b></sub></a><br /><a href="#design-krystiancharubin" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/kieron"><img src="https://avatars2.githubusercontent.com/u/8655212?v=4" width="100px;" alt=""/><br /><sub><b>Kieron Boswell</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=kieron" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/samwiseg0"><img src="https://avatars1.githubusercontent.com/u/2241731?v=4" width="100px;" alt=""/><br /><sub><b>samwiseg0</b></sub></a><br /><a href="#question-samwiseg0" title="Answering Questions">💬</a> <a href="#infra-samwiseg0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/ecelebi29"><img src="https://avatars2.githubusercontent.com/u/8337120?v=4" width="100px;" alt=""/><br /><sub><b>ecelebi29</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mmozeiko"><img src="https://avatars3.githubusercontent.com/u/1665010?v=4" width="100px;" alt=""/><br /><sub><b>Mārtiņš Možeiko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=mmozeiko" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mazzetta86"><img src="https://avatars2.githubusercontent.com/u/45591560?v=4" width="100px;" alt=""/><br /><sub><b>mazzetta86</b></sub></a><br /><a href="#translation-mazzetta86" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/Panzer1119"><img src="https://avatars1.githubusercontent.com/u/23016343?v=4" width="100px;" alt=""/><br /><sub><b>Paul Hagedorn</b></sub></a><br /><a href="#translation-Panzer1119" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/Shagon94"><img src="https://avatars3.githubusercontent.com/u/9140783?v=4" width="100px;" alt=""/><br /><sub><b>Shagon94</b></sub></a><br /><a href="#translation-Shagon94" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/sebstrgg"><img src="https://avatars3.githubusercontent.com/u/27026694?v=4" width="100px;" alt=""/><br /><sub><b>sebstrgg</b></sub></a><br /><a href="#translation-sebstrgg" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## Contributing
|
||||
|
||||
You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile.local
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 5055:5055
|
||||
volumes:
|
||||
- .:/app:rw,cached
|
||||
- /app/node_modules
|
||||
|
||||
@@ -5,6 +5,7 @@ const devConfig = {
|
||||
logging: false,
|
||||
entities: ['server/entity/**/*.ts'],
|
||||
migrations: ['server/migration/**/*.ts'],
|
||||
subscribers: ['server/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: 'server/entity',
|
||||
migrationsDir: 'server/migration',
|
||||
@@ -19,6 +20,7 @@ const prodConfig = {
|
||||
entities: ['dist/entity/**/*.js'],
|
||||
migrations: ['dist/migration/**/*.js'],
|
||||
migrationsRun: true,
|
||||
subscribers: ['dist/subscriber/**/*.js'],
|
||||
cli: {
|
||||
entitiesDir: 'dist/entity',
|
||||
migrationsDir: 'dist/migration',
|
||||
|
||||
@@ -436,12 +436,7 @@ components:
|
||||
spokenLanguages:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
iso_639_1:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SpokenLanguage'
|
||||
status:
|
||||
type: string
|
||||
tagline:
|
||||
@@ -592,6 +587,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ProductionCompany'
|
||||
spokenLanguages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SpokenLanguage'
|
||||
seasons:
|
||||
type: array
|
||||
items:
|
||||
@@ -617,6 +616,10 @@ components:
|
||||
$ref: '#/components/schemas/Crew'
|
||||
externalIds:
|
||||
$ref: '#/components/schemas/ExternalIds'
|
||||
keywords:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Keyword'
|
||||
mediaInfo:
|
||||
$ref: '#/components/schemas/MediaInfo'
|
||||
MediaRequest:
|
||||
@@ -643,7 +646,6 @@ components:
|
||||
readOnly: true
|
||||
requestedBy:
|
||||
$ref: '#/components/schemas/User'
|
||||
readOnly: true
|
||||
modifiedBy:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/User'
|
||||
@@ -962,17 +964,43 @@ components:
|
||||
type: string
|
||||
mediaInfo:
|
||||
$ref: '#/components/schemas/MediaInfo'
|
||||
Keyword:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: 'anime'
|
||||
SpokenLanguage:
|
||||
type: object
|
||||
properties:
|
||||
englishName:
|
||||
type: string
|
||||
example: 'English'
|
||||
nullable: true
|
||||
iso_639_1:
|
||||
type: string
|
||||
example: 'en'
|
||||
name:
|
||||
type: string
|
||||
example: 'English'
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
name: connect.sid
|
||||
in: cookie
|
||||
apiKey:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-Api-Key
|
||||
|
||||
paths:
|
||||
/settings/main:
|
||||
get:
|
||||
summary: Returns main settings
|
||||
description: Retreives all main settings in JSON format
|
||||
description: Retrieves all main settings in JSON format
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
@@ -1000,6 +1028,19 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MainSettings'
|
||||
/settings/main/regenerate:
|
||||
get:
|
||||
summary: Returns main settings with newly generated API Key
|
||||
description: Retreives all main settings in JSON format with new API Key
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MainSettings'
|
||||
/settings/plex:
|
||||
get:
|
||||
summary: Returns plex settings
|
||||
@@ -1137,7 +1178,7 @@ paths:
|
||||
/settings/radarr/test:
|
||||
post:
|
||||
summary: Test radarr configuration
|
||||
description: Test if the provided Radarr congifuration values are valid. Returns profiles and root folders on success
|
||||
description: Test if the provided Radarr configuration values are valid. Returns profiles and root folders on success
|
||||
tags:
|
||||
- settings
|
||||
requestBody:
|
||||
@@ -1281,7 +1322,7 @@ paths:
|
||||
/settings/sonarr/test:
|
||||
post:
|
||||
summary: Test Sonarr configuration
|
||||
description: Test if the provided Sonarr congifuration values are valid. Returns profiles and root folders on success
|
||||
description: Test if the provided Sonarr configuration values are valid. Returns profiles and root folders on success
|
||||
tags:
|
||||
- settings
|
||||
requestBody:
|
||||
@@ -1384,7 +1425,7 @@ paths:
|
||||
/settings/initialize:
|
||||
get:
|
||||
summary: Set the application as initialized
|
||||
description: Sets the app as initalized and allows the user to navigate to pages other than the setup page
|
||||
description: Sets the app as initialized and allows the user to navigate to pages other than the setup page
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
@@ -1478,6 +1519,33 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DiscordSettings'
|
||||
/settings/about:
|
||||
get:
|
||||
summary: Return current about stats
|
||||
description: Returns current server stats in JSON format
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
'200':
|
||||
description: Returned about settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
example: '1.0.0'
|
||||
totalRequests:
|
||||
type: number
|
||||
example: 100
|
||||
totalMediaItems:
|
||||
type: number
|
||||
example: 100
|
||||
tz:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Asia/Tokyo
|
||||
/auth/me:
|
||||
get:
|
||||
summary: Returns the currently logged in user
|
||||
@@ -1844,6 +1912,51 @@ paths:
|
||||
- $ref: '#/components/schemas/MovieResult'
|
||||
- $ref: '#/components/schemas/TvResult'
|
||||
- $ref: '#/components/schemas/PersonResult'
|
||||
/discover/keyword/{keywordId}/movies:
|
||||
get:
|
||||
summary: Request list of movies from keyword
|
||||
description: Returns list of movies based on provided keyword ID in JSON format
|
||||
tags:
|
||||
- search
|
||||
parameters:
|
||||
- in: path
|
||||
name: keywordId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
example: 207317
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: number
|
||||
example: 1
|
||||
default: 1
|
||||
- in: query
|
||||
name: language
|
||||
schema:
|
||||
type: string
|
||||
example: en
|
||||
responses:
|
||||
'200':
|
||||
description: List of movies
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: number
|
||||
example: 1
|
||||
totalPages:
|
||||
type: number
|
||||
example: 20
|
||||
totalResults:
|
||||
type: number
|
||||
example: 200
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MovieResult'
|
||||
/request:
|
||||
get:
|
||||
summary: Get all requests
|
||||
@@ -2463,3 +2576,4 @@ paths:
|
||||
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- apiKey: []
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "overseerr",
|
||||
"version": "1.3.0",
|
||||
"version": "1.11.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts",
|
||||
@@ -11,6 +11,7 @@
|
||||
"start": "NODE_ENV=production node dist/index.js",
|
||||
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault false './src/**/!(*.test).{ts,tsx}'",
|
||||
"migration:generate": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:generate",
|
||||
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:create",
|
||||
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:run",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
@@ -39,6 +40,7 @@
|
||||
"react-dom": "17.0.1",
|
||||
"react-intersection-observer": "^8.31.0",
|
||||
"react-intl": "^5.10.6",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-toast-notifications": "^2.4.0",
|
||||
"react-transition-group": "^4.4.1",
|
||||
@@ -170,7 +172,13 @@
|
||||
"prepareCmd": "docker build -t sctx/overseerr ."
|
||||
}
|
||||
],
|
||||
"semantic-release-docker"
|
||||
"semantic-release-docker",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"addReleases": "bottom"
|
||||
}
|
||||
]
|
||||
],
|
||||
"branches": [
|
||||
"master"
|
||||
@@ -180,7 +188,8 @@
|
||||
{
|
||||
"path": "semantic-release-docker",
|
||||
"name": "sctx/overseerr"
|
||||
}
|
||||
},
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#1e2937","display":"standalone"}
|
||||
|
||||
@@ -4,10 +4,12 @@ import { getSettings } from '../lib/settings';
|
||||
export interface PlexLibraryItem {
|
||||
ratingKey: string;
|
||||
parentRatingKey?: string;
|
||||
grandparentRatingKey?: string;
|
||||
title: string;
|
||||
guid: string;
|
||||
parentGuid?: string;
|
||||
type: 'movie' | 'show' | 'season';
|
||||
grandparentGuid?: string;
|
||||
type: 'movie' | 'show' | 'season' | 'episode';
|
||||
}
|
||||
|
||||
interface PlexLibraryResponse {
|
||||
@@ -20,6 +22,7 @@ export interface PlexLibrary {
|
||||
type: 'show' | 'movie';
|
||||
key: string;
|
||||
title: string;
|
||||
agent: string;
|
||||
}
|
||||
|
||||
interface PlexLibrariesResponse {
|
||||
@@ -61,7 +64,8 @@ class PlexAPI {
|
||||
|
||||
this.plexClient = new NodePlexAPI({
|
||||
hostname: settings.plex.ip,
|
||||
post: settings.plex.port,
|
||||
port: settings.plex.port,
|
||||
https: settings.plex.useSsl,
|
||||
token: plexToken,
|
||||
authenticator: {
|
||||
authenticate: (
|
||||
@@ -119,9 +123,9 @@ class PlexAPI {
|
||||
return response.MediaContainer.Metadata[0];
|
||||
}
|
||||
|
||||
public async getRecentlyAdded(): Promise<PlexLibraryItem[]> {
|
||||
public async getRecentlyAdded(id: string): Promise<PlexLibraryItem[]> {
|
||||
const response = await this.plexClient.query<PlexLibraryResponse>(
|
||||
'/library/recentlyAdded'
|
||||
`/library/sections/${id}/recentlyAdded`
|
||||
);
|
||||
|
||||
return response.MediaContainer.Metadata;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Axios, { AxiosInstance } from 'axios';
|
||||
import logger from '../logger';
|
||||
|
||||
interface RadarrMovieOptions {
|
||||
title: string;
|
||||
@@ -75,11 +76,9 @@ class RadarrAPI {
|
||||
}
|
||||
};
|
||||
|
||||
public addMovie = async (
|
||||
options: RadarrMovieOptions
|
||||
): Promise<RadarrMovie> => {
|
||||
public addMovie = async (options: RadarrMovieOptions): Promise<void> => {
|
||||
try {
|
||||
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
||||
await this.axios.post<RadarrMovie>(`/movie`, {
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
profileId: options.profileId,
|
||||
@@ -93,10 +92,15 @@ class RadarrAPI {
|
||||
searchForMovie: options.searchNow,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to add movie: ${e.message}`);
|
||||
logger.error(
|
||||
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
||||
{
|
||||
label: 'Radarr',
|
||||
errorMessage: e.message,
|
||||
options,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class RottenTomatoes {
|
||||
|
||||
public async getTVRatings(
|
||||
name: string,
|
||||
year: number
|
||||
year?: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMultiSearchResponse>(
|
||||
@@ -126,9 +126,13 @@ class RottenTomatoes {
|
||||
}
|
||||
);
|
||||
|
||||
const tvshow = response.data.tvSeries.find(
|
||||
(series) => series.startYear === year
|
||||
);
|
||||
let tvshow: RTTvSearchResult | undefined = response.data.tvSeries[0];
|
||||
|
||||
if (year) {
|
||||
tvshow = response.data.tvSeries.find(
|
||||
(series) => series.startYear === year
|
||||
);
|
||||
}
|
||||
|
||||
if (!tvshow) {
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,7 @@ interface SonarrSeason {
|
||||
monitored: boolean;
|
||||
}
|
||||
|
||||
interface SonarrSeries {
|
||||
export interface SonarrSeries {
|
||||
title: string;
|
||||
sortTitle: string;
|
||||
seasonCount: number;
|
||||
@@ -33,7 +33,7 @@ interface SonarrSeries {
|
||||
tvMazeId: number;
|
||||
firstAired: string;
|
||||
lastInfoSync?: string;
|
||||
seriesType: string;
|
||||
seriesType: 'standard' | 'daily' | 'anime';
|
||||
cleanTitle: string;
|
||||
imdbId: string;
|
||||
titleSlug: string;
|
||||
@@ -76,7 +76,9 @@ interface AddSeriesOptions {
|
||||
title: string;
|
||||
profileId: number;
|
||||
seasons: number[];
|
||||
seasonFolder: boolean;
|
||||
rootFolderPath: string;
|
||||
seriesType: SonarrSeries['seriesType'];
|
||||
monitored?: boolean;
|
||||
searchNow?: boolean;
|
||||
}
|
||||
@@ -149,8 +151,10 @@ class SonarrAPI {
|
||||
monitored: false,
|
||||
}))
|
||||
),
|
||||
seasonFolder: options.seasonFolder,
|
||||
monitored: options.monitored,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
seriesType: options.seriesType,
|
||||
addOptions: {
|
||||
ignoreEpisodesWithFiles: true,
|
||||
searchForMissingEpisodes: options.searchNow,
|
||||
@@ -162,7 +166,7 @@ class SonarrAPI {
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong adding a series to Sonarr', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
errorMessage: e.message,
|
||||
error: e,
|
||||
});
|
||||
throw new Error('Failed to add series');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export const ANIME_KEYWORD_ID = 210024;
|
||||
|
||||
interface SearchOptions {
|
||||
query: string;
|
||||
page?: number;
|
||||
@@ -258,6 +260,11 @@ export interface TmdbTvDetails {
|
||||
name: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
spoken_languages: {
|
||||
english_name: string;
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
seasons: TmdbTvSeasonResult[];
|
||||
status: string;
|
||||
type: string;
|
||||
@@ -268,6 +275,14 @@ export interface TmdbTvDetails {
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
keywords: {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TmdbKeyword {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonDetail {
|
||||
@@ -437,7 +452,10 @@ class TheMovieDb {
|
||||
}): Promise<TmdbTvDetails> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbTvDetails>(`/tv/${tvId}`, {
|
||||
params: { language, append_to_response: 'credits,external_ids' },
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,keywords',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
@@ -524,6 +542,32 @@ class TheMovieDb {
|
||||
}
|
||||
}
|
||||
|
||||
public async getMoviesByKeyword({
|
||||
keywordId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
keywordId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/keyword/${keywordId}/movies`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvRecommendations({
|
||||
tvId,
|
||||
page = 1,
|
||||
|
||||
@@ -8,14 +8,11 @@ import {
|
||||
UpdateDateColumn,
|
||||
getRepository,
|
||||
In,
|
||||
AfterUpdate,
|
||||
} from 'typeorm';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import logger from '../logger';
|
||||
import Season from './Season';
|
||||
import notificationManager, { Notification } from '../lib/notifications';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
|
||||
@Entity()
|
||||
class Media {
|
||||
@@ -98,32 +95,6 @@ class Media {
|
||||
constructor(init?: Partial<Media>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterUpdate()
|
||||
private async _notifyAvailable() {
|
||||
if (this.status === MediaStatus.AVAILABLE) {
|
||||
if (this.mediaType === MediaType.MOVIE) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const relatedRequests = await requestRepository.find({
|
||||
where: { media: this },
|
||||
});
|
||||
|
||||
if (relatedRequests.length > 0) {
|
||||
const tmdb = new TheMovieDb();
|
||||
const movie = await tmdb.getMovie({ movieId: this.tmdbId });
|
||||
|
||||
relatedRequests.forEach((request) => {
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
notifyUser: request.requestedBy,
|
||||
subject: movie.title,
|
||||
message: movie.overview,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Media;
|
||||
|
||||
@@ -15,11 +15,11 @@ import { User } from './User';
|
||||
import Media from './Media';
|
||||
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import TheMovieDb, { ANIME_KEYWORD_ID } from '../api/themoviedb';
|
||||
import RadarrAPI from '../api/radarr';
|
||||
import logger from '../logger';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
import SonarrAPI from '../api/sonarr';
|
||||
import SonarrAPI, { SonarrSeries } from '../api/sonarr';
|
||||
import notificationManager, { Notification } from '../lib/notifications';
|
||||
|
||||
@Entity()
|
||||
@@ -36,10 +36,18 @@ export class MediaRequest {
|
||||
})
|
||||
public media: Media;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.requests, { eager: true })
|
||||
@ManyToOne(() => User, (user) => user.requests, {
|
||||
eager: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
public requestedBy: User;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true, cascade: true, eager: true })
|
||||
@ManyToOne(() => User, {
|
||||
nullable: true,
|
||||
cascade: true,
|
||||
eager: true,
|
||||
onDelete: 'SET NULL',
|
||||
})
|
||||
public modifiedBy?: User;
|
||||
|
||||
@CreateDateColumn()
|
||||
@@ -234,13 +242,24 @@ export class MediaRequest {
|
||||
return;
|
||||
}
|
||||
|
||||
const radarrSettings = settings.radarr.find(
|
||||
(radarr) => radarr.isDefault && !radarr.is4k
|
||||
);
|
||||
|
||||
if (!radarrSettings) {
|
||||
logger.info(
|
||||
'There is no default radarr configured. Did you set any of your Radarr servers as default?',
|
||||
{ label: 'Media Request' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tmdb = new TheMovieDb();
|
||||
const radarrSettings = settings.radarr[0];
|
||||
const radarr = new RadarrAPI({
|
||||
apiKey: radarrSettings.apiKey,
|
||||
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
radarrSettings.hostname
|
||||
}:${radarrSettings.port}/api`,
|
||||
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}/api`,
|
||||
});
|
||||
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
||||
|
||||
@@ -283,6 +302,18 @@ export class MediaRequest {
|
||||
return;
|
||||
}
|
||||
|
||||
const sonarrSettings = settings.sonarr.find(
|
||||
(sonarr) => sonarr.isDefault && !sonarr.is4k
|
||||
);
|
||||
|
||||
if (!sonarrSettings) {
|
||||
logger.info(
|
||||
'There is no default sonarr configured. Did you set any of your Sonarr servers as default?',
|
||||
{ label: 'Media Request' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: this.media.id },
|
||||
relations: ['requests'],
|
||||
@@ -293,12 +324,11 @@ export class MediaRequest {
|
||||
}
|
||||
|
||||
const tmdb = new TheMovieDb();
|
||||
const sonarrSettings = settings.sonarr[0];
|
||||
const sonarr = new SonarrAPI({
|
||||
apiKey: sonarrSettings.apiKey,
|
||||
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
sonarrSettings.hostname
|
||||
}:${sonarrSettings.port}/api`,
|
||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
|
||||
});
|
||||
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
|
||||
|
||||
@@ -306,13 +336,32 @@ export class MediaRequest {
|
||||
throw new Error('Series was missing tvdb id');
|
||||
}
|
||||
|
||||
let seriesType: SonarrSeries['seriesType'] = 'standard';
|
||||
|
||||
// Change series type to anime if the anime keyword is present on tmdb
|
||||
if (
|
||||
series.keywords.results.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)
|
||||
) {
|
||||
seriesType = 'anime';
|
||||
}
|
||||
|
||||
// Run this asynchronously so we don't wait for it on the UI side
|
||||
sonarr.addSeries({
|
||||
profileId: sonarrSettings.activeProfileId,
|
||||
rootFolderPath: sonarrSettings.activeDirectory,
|
||||
profileId:
|
||||
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
|
||||
? sonarrSettings.activeAnimeProfileId
|
||||
: sonarrSettings.activeProfileId,
|
||||
rootFolderPath:
|
||||
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
|
||||
? sonarrSettings.activeAnimeDirectory
|
||||
: sonarrSettings.activeDirectory,
|
||||
title: series.name,
|
||||
tvdbid: series.external_ids.tvdb_id,
|
||||
seasons: this.seasons.map((season) => season.seasonNumber),
|
||||
seasonFolder: sonarrSettings.enableSeasonFolders,
|
||||
seriesType,
|
||||
monitored: true,
|
||||
searchNow: true,
|
||||
});
|
||||
|
||||
@@ -5,15 +5,9 @@ import {
|
||||
ManyToOne,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
AfterInsert,
|
||||
AfterUpdate,
|
||||
getRepository,
|
||||
} from 'typeorm';
|
||||
import { MediaStatus } from '../constants/media';
|
||||
import Media from './Media';
|
||||
import logger from '../logger';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import notificationManager, { Notification } from '../lib/notifications';
|
||||
|
||||
@Entity()
|
||||
class Season {
|
||||
@@ -38,60 +32,6 @@ class Season {
|
||||
constructor(init?: Partial<Season>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterInsert()
|
||||
@AfterUpdate()
|
||||
private async _sendSeasonAvailableNotification() {
|
||||
if (this.status === MediaStatus.AVAILABLE) {
|
||||
try {
|
||||
const lazyMedia = await this.media;
|
||||
const tmdb = new TheMovieDb();
|
||||
const mediaRepository = getRepository(Media);
|
||||
const media = await mediaRepository.findOneOrFail({
|
||||
where: { id: lazyMedia.id },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
const availableSeasons = media.seasons.map(
|
||||
(season) => season.seasonNumber
|
||||
);
|
||||
|
||||
const request = media.requests.find(
|
||||
(request) =>
|
||||
// Check if the season is complete AND it contains the current season that was just marked available
|
||||
request.seasons.every((season) =>
|
||||
availableSeasons.includes(season.seasonNumber)
|
||||
) &&
|
||||
request.seasons.some(
|
||||
(season) => season.seasonNumber === this.seasonNumber
|
||||
)
|
||||
);
|
||||
|
||||
if (request) {
|
||||
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
subject: tv.name,
|
||||
message: tv.overview,
|
||||
notifyUser: request.requestedBy,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
extra: [
|
||||
{
|
||||
name: 'Seasons',
|
||||
value: request.seasons
|
||||
.map((season) => season.seasonNumber)
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong sending season available notice', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Season;
|
||||
|
||||
@@ -17,9 +17,11 @@ import { startJobs } from './job/schedule';
|
||||
import notificationManager from './lib/notifications';
|
||||
import DiscordAgent from './lib/notifications/agents/discord';
|
||||
import EmailAgent from './lib/notifications/agents/email';
|
||||
import { getAppVersion } from './utils/appVersion';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
@@ -98,12 +100,21 @@ app
|
||||
}
|
||||
);
|
||||
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
server.listen(port, () => {
|
||||
logger.info(`Server ready on port ${port}`, {
|
||||
label: 'SERVER',
|
||||
const port = Number(process.env.PORT) || 5055;
|
||||
const host = process.env.HOST;
|
||||
if (host) {
|
||||
server.listen(port, host, () => {
|
||||
logger.info(`Server ready on ${host} port ${port}`, {
|
||||
label: 'Server',
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
server.listen(port, () => {
|
||||
logger.info(`Server ready on port ${port}`, {
|
||||
label: 'Server',
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err.stack);
|
||||
|
||||
6
server/interfaces/api/settingsInterfaces.ts
Normal file
6
server/interfaces/api/settingsInterfaces.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface SettingsAboutResponse {
|
||||
version: string;
|
||||
totalRequests: number;
|
||||
totalMediaItems: number;
|
||||
tz?: string;
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import logger from '../../logger';
|
||||
import { getSettings, Library } from '../../lib/settings';
|
||||
import Season from '../../entity/Season';
|
||||
import { uniqWith } from 'lodash';
|
||||
|
||||
const BUNDLE_SIZE = 10;
|
||||
const BUNDLE_SIZE = 20;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
|
||||
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
||||
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
|
||||
@@ -51,64 +53,81 @@ class JobPlexSync {
|
||||
|
||||
private async processMovie(plexitem: PlexLibraryItem) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
if (plexitem.guid.match(plexRegex)) {
|
||||
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
|
||||
const newMedia = new Media();
|
||||
try {
|
||||
if (plexitem.guid.match(plexRegex)) {
|
||||
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
|
||||
const newMedia = new Media();
|
||||
|
||||
metadata.Guid.forEach((ref) => {
|
||||
if (ref.id.match(imdbRegex)) {
|
||||
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
|
||||
} else if (ref.id.match(tmdbRegex)) {
|
||||
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
|
||||
newMedia.tmdbId = Number(tmdbMatch);
|
||||
if (!metadata.Guid) {
|
||||
logger.debug('No Guid metadata for this title. Skipping', {
|
||||
label: 'Plex Sync',
|
||||
ratingKey: plexitem.ratingKey,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const existing = await this.getExisting(newMedia.tmdbId);
|
||||
|
||||
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||
this.log(`Title exists and is already available ${metadata.title}`);
|
||||
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
||||
existing.status = MediaStatus.AVAILABLE;
|
||||
mediaRepository.save(existing);
|
||||
this.log(
|
||||
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
|
||||
'info'
|
||||
);
|
||||
} else {
|
||||
newMedia.status = MediaStatus.AVAILABLE;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${plexitem.title}`);
|
||||
}
|
||||
} else {
|
||||
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
|
||||
|
||||
if (matchedid?.[1]) {
|
||||
const tmdbMovie = await this.tmdb.getMovieByImdbId({
|
||||
imdbId: matchedid[1],
|
||||
metadata.Guid.forEach((ref) => {
|
||||
if (ref.id.match(imdbRegex)) {
|
||||
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
|
||||
} else if (ref.id.match(tmdbRegex)) {
|
||||
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
|
||||
newMedia.tmdbId = Number(tmdbMatch);
|
||||
}
|
||||
});
|
||||
|
||||
const existing = await this.getExisting(tmdbMovie.id);
|
||||
const existing = await this.getExisting(newMedia.tmdbId);
|
||||
|
||||
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||
this.log(`Title exists and is already available ${plexitem.title}`);
|
||||
this.log(`Title exists and is already available ${metadata.title}`);
|
||||
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
||||
existing.status = MediaStatus.AVAILABLE;
|
||||
await mediaRepository.save(existing);
|
||||
mediaRepository.save(existing);
|
||||
this.log(
|
||||
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
|
||||
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
|
||||
'info'
|
||||
);
|
||||
} else if (tmdbMovie) {
|
||||
const newMedia = new Media();
|
||||
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
|
||||
newMedia.tmdbId = tmdbMovie.id;
|
||||
} else {
|
||||
newMedia.status = MediaStatus.AVAILABLE;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${tmdbMovie.title}`);
|
||||
this.log(`Saved ${plexitem.title}`);
|
||||
}
|
||||
} else {
|
||||
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
|
||||
|
||||
if (matchedid?.[1]) {
|
||||
const tmdbMovie = await this.tmdb.getMovieByImdbId({
|
||||
imdbId: matchedid[1],
|
||||
});
|
||||
|
||||
const existing = await this.getExisting(tmdbMovie.id);
|
||||
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||
this.log(`Title exists and is already available ${plexitem.title}`);
|
||||
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
||||
existing.status = MediaStatus.AVAILABLE;
|
||||
await mediaRepository.save(existing);
|
||||
this.log(
|
||||
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
|
||||
'info'
|
||||
);
|
||||
} else if (tmdbMovie) {
|
||||
const newMedia = new Media();
|
||||
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
|
||||
newMedia.tmdbId = tmdbMovie.id;
|
||||
newMedia.status = MediaStatus.AVAILABLE;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${tmdbMovie.title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Failed to process plex item. ratingKey: ${
|
||||
plexitem.parentRatingKey ?? plexitem.ratingKey
|
||||
}`,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +138,9 @@ class JobPlexSync {
|
||||
|
||||
try {
|
||||
const metadata = await this.plexClient.getMetadata(
|
||||
plexitem.parentRatingKey ?? plexitem.ratingKey,
|
||||
plexitem.grandparentRatingKey ??
|
||||
plexitem.parentRatingKey ??
|
||||
plexitem.ratingKey,
|
||||
{ includeChildren: true }
|
||||
);
|
||||
if (metadata.guid.match(tvdbRegex)) {
|
||||
@@ -222,9 +243,15 @@ class JobPlexSync {
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Failed to process plex item. ratingKey: ${
|
||||
plexitem.parentRatingKey ?? plexitem.ratingKey
|
||||
plexitem.grandparentRatingKey ??
|
||||
plexitem.parentRatingKey ??
|
||||
plexitem.ratingKey
|
||||
}`,
|
||||
'error'
|
||||
'error',
|
||||
{
|
||||
errorMessage: e.message,
|
||||
plexitem,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,7 +261,11 @@ class JobPlexSync {
|
||||
slicedItems.map(async (plexitem) => {
|
||||
if (plexitem.type === 'movie') {
|
||||
await this.processMovie(plexitem);
|
||||
} else if (plexitem.type === 'show') {
|
||||
} else if (
|
||||
plexitem.type === 'show' ||
|
||||
plexitem.type === 'episode' ||
|
||||
plexitem.type === 'season'
|
||||
) {
|
||||
await this.processShow(plexitem);
|
||||
}
|
||||
})
|
||||
@@ -260,16 +291,17 @@ class JobPlexSync {
|
||||
end: end + BUNDLE_SIZE,
|
||||
});
|
||||
resolve();
|
||||
}, 5000)
|
||||
}, UPDATE_RATE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private log(
|
||||
message: string,
|
||||
level: 'info' | 'error' | 'debug' = 'debug'
|
||||
level: 'info' | 'error' | 'debug' = 'debug',
|
||||
optional?: Record<string, unknown>
|
||||
): void {
|
||||
logger[level](message, { label: 'Plex Sync' });
|
||||
logger[level](message, { label: 'Plex Sync', ...optional });
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
@@ -283,20 +315,40 @@ class JobPlexSync {
|
||||
});
|
||||
|
||||
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
||||
if (this.isRecentOnly) {
|
||||
this.currentLibrary = {
|
||||
id: '0',
|
||||
name: 'Recently Added',
|
||||
enabled: true,
|
||||
};
|
||||
this.log(`Beginning to process recently added`, 'info');
|
||||
this.items = await this.plexClient.getRecentlyAdded();
|
||||
await this.loop();
|
||||
} else {
|
||||
this.libraries = settings.plex.libraries.filter(
|
||||
(library) => library.enabled
|
||||
);
|
||||
|
||||
this.libraries = settings.plex.libraries.filter(
|
||||
(library) => library.enabled
|
||||
);
|
||||
|
||||
if (this.isRecentOnly) {
|
||||
for (const library of this.libraries) {
|
||||
this.currentLibrary = library;
|
||||
this.log(
|
||||
`Beginning to process recently added for library: ${library.name}`,
|
||||
'info'
|
||||
);
|
||||
const libraryItems = await this.plexClient.getRecentlyAdded(
|
||||
library.id
|
||||
);
|
||||
|
||||
// Bundle items up by rating keys
|
||||
this.items = uniqWith(libraryItems, (mediaA, mediaB) => {
|
||||
if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) {
|
||||
return (
|
||||
mediaA.grandparentRatingKey === mediaB.grandparentRatingKey
|
||||
);
|
||||
}
|
||||
|
||||
if (mediaA.parentRatingKey && mediaB.parentRatingKey) {
|
||||
return mediaA.parentRatingKey === mediaB.parentRatingKey;
|
||||
}
|
||||
|
||||
return mediaA.ratingKey === mediaB.ratingKey;
|
||||
});
|
||||
|
||||
await this.loop();
|
||||
}
|
||||
} else {
|
||||
for (const library of this.libraries) {
|
||||
this.currentLibrary = library;
|
||||
this.log(`Beginning to process library: ${library.name}`, 'info');
|
||||
|
||||
@@ -13,7 +13,7 @@ export const startJobs = (): void => {
|
||||
// Run recently added plex sync every 5 minutes
|
||||
scheduledJobs.push({
|
||||
name: 'Plex Recently Added Sync',
|
||||
job: schedule.scheduleJob('0 */10 * * * *', () => {
|
||||
job: schedule.scheduleJob('0 */5 * * * *', () => {
|
||||
logger.info('Starting scheduled job: Plex Recently Added Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export const startJobs = (): void => {
|
||||
// Run full plex sync every 6 hours
|
||||
scheduledJobs.push({
|
||||
name: 'Plex Full Library Sync',
|
||||
job: schedule.scheduleJob('0 0 */6 * * *', () => {
|
||||
job: schedule.scheduleJob('0 0 3 * * *', () => {
|
||||
logger.info('Starting scheduled job: Plex Full Sync', { label: 'Jobs' });
|
||||
jobPlexFullSync.run();
|
||||
}),
|
||||
|
||||
@@ -29,10 +29,13 @@ class EmailAgent implements NotificationAgent {
|
||||
host: emailSettings.smtpHost,
|
||||
port: emailSettings.smtpPort,
|
||||
secure: emailSettings.secure,
|
||||
auth: {
|
||||
user: emailSettings.authUser,
|
||||
pass: emailSettings.authPass,
|
||||
},
|
||||
auth:
|
||||
emailSettings.authUser && emailSettings.authPass
|
||||
? {
|
||||
user: emailSettings.authUser,
|
||||
pass: emailSettings.authPass,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,7 +138,7 @@ class EmailAgent implements NotificationAgent {
|
||||
to: payload.notifyUser.email,
|
||||
},
|
||||
locals: {
|
||||
body: 'Your requsested media is now available!',
|
||||
body: 'Your requested media is now available!',
|
||||
mediaName: payload.subject,
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface PlexSettings {
|
||||
machineId?: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
useSsl?: boolean;
|
||||
libraries: Library[];
|
||||
}
|
||||
|
||||
@@ -109,6 +110,7 @@ class Settings {
|
||||
name: '',
|
||||
ip: '127.0.0.1',
|
||||
port: 32400,
|
||||
useSsl: false,
|
||||
libraries: [],
|
||||
},
|
||||
radarr: [],
|
||||
@@ -211,7 +213,7 @@ class Settings {
|
||||
}
|
||||
|
||||
private generateApiKey(): string {
|
||||
return Buffer.from(`${Date.now()}${this.clientId}`).toString('base64');
|
||||
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../entity/User';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
|
||||
export const checkUser: Middleware = async (req, _res, next) => {
|
||||
if (req.session?.userId) {
|
||||
const settings = getSettings();
|
||||
if (req.header('X-API-Key') === settings.main.apiKey) {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
let userId = 1; // Work on original administrator account
|
||||
|
||||
// If a User ID is provided, we will act on that users behalf
|
||||
if (req.header('X-API-User')) {
|
||||
userId = Number(req.header('X-API-User'));
|
||||
}
|
||||
const user = await userRepository.findOne({ where: { id: userId } });
|
||||
|
||||
if (user) {
|
||||
req.user = user;
|
||||
}
|
||||
} else if (req.session?.userId) {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const user = await userRepository.findOne({
|
||||
@@ -24,7 +40,7 @@ export const isAuthenticated = (
|
||||
if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
|
||||
res.status(403).json({
|
||||
status: 403,
|
||||
error: 'You do not have permisson to access this endpoint',
|
||||
error: 'You do not have permission to access this endpoint',
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
|
||||
20
server/migration/1607928251245-DropImdbIdConstraint.ts
Normal file
20
server/migration/1607928251245-DropImdbIdConstraint.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm';
|
||||
|
||||
export class DropImdbIdConstraint1607928251245 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropUniqueConstraint(
|
||||
'media',
|
||||
'UQ_7ff2d11f6a83cb52386eaebe74b'
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createUniqueConstraint(
|
||||
'media',
|
||||
new TableUnique({
|
||||
name: 'UQ_7ff2d11f6a83cb52386eaebe74b',
|
||||
columnNames: ['imdbId'],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUserRequestDeleteCascades1608219049304
|
||||
implements MigrationInterface {
|
||||
name = 'AddUserRequestDeleteCascades1608219049304';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "media_request"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media_request"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
mapCrew,
|
||||
ExternalIds,
|
||||
mapExternalIds,
|
||||
Keyword,
|
||||
} from './common';
|
||||
import {
|
||||
TmdbTvEpisodeResult,
|
||||
@@ -45,6 +46,12 @@ export interface SeasonWithEpisodes extends Season {
|
||||
externalIds: ExternalIds;
|
||||
}
|
||||
|
||||
interface SpokenLanguage {
|
||||
englishName: string;
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TvDetails {
|
||||
id: number;
|
||||
backdropPath?: string;
|
||||
@@ -56,7 +63,7 @@ export interface TvDetails {
|
||||
profilePath?: string;
|
||||
}[];
|
||||
episodeRunTime: number[];
|
||||
firstAirDate: string;
|
||||
firstAirDate?: string;
|
||||
genres: Genre[];
|
||||
homepage: string;
|
||||
inProduction: boolean;
|
||||
@@ -74,6 +81,7 @@ export interface TvDetails {
|
||||
overview: string;
|
||||
popularity: number;
|
||||
productionCompanies: ProductionCompany[];
|
||||
spokenLanguages: SpokenLanguage[];
|
||||
seasons: Season[];
|
||||
status: string;
|
||||
type: string;
|
||||
@@ -84,6 +92,7 @@ export interface TvDetails {
|
||||
crew: Crew[];
|
||||
};
|
||||
externalIds: ExternalIds;
|
||||
keywords: Keyword[];
|
||||
mediaInfo?: Media;
|
||||
}
|
||||
|
||||
@@ -161,6 +170,11 @@ export const mapTvDetails = (
|
||||
originCountry: company.origin_country,
|
||||
logoPath: company.logo_path,
|
||||
})),
|
||||
spokenLanguages: show.spoken_languages.map((language) => ({
|
||||
englishName: language.english_name,
|
||||
iso_639_1: language.iso_639_1,
|
||||
name: language.name,
|
||||
})),
|
||||
seasons: show.seasons.map(mapSeasonResult),
|
||||
status: show.status,
|
||||
type: show.type,
|
||||
@@ -179,5 +193,9 @@ export const mapTvDetails = (
|
||||
crew: show.credits.crew.map(mapCrew),
|
||||
},
|
||||
externalIds: mapExternalIds(show.external_ids),
|
||||
keywords: show.keywords.results.map((keyword) => ({
|
||||
id: keyword.id,
|
||||
name: keyword.name,
|
||||
})),
|
||||
mediaInfo: media,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,11 @@ export interface ProductionCompany {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Keyword {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Genre {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -14,7 +14,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
||||
return res.status(500).json({
|
||||
status: 500,
|
||||
error:
|
||||
'Requsted user endpoint withuot valid authenticated user in session',
|
||||
'Requested user endpoint without valid authenticated user in session',
|
||||
});
|
||||
}
|
||||
const user = await userRepository.findOneOrFail({
|
||||
@@ -24,7 +24,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
||||
return res.status(200).json(user.filter());
|
||||
});
|
||||
|
||||
authRoutes.post('/login', async (req, res) => {
|
||||
authRoutes.post('/login', async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const body = req.body as { authToken?: string };
|
||||
|
||||
@@ -86,6 +86,22 @@ authRoutes.post('/login', async (req, res) => {
|
||||
avatar: account.thumb,
|
||||
});
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
logger.info(
|
||||
'Failed login attempt from user without access to plex server',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
...account,
|
||||
authentication_token: '__REDACTED__',
|
||||
authToken: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have access to this Plex server',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +113,10 @@ authRoutes.post('/login', async (req, res) => {
|
||||
return res.status(200).json(user?.filter() ?? {});
|
||||
} catch (e) {
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: 'Something went wrong. Is your auth token valid?' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong. Is your auth token valid?',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -121,4 +121,36 @@ discoverRoutes.get('/trending', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
discoverRoutes.get<{ keywordId: string }>(
|
||||
'/keyword/:keywordId/movies',
|
||||
async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const data = await tmdb.getMoviesByKeyword({
|
||||
keywordId: Number(req.params.keywordId),
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
results: data.results.map((result) =>
|
||||
mapMovieResult(
|
||||
result,
|
||||
media.find(
|
||||
(req) =>
|
||||
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default discoverRoutes;
|
||||
|
||||
@@ -17,19 +17,34 @@ import { scheduledJobs } from '../job/schedule';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import { merge } from 'lodash';
|
||||
import Media from '../entity/Media';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import { getAppVersion } from '../utils/appVersion';
|
||||
import { SettingsAboutResponse } from '../interfaces/api/settingsInterfaces';
|
||||
|
||||
const settingsRoutes = Router();
|
||||
|
||||
settingsRoutes.get('/main', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
if (!req.user?.hasPermission(Permission.ADMIN)) {
|
||||
return res.status(200).json({
|
||||
applicationUrl: settings.main.applicationUrl,
|
||||
} as Partial<MainSettings>);
|
||||
const filteredMainSettings = (
|
||||
user: User,
|
||||
main: MainSettings
|
||||
): Partial<MainSettings> => {
|
||||
if (!user?.hasPermission(Permission.ADMIN)) {
|
||||
return {
|
||||
applicationUrl: main.applicationUrl,
|
||||
};
|
||||
}
|
||||
|
||||
res.status(200).json(settings.main);
|
||||
return main;
|
||||
};
|
||||
|
||||
settingsRoutes.get('/main', (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
if (!req.user) {
|
||||
return next({ status: 500, message: 'User missing from request' });
|
||||
}
|
||||
|
||||
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||
});
|
||||
|
||||
settingsRoutes.post('/main', (req, res) => {
|
||||
@@ -41,6 +56,18 @@ settingsRoutes.post('/main', (req, res) => {
|
||||
return res.status(200).json(settings.main);
|
||||
});
|
||||
|
||||
settingsRoutes.get('/main/regenerate', (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const main = settings.regenerateApiKey();
|
||||
|
||||
if (!req.user) {
|
||||
return next({ status: 500, message: 'User missing from request' });
|
||||
}
|
||||
|
||||
return res.status(200).json(filteredMainSettings(req.user, main));
|
||||
});
|
||||
|
||||
settingsRoutes.get('/plex', (_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
@@ -91,17 +118,22 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
|
||||
const libraries = await plexapi.getLibraries();
|
||||
|
||||
const newLibraries: Library[] = libraries.map((library) => {
|
||||
const existing = settings.plex.libraries.find(
|
||||
(l) => l.id === library.key
|
||||
);
|
||||
const newLibraries: Library[] = libraries
|
||||
// Remove libraries that are not movie or show
|
||||
.filter((library) => library.type === 'movie' || library.type === 'show')
|
||||
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
|
||||
.filter((library) => library.agent !== 'com.plexapp.agents.none')
|
||||
.map((library) => {
|
||||
const existing = settings.plex.libraries.find(
|
||||
(l) => l.id === library.key && l.name === library.title
|
||||
);
|
||||
|
||||
return {
|
||||
id: library.key,
|
||||
name: library.title,
|
||||
enabled: existing?.enabled ?? false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
id: library.key,
|
||||
name: library.title,
|
||||
enabled: existing?.enabled ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
settings.plex.libraries = newLibraries;
|
||||
}
|
||||
@@ -431,4 +463,19 @@ settingsRoutes.post('/notifications/email', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.email);
|
||||
});
|
||||
|
||||
settingsRoutes.get('/about', async (req, res) => {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const mediaRequestRepository = getRepository(MediaRequest);
|
||||
|
||||
const totalMediaItems = await mediaRepository.count();
|
||||
const totalRequests = await mediaRequestRepository.count();
|
||||
|
||||
return res.status(200).json({
|
||||
version: getAppVersion(),
|
||||
totalMediaItems,
|
||||
totalRequests,
|
||||
tz: process.env.TZ,
|
||||
} as SettingsAboutResponse);
|
||||
});
|
||||
|
||||
export default settingsRoutes;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
|
||||
import { mapTvResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
import logger from '../logger';
|
||||
|
||||
const tvRoutes = Router();
|
||||
|
||||
@@ -19,6 +20,10 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
||||
|
||||
return res.status(200).json(mapTvDetails(tv, media));
|
||||
} catch (e) {
|
||||
logger.error('Failed to get tv show', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({ status: 404, message: 'TV Show does not exist' });
|
||||
}
|
||||
});
|
||||
@@ -101,7 +106,7 @@ tvRoutes.get('/:id/ratings', async (req, res, next) => {
|
||||
|
||||
const rtratings = await rtapi.getTVRatings(
|
||||
tv.name,
|
||||
Number(tv.first_air_date.slice(0, 4))
|
||||
tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined
|
||||
);
|
||||
|
||||
if (!rtratings) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import { User } from '../entity/User';
|
||||
import { hasPermission, Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -94,13 +96,49 @@ router.delete<{ id: string }>('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const user = await userRepository.findOneOrFail({
|
||||
const user = await userRepository.findOne({
|
||||
where: { id: Number(req.params.id) },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return next({ status: 404, message: 'User not found' });
|
||||
}
|
||||
|
||||
if (user.id === 1) {
|
||||
return next({ status: 405, message: 'This account cannot be deleted.' });
|
||||
}
|
||||
|
||||
if (user.hasPermission(Permission.ADMIN)) {
|
||||
return next({
|
||||
status: 405,
|
||||
message: 'You cannot delete users with administrative privileges.',
|
||||
});
|
||||
}
|
||||
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
/**
|
||||
* Requests are usually deleted through a cascade constraint. Those however, do
|
||||
* not trigger the removal event so listeners to not run and the parent Media
|
||||
* will not be updated back to unknown for titles that were still pending. So
|
||||
* we manually remove all requests from the user here so the parent media's
|
||||
* properly reflect the change.
|
||||
*/
|
||||
await requestRepository.remove(user.requests);
|
||||
|
||||
await userRepository.delete(user.id);
|
||||
return res.status(200).json(user.filter());
|
||||
} catch (e) {
|
||||
next({ status: 404, message: 'User not found' });
|
||||
logger.error('Something went wrong deleting a user', {
|
||||
label: 'API',
|
||||
userId: req.params.id,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong deleting the user',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
112
server/subscriber/MediaSubscriber.ts
Normal file
112
server/subscriber/MediaSubscriber.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
EntitySubscriberInterface,
|
||||
EventSubscriber,
|
||||
getRepository,
|
||||
UpdateEvent,
|
||||
} from 'typeorm';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import notificationManager, { Notification } from '../lib/notifications';
|
||||
|
||||
@EventSubscriber()
|
||||
export class MediaSubscriber implements EntitySubscriberInterface {
|
||||
private async notifyAvailableMovie(entity: Media) {
|
||||
if (entity.status === MediaStatus.AVAILABLE) {
|
||||
if (entity.mediaType === MediaType.MOVIE) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const relatedRequests = await requestRepository.find({
|
||||
where: { media: entity },
|
||||
});
|
||||
|
||||
if (relatedRequests.length > 0) {
|
||||
const tmdb = new TheMovieDb();
|
||||
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
|
||||
|
||||
relatedRequests.forEach((request) => {
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
notifyUser: request.requestedBy,
|
||||
subject: movie.title,
|
||||
message: movie.overview,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyAvailableSeries(entity: Media, dbEntity: Media) {
|
||||
const newAvailableSeasons = entity.seasons
|
||||
.filter((season) => season.status === MediaStatus.AVAILABLE)
|
||||
.map((season) => season.seasonNumber);
|
||||
const oldAvailableSeasons = dbEntity.seasons
|
||||
.filter((season) => season.status === MediaStatus.AVAILABLE)
|
||||
.map((season) => season.seasonNumber);
|
||||
|
||||
const changedSeasons = newAvailableSeasons.filter(
|
||||
(seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
|
||||
);
|
||||
|
||||
if (changedSeasons.length > 0) {
|
||||
const tmdb = new TheMovieDb();
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const processedSeasons: number[] = [];
|
||||
|
||||
for (const changedSeasonNumber of changedSeasons) {
|
||||
const requests = await requestRepository.find({
|
||||
where: { media: entity },
|
||||
});
|
||||
const request = requests.find(
|
||||
(request) =>
|
||||
// Check if the season is complete AND it contains the current season that was just marked available
|
||||
request.seasons.every((season) =>
|
||||
newAvailableSeasons.includes(season.seasonNumber)
|
||||
) &&
|
||||
request.seasons.some(
|
||||
(season) => season.seasonNumber === changedSeasonNumber
|
||||
)
|
||||
);
|
||||
|
||||
if (request && !processedSeasons.includes(changedSeasonNumber)) {
|
||||
processedSeasons.push(
|
||||
...request.seasons.map((season) => season.seasonNumber)
|
||||
);
|
||||
const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
subject: tv.name,
|
||||
message: tv.overview,
|
||||
notifyUser: request.requestedBy,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
extra: [
|
||||
{
|
||||
name: 'Seasons',
|
||||
value: request.seasons
|
||||
.map((season) => season.seasonNumber)
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public beforeUpdate(event: UpdateEvent<Media>): void {
|
||||
if (
|
||||
event.entity.mediaType === MediaType.MOVIE &&
|
||||
event.entity.status === MediaStatus.AVAILABLE
|
||||
) {
|
||||
this.notifyAvailableMovie(event.entity);
|
||||
}
|
||||
|
||||
if (
|
||||
event.entity.mediaType === MediaType.TV &&
|
||||
(event.entity.status === MediaStatus.AVAILABLE ||
|
||||
event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
|
||||
) {
|
||||
this.notifyAvailableSeries(event.entity, event.databaseEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
||||
padding-bottom: 25px;\
|
||||
text-align: center;\
|
||||
')
|
||||
a(href='https://example.com' style='\
|
||||
a(href=actionUrl style='\
|
||||
text-shadow: 0 1px 0 #ffffff;\
|
||||
font-weight: 700;\
|
||||
font-size: 16px;\
|
||||
|
||||
3
server/types/plex-api.d.ts
vendored
3
server/types/plex-api.d.ts
vendored
@@ -2,8 +2,9 @@ declare module 'plex-api' {
|
||||
export default class PlexAPI {
|
||||
constructor(intiialOptions: {
|
||||
hostname: string;
|
||||
post: number;
|
||||
port: number;
|
||||
token?: string;
|
||||
https?: boolean;
|
||||
authenticator: {
|
||||
authenticate: (
|
||||
_plexApi: PlexAPI,
|
||||
|
||||
12
server/utils/appVersion.ts
Normal file
12
server/utils/appVersion.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const getAppVersion = (): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { version } = require('../../package.json');
|
||||
|
||||
let finalVersion = version;
|
||||
|
||||
if (version === '0.1.0') {
|
||||
finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`;
|
||||
}
|
||||
|
||||
return finalVersion;
|
||||
};
|
||||
3
src/assets/services/imdb.svg
Normal file
3
src/assets/services/imdb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 575 289.83"><defs><path d="M575 24.91C573.44 12.15 563.97 1.98 551.91 0C499.05 0 76.18 0 23.32 0C10.11 2.17 0 14.16 0 28.61C0 51.84 0 237.64 0 260.86C0 276.86 12.37 289.83 27.64 289.83C79.63 289.83 495.6 289.83 547.59 289.83C561.65 289.83 573.26 278.82 575 264.57C575 216.64 575 48.87 575 24.91Z" id="d1pwhf9wy2"></path><path d="M69.35 58.24L114.98 58.24L114.98 233.89L69.35 233.89L69.35 58.24Z" id="g5jjnq26yS"></path><path d="M201.2 139.15C197.28 112.38 195.1 97.5 194.67 94.53C192.76 80.2 190.94 67.73 189.2 57.09C185.25 57.09 165.54 57.09 130.04 57.09L130.04 232.74L170.01 232.74L170.15 116.76L186.97 232.74L215.44 232.74L231.39 114.18L231.54 232.74L271.38 232.74L271.38 57.09L211.77 57.09L201.2 139.15Z" id="i3Prh1JpXt"></path><path d="M346.71 93.63C347.21 95.87 347.47 100.95 347.47 108.89C347.47 115.7 347.47 170.18 347.47 176.99C347.47 188.68 346.71 195.84 345.2 198.48C343.68 201.12 339.64 202.43 333.09 202.43C333.09 190.9 333.09 98.66 333.09 87.13C338.06 87.13 341.45 87.66 343.25 88.7C345.05 89.75 346.21 91.39 346.71 93.63ZM367.32 230.95C372.75 229.76 377.31 227.66 381.01 224.67C384.7 221.67 387.29 217.52 388.77 212.21C390.26 206.91 391.14 196.38 391.14 180.63C391.14 174.47 391.14 125.12 391.14 118.95C391.14 102.33 390.49 91.19 389.48 85.53C388.46 79.86 385.93 74.71 381.88 70.09C377.82 65.47 371.9 62.15 364.12 60.13C356.33 58.11 343.63 57.09 321.54 57.09C319.27 57.09 307.93 57.09 287.5 57.09L287.5 232.74L342.78 232.74C355.52 232.34 363.7 231.75 367.32 230.95Z" id="a4ov9rRGQm"></path><path d="M464.76 204.7C463.92 206.93 460.24 208.06 457.46 208.06C454.74 208.06 452.93 206.98 452.01 204.81C451.09 202.65 450.64 197.72 450.64 190C450.64 185.36 450.64 148.22 450.64 143.58C450.64 135.58 451.04 130.59 451.85 128.6C452.65 126.63 454.41 125.63 457.13 125.63C459.91 125.63 463.64 126.76 464.6 129.03C465.55 131.3 466.03 136.15 466.03 143.58C466.03 146.58 466.03 161.58 466.03 188.59C465.74 197.84 465.32 203.21 464.76 204.7ZM406.68 231.21L447.76 231.21C449.47 224.5 450.41 220.77 450.6 220.02C454.32 224.52 458.41 227.9 462.9 230.14C467.37 232.39 474.06 233.51 479.24 233.51C486.45 233.51 492.67 231.62 497.92 227.83C503.16 224.05 506.5 219.57 507.92 214.42C509.34 209.26 510.05 201.42 510.05 190.88C510.05 185.95 510.05 146.53 510.05 141.6C510.05 131 509.81 124.08 509.34 120.83C508.87 117.58 507.47 114.27 505.14 110.88C502.81 107.49 499.42 104.86 494.98 102.98C490.54 101.1 485.3 100.16 479.26 100.16C474.01 100.16 467.29 101.21 462.81 103.28C458.34 105.35 454.28 108.49 450.64 112.7C450.64 108.89 450.64 89.85 450.64 55.56L406.68 55.56L406.68 231.21Z" id="fk968BpsX"></path></defs><g><g><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill="#f6c700" fill-opacity="1"></use><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#g5jjnq26yS" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#g5jjnq26yS" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#fk968BpsX" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#fk968BpsX" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
src/assets/services/rt.svg
Normal file
1
src/assets/services/rt.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.4 KiB |
1
src/assets/services/tmdb.svg
Normal file
1
src/assets/services/tmdb.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="66.7" x2="185.04" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
36
src/components/Common/Alert/index.tsx
Normal file
36
src/components/Common/Alert/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
interface AlertProps {
|
||||
title: string;
|
||||
type?: 'warning';
|
||||
}
|
||||
|
||||
const Alert: React.FC<AlertProps> = ({ title, children }) => {
|
||||
return (
|
||||
<div className="rounded-md bg-yellow-600 p-4 mb-8">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg
|
||||
className="h-5 w-5 text-yellow-200"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-200">{title}</h3>
|
||||
<div className="mt-2 text-sm text-yellow-300">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
40
src/components/Common/List/index.tsx
Normal file
40
src/components/Common/List/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { withProperties } from '../../../utils/typeHelpers';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({ title, children }) => {
|
||||
return (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt className="text-sm font-medium text-gray-200">{title}</dt>
|
||||
<dd className="mt-1 flex text-sm text-gray-400 sm:mt-0 sm:col-span-2">
|
||||
<span className="flex-grow">{children}</span>
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ListProps {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
}
|
||||
|
||||
const List: React.FC<ListProps> = ({ title, subTitle, children }) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-100">{title}</h3>
|
||||
{subTitle && (
|
||||
<p className="mt-1 max-w-2xl text-sm text-gray-300">{subTitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5 border-t border-gray-800">
|
||||
<dl className="divide-y divide-gray-800">{children}</dl>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProperties(List, { Item: ListItem });
|
||||
@@ -8,7 +8,7 @@ import { useIntl } from 'react-intl';
|
||||
import globalMessages from '../../../i18n/globalMessages';
|
||||
import Transition from '../../Transition';
|
||||
|
||||
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
interface ModalProps {
|
||||
title?: string;
|
||||
onCancel?: (e?: MouseEvent<HTMLElement>) => void;
|
||||
onOk?: (e?: MouseEvent<HTMLButtonElement>) => void;
|
||||
@@ -63,126 +63,122 @@ const Modal: React.FC<ModalProps> = ({
|
||||
});
|
||||
useLockBodyScroll(true, disableScrollLock);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ReactDOM.createPortal(
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
return ReactDOM.createPortal(
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 bottom-0 bg-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
typeof onCancel === 'function' && backgroundClickable
|
||||
? onCancel()
|
||||
: undefined;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300 transform scale-75"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={loading}
|
||||
>
|
||||
<div style={{ position: 'absolute' }}>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300 transform scale-75"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={!loading}
|
||||
>
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 bottom-0 bg-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
typeof onCancel === 'function' && backgroundClickable
|
||||
? onCancel()
|
||||
: undefined;
|
||||
}
|
||||
}}
|
||||
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline"
|
||||
ref={modalRef}
|
||||
>
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300 transform scale-75"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={loading}
|
||||
>
|
||||
<div style={{ position: 'absolute' }}>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300 transform scale-75"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={!loading}
|
||||
>
|
||||
<div
|
||||
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline"
|
||||
ref={modalRef}
|
||||
>
|
||||
<div className="sm:flex sm:items-center">
|
||||
{iconSvg && (
|
||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
|
||||
{iconSvg}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`mt-3 text-center sm:mt-0 sm:text-left ${
|
||||
iconSvg ? 'sm:ml-4' : 'mb-6'
|
||||
}`}
|
||||
>
|
||||
{title && (
|
||||
<h3
|
||||
className="text-lg leading-6 font-medium text-white"
|
||||
id="modal-headline"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
<div className="sm:flex sm:items-center">
|
||||
{iconSvg && (
|
||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
|
||||
{iconSvg}
|
||||
</div>
|
||||
{children && (
|
||||
<div className="mt-4 text-sm leading-5 text-gray-300">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
{(onCancel || onOk || onSecondary || onTertiary) && (
|
||||
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
|
||||
{typeof onOk === 'function' && (
|
||||
<Button
|
||||
buttonType={okButtonType}
|
||||
onClick={onOk}
|
||||
className="ml-3"
|
||||
disabled={okDisabled}
|
||||
>
|
||||
{okText ? okText : 'Ok'}
|
||||
</Button>
|
||||
)}
|
||||
{typeof onSecondary === 'function' && secondaryText && (
|
||||
<Button
|
||||
buttonType={secondaryButtonType}
|
||||
onClick={onSecondary}
|
||||
className="ml-3"
|
||||
disabled={secondaryDisabled}
|
||||
>
|
||||
{secondaryText}
|
||||
</Button>
|
||||
)}
|
||||
{typeof onTertiary === 'function' && tertiaryText && (
|
||||
<Button
|
||||
buttonType={tertiaryButtonType}
|
||||
onClick={onTertiary}
|
||||
className="ml-3"
|
||||
disabled={tertiaryDisabled}
|
||||
>
|
||||
{tertiaryText}
|
||||
</Button>
|
||||
)}
|
||||
{typeof onCancel === 'function' && (
|
||||
<Button
|
||||
buttonType={cancelButtonType}
|
||||
onClick={onCancel}
|
||||
className="ml-3 sm:ml-0 sm:px-4"
|
||||
>
|
||||
{cancelText
|
||||
? cancelText
|
||||
: intl.formatMessage(globalMessages.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`mt-3 text-center sm:mt-0 sm:text-left ${
|
||||
iconSvg ? 'sm:ml-4' : 'mb-6'
|
||||
}`}
|
||||
>
|
||||
{title && (
|
||||
<h3
|
||||
className="text-lg leading-6 font-medium text-white"
|
||||
id="modal-headline"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
{children && (
|
||||
<div className="mt-4 text-sm leading-5 text-gray-300">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
{(onCancel || onOk || onSecondary || onTertiary) && (
|
||||
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
|
||||
{typeof onOk === 'function' && (
|
||||
<Button
|
||||
buttonType={okButtonType}
|
||||
onClick={onOk}
|
||||
className="ml-3"
|
||||
disabled={okDisabled}
|
||||
>
|
||||
{okText ? okText : 'Ok'}
|
||||
</Button>
|
||||
)}
|
||||
{typeof onSecondary === 'function' && secondaryText && (
|
||||
<Button
|
||||
buttonType={secondaryButtonType}
|
||||
onClick={onSecondary}
|
||||
className="ml-3"
|
||||
disabled={secondaryDisabled}
|
||||
>
|
||||
{secondaryText}
|
||||
</Button>
|
||||
)}
|
||||
{typeof onTertiary === 'function' && tertiaryText && (
|
||||
<Button
|
||||
buttonType={tertiaryButtonType}
|
||||
onClick={onTertiary}
|
||||
className="ml-3"
|
||||
disabled={tertiaryDisabled}
|
||||
>
|
||||
{tertiaryText}
|
||||
</Button>
|
||||
)}
|
||||
{typeof onCancel === 'function' && (
|
||||
<Button
|
||||
buttonType={cancelButtonType}
|
||||
onClick={onCancel}
|
||||
className="ml-3 sm:ml-0 sm:px-4"
|
||||
>
|
||||
{cancelText
|
||||
? cancelText
|
||||
: intl.formatMessage(globalMessages.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
70
src/components/Discover/Holiday/index.tsx
Normal file
70
src/components/Discover/Holiday/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useSWRInfinite } from 'swr';
|
||||
import type { MovieResult } from '../../../../server/models/Search';
|
||||
import ListView from '../../Common/ListView';
|
||||
import { LanguageContext } from '../../../context/LanguageContext';
|
||||
import Header from '../../Common/Header';
|
||||
|
||||
interface SearchResult {
|
||||
page: number;
|
||||
totalResults: number;
|
||||
totalPages: number;
|
||||
results: MovieResult[];
|
||||
}
|
||||
|
||||
const Holiday: React.FC = () => {
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
|
||||
(pageIndex: number, previousPageData: SearchResult | null) => {
|
||||
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `/api/v1/discover/keyword/207317/movies?page=${
|
||||
pageIndex + 1
|
||||
}&language=${locale}`;
|
||||
},
|
||||
{
|
||||
initialSize: 3,
|
||||
}
|
||||
);
|
||||
|
||||
const isLoadingInitialData = !data && !error;
|
||||
const isLoadingMore =
|
||||
isLoadingInitialData ||
|
||||
(size > 0 && data && typeof data[size - 1] === 'undefined');
|
||||
|
||||
const fetchMore = () => {
|
||||
setSize(size + 1);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <div>{error}</div>;
|
||||
}
|
||||
|
||||
const titles = data?.reduce(
|
||||
(a, v) => [...a, ...v.results],
|
||||
[] as MovieResult[]
|
||||
);
|
||||
|
||||
const isEmpty = !isLoadingInitialData && titles?.length === 0;
|
||||
const isReachingEnd =
|
||||
isEmpty || (data && data[data.length - 1]?.results.length < 20);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>Happy Holidays!</Header>
|
||||
<ListView
|
||||
items={titles}
|
||||
isEmpty={isEmpty}
|
||||
isLoading={
|
||||
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
|
||||
}
|
||||
isReachingEnd={isReachingEnd}
|
||||
onScrollBottom={fetchMore}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Holiday;
|
||||
@@ -63,6 +63,12 @@ const Discover: React.FC = () => {
|
||||
} = useSWR<MovieDiscoverResult>(
|
||||
`/api/v1/discover/movies/upcoming?language=${locale}`
|
||||
);
|
||||
const {
|
||||
data: holUpcomingData,
|
||||
error: holUpcomingError,
|
||||
} = useSWR<MovieDiscoverResult>(
|
||||
`/api/v1/discover/keyword/207317/movies?language=${locale}`
|
||||
);
|
||||
|
||||
const { data: trendingData, error: trendingError } = useSWR<MixedResult>(
|
||||
`/api/v1/discover/trending?language=${locale}`
|
||||
@@ -140,6 +146,57 @@ const Discover: React.FC = () => {
|
||||
placeholder={<RequestCard.Placeholder />}
|
||||
emptyMessage={intl.formatMessage(messages.nopending)}
|
||||
/>
|
||||
{/* Special Temporary Slider */}
|
||||
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href="/discover/holiday">
|
||||
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
||||
<span>
|
||||
<span role="img" aria-label="Christmas Tree" className="mr-2">
|
||||
🎄
|
||||
</span>
|
||||
Happy Holidays!
|
||||
<span role="img" aria-label="Christmas Tree" className="ml-2">
|
||||
🎄
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className="w-6 h-6 ml-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
sliderKey="holiday"
|
||||
isLoading={!holUpcomingData && !holUpcomingError}
|
||||
isEmpty={false}
|
||||
items={holUpcomingData?.results.map((title) => (
|
||||
<TitleCard
|
||||
key={`holiday-movie-slider-${title.id}`}
|
||||
id={title.id}
|
||||
image={title.posterPath}
|
||||
status={title.mediaInfo?.status}
|
||||
summary={title.overview}
|
||||
title={title.title}
|
||||
userScore={title.voteAverage}
|
||||
year={title.releaseDate}
|
||||
mediaType={title.mediaType}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
{/* End Special Temporary Slider */}
|
||||
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href="/discover/movies/upcoming">
|
||||
|
||||
55
src/components/ExternalLinkBlock/index.tsx
Normal file
55
src/components/ExternalLinkBlock/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import TmdbLogo from '../../assets/services/tmdb.svg';
|
||||
import ImdbLogo from '../../assets/services/imdb.svg';
|
||||
import RTLogo from '../../assets/services/rt.svg';
|
||||
|
||||
interface ExternalLinkBlockProps {
|
||||
mediaType: 'movie' | 'tv';
|
||||
imdbId?: string;
|
||||
tmdbId?: number;
|
||||
rtUrl?: string;
|
||||
}
|
||||
|
||||
const ExternalLinkBlock: React.FC<ExternalLinkBlockProps> = ({
|
||||
imdbId,
|
||||
tmdbId,
|
||||
rtUrl,
|
||||
mediaType,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-end items-center">
|
||||
{tmdbId && (
|
||||
<a
|
||||
href={`https://www.themoviedb.org/${mediaType}/${tmdbId}`}
|
||||
className="w-8 mx-2 opacity-50 hover:opacity-100 transition duration-300"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<TmdbLogo />
|
||||
</a>
|
||||
)}
|
||||
{imdbId && (
|
||||
<a
|
||||
href={`https://www.imdb.com/title/${imdbId}`}
|
||||
className="w-8 mx-2 opacity-50 hover:opacity-100 transition duration-300"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ImdbLogo />
|
||||
</a>
|
||||
)}
|
||||
{rtUrl && (
|
||||
<a
|
||||
href={`${rtUrl}`}
|
||||
className="w-14 mx-2 opacity-50 hover:opacity-100 transition duration-300"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<RTLogo />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalLinkBlock;
|
||||
@@ -45,6 +45,26 @@ const availableLanguages: AvailableLanguageObject = {
|
||||
code: 'nl',
|
||||
display: 'Nederlands',
|
||||
},
|
||||
es: {
|
||||
code: 'es',
|
||||
display: 'Spanish',
|
||||
},
|
||||
it: {
|
||||
code: 'it',
|
||||
display: 'Italian',
|
||||
},
|
||||
'pt-BR': {
|
||||
code: 'pt-BR',
|
||||
display: 'Portuguese (Brazil)',
|
||||
},
|
||||
sr: {
|
||||
code: 'sr',
|
||||
display: 'Serbian',
|
||||
},
|
||||
sv: {
|
||||
code: 'sv',
|
||||
display: 'Swedish',
|
||||
},
|
||||
};
|
||||
|
||||
const LanguagePicker: React.FC = () => {
|
||||
@@ -54,7 +74,7 @@ const LanguagePicker: React.FC = () => {
|
||||
useClickOutside(dropdownRef, () => setDropdownOpen(false));
|
||||
|
||||
return (
|
||||
<div className="ml-3 relative">
|
||||
<div className="relative">
|
||||
<div>
|
||||
<button
|
||||
className="p-1 text-gray-400 rounded-full hover:bg-gray-500 hover:text-white focus:outline-none focus:ring focus:text-white"
|
||||
|
||||
@@ -27,7 +27,7 @@ const SearchInput: React.FC = () => {
|
||||
</div>
|
||||
<input
|
||||
id="search_field"
|
||||
className="block w-full h-full pl-8 pr-3 py-2 rounded-md border-transparent focus:border-transparent bg-gray-600 text-white placeholder-gray-300 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
|
||||
className="block w-full h-full pl-8 pr-1 py-2 rounded-md border-transparent focus:border-transparent bg-gray-600 text-white placeholder-gray-300 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
type="search"
|
||||
value={searchValue}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useRef } from 'react';
|
||||
import Transition from '../../Transition';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { useUser, Permission } from '../../../hooks/useUser';
|
||||
import useClickOutside from '../../../hooks/useClickOutside';
|
||||
|
||||
const messages = defineMessages({
|
||||
dashboard: 'Discover',
|
||||
@@ -116,8 +117,10 @@ const SidebarLinks: SidebarLinkProps[] = [
|
||||
];
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const { hasPermission } = useUser();
|
||||
useClickOutside(navRef, () => setClosed());
|
||||
return (
|
||||
<>
|
||||
<div className="md:hidden">
|
||||
@@ -166,7 +169,10 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
|
||||
<div
|
||||
ref={navRef}
|
||||
className="flex-1 h-0 pt-5 pb-4 overflow-y-auto"
|
||||
>
|
||||
<div className="flex-shrink-0 flex items-center px-4">
|
||||
<span className="text-xl text-gray-50">
|
||||
<Link href="/">
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useState } from 'react';
|
||||
import SearchInput from './SearchInput';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import Sidebar from './Sidebar';
|
||||
import Notifications from './Notifications';
|
||||
import LanguagePicker from './LanguagePicker';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { Permission, useUser } from '../../hooks/useUser';
|
||||
|
||||
const messages = defineMessages({
|
||||
alphawarning:
|
||||
@@ -14,6 +14,7 @@ const messages = defineMessages({
|
||||
|
||||
const Layout: React.FC = ({ children }) => {
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
const { hasPermission } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
@@ -43,9 +44,8 @@ const Layout: React.FC = ({ children }) => {
|
||||
</button>
|
||||
<div className="flex-1 px-4 flex justify-between">
|
||||
<SearchInput />
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
<div className="flex items-center md:ml-6">
|
||||
<LanguagePicker />
|
||||
<Notifications />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ const Layout: React.FC = ({ children }) => {
|
||||
>
|
||||
<div className="pt-2 pb-6 md:py-6">
|
||||
<div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8">
|
||||
{router.pathname === '/' && (
|
||||
{router.pathname === '/' && hasPermission(Permission.ADMIN) && (
|
||||
<div className="rounded-md bg-indigo-700 p-4 mt-2">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
@@ -85,7 +85,7 @@ const Layout: React.FC = ({ children }) => {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Github →
|
||||
GitHub →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,25 +5,36 @@ import axios from 'axios';
|
||||
import { useRouter } from 'next/dist/client/router';
|
||||
import ImageFader from '../Common/ImageFader';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import Transition from '../Transition';
|
||||
import LanguagePicker from '../Layout/LanguagePicker';
|
||||
|
||||
const messages = defineMessages({
|
||||
signinplex: 'Sign in to continue',
|
||||
});
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [error, setError] = useState('');
|
||||
const [isProcessing, setProcessing] = useState(false);
|
||||
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
|
||||
const { user, revalidate } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
|
||||
// We take the token and attempt to login. If we get a success message, we will
|
||||
// ask swr to revalidate the user which _shouid_ come back with a valid user.
|
||||
// ask swr to revalidate the user which _should_ come back with a valid user.
|
||||
useEffect(() => {
|
||||
const login = async () => {
|
||||
const response = await axios.post('/api/v1/auth/login', { authToken });
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await axios.post('/api/v1/auth/login', { authToken });
|
||||
|
||||
if (response.data?.email) {
|
||||
revalidate();
|
||||
if (response.data?.email) {
|
||||
revalidate();
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.response.data.message);
|
||||
setAuthToken(undefined);
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
if (authToken) {
|
||||
@@ -49,7 +60,10 @@ const Login: React.FC = () => {
|
||||
'/images/rotate4.jpg',
|
||||
]}
|
||||
/>
|
||||
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-md relative z-50">
|
||||
<div className="absolute top-4 right-4 z-50">
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-md relative z-40">
|
||||
<img
|
||||
src="/logo.png"
|
||||
className="mx-auto max-h-32 w-auto"
|
||||
@@ -64,7 +78,40 @@ const Login: React.FC = () => {
|
||||
className="bg-gray-800 bg-opacity-50 py-8 px-4 shadow sm:rounded-lg sm:px-10"
|
||||
style={{ backdropFilter: 'blur(5px)' }}
|
||||
>
|
||||
<Transition
|
||||
show={!!error}
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="rounded-md bg-red-600 p-4 mb-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg
|
||||
className="h-5 w-5 text-red-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-300">{error}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<PlexLoginButton
|
||||
isProcessing={isProcessing}
|
||||
onAuthToken={(authToken) => setAuthToken(authToken)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { RTRating } from '../../../server/api/rottentomatoes';
|
||||
import Error from '../../pages/_error';
|
||||
import Head from 'next/head';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import ExternalLinkBlock from '../ExternalLinkBlock';
|
||||
|
||||
const messages = defineMessages({
|
||||
releasedate: 'Release Date',
|
||||
@@ -65,6 +66,7 @@ const messages = defineMessages({
|
||||
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
|
||||
approve: 'Approve',
|
||||
decline: 'Decline',
|
||||
studio: 'Studio',
|
||||
});
|
||||
|
||||
interface MovieDetailsProps {
|
||||
@@ -193,14 +195,14 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
)}
|
||||
</SlideOver>
|
||||
<div className="flex flex-col items-center md:flex-row md:items-end pt-4">
|
||||
<div className="mr-4 flex-shrink-0">
|
||||
<div className="md:mr-4 flex-shrink-0">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
|
||||
alt=""
|
||||
className="rounded md:rounded-lg shadow md:shadow-2xl w-32 md:w-52"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
|
||||
<div className="text-white flex flex-col md:mr-4 mt-4 md:mt-0 text-center md:text-left">
|
||||
<div className="mb-2">
|
||||
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
|
||||
<Badge badgeType="success">
|
||||
@@ -352,7 +354,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="default"
|
||||
className="ml-2"
|
||||
className="ml-2 first:ml-0"
|
||||
onClick={() => setShowManager(true)}
|
||||
>
|
||||
<svg
|
||||
@@ -395,34 +397,36 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
<div className="bg-gray-900 rounded-lg shadow border border-gray-800">
|
||||
{(data.voteCount > 0 || ratingData) && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0 items-center justify-center">
|
||||
{ratingData?.criticsRating && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.criticsRating === 'Rotten' ? (
|
||||
<RTRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.criticsScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ratingData?.audienceRating && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.audienceRating === 'Spilled' ? (
|
||||
<RTAudRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTAudFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.audienceScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ratingData?.criticsRating &&
|
||||
(ratingData?.criticsScore ?? 0) > 0 && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.criticsRating === 'Rotten' ? (
|
||||
<RTRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.criticsScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ratingData?.audienceRating &&
|
||||
(ratingData?.audienceScore ?? 0) > 0 && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.audienceRating === 'Spilled' ? (
|
||||
<RTAudRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTAudFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.audienceScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{data.voteCount > 0 && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
@@ -484,14 +488,40 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.originallanguage} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{data.originalLanguage}
|
||||
</span>
|
||||
</div>
|
||||
{data.spokenLanguages.some(
|
||||
(lng) => lng.iso_639_1 === data.originalLanguage
|
||||
) && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.originallanguage} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{
|
||||
data.spokenLanguages.find(
|
||||
(lng) => lng.iso_639_1 === data.originalLanguage
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.productionCompanies[0] && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.studio} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{data.productionCompanies[0]?.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<ExternalLinkBlock
|
||||
mediaType="movie"
|
||||
tmdbId={data.id}
|
||||
imdbId={data.externalIds.imdbId}
|
||||
rtUrl={ratingData?.url}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,23 +12,23 @@ const plexOAuth = new PlexOAuth();
|
||||
|
||||
interface PlexLoginButtonProps {
|
||||
onAuthToken: (authToken: string) => void;
|
||||
isProcessing?: boolean;
|
||||
onError?: (message: string) => void;
|
||||
}
|
||||
|
||||
const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
|
||||
onAuthToken,
|
||||
onError,
|
||||
isProcessing,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const getPlexLogin = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const authToken = await plexOAuth.login();
|
||||
setLoading(false);
|
||||
setIsProcessing(true);
|
||||
onAuthToken(authToken);
|
||||
} catch (e) {
|
||||
if (onError) {
|
||||
|
||||
@@ -43,26 +43,30 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="px-4 py-4 sm:px-6 ">
|
||||
<div className="px-4 py-4 sm:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
|
||||
<svg
|
||||
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{request.requestedBy.username}</span>
|
||||
<div className="mr-6 flex-col items-center text-sm leading-5 text-gray-300 flex-1 min-w-0">
|
||||
<div className="flex flex-nowrap mb-1 white">
|
||||
<svg
|
||||
className="min-w-0 flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="truncate w-40 md:w-auto">
|
||||
{request.requestedBy.username}
|
||||
</span>
|
||||
</div>
|
||||
{request.modifiedBy && (
|
||||
<>
|
||||
<div className="flex flex-nowrap">
|
||||
<svg
|
||||
className="flex-shrink-0 ml-2 mr-1.5 h-5 w-5 text-gray-300"
|
||||
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -74,8 +78,10 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{request.modifiedBy?.username}</span>
|
||||
</>
|
||||
<span className="truncate w-40 md:w-auto">
|
||||
{request.modifiedBy?.username}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-2 flex-shrink-0 flex">
|
||||
@@ -189,13 +195,18 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
</div>
|
||||
</div>
|
||||
{(request.seasons ?? []).length > 0 && (
|
||||
<div className="mt-2 text-sm flex items-center">
|
||||
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
<div className="mt-2 text-sm flex flex-col">
|
||||
<div className="mb-2">{intl.formatMessage(messages.seasons)}</div>
|
||||
<div>
|
||||
{request.seasons.map((season) => (
|
||||
<span
|
||||
key={`season-${season.id}`}
|
||||
className="mr-2 mb-1 inline-block"
|
||||
>
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ import ListView from '../Common/ListView';
|
||||
import { LanguageContext } from '../../context/LanguageContext';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Header from '../Common/Header';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
searchresults: 'Search Results',
|
||||
@@ -66,25 +65,7 @@ const Search: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="md:flex md:items-center md:justify-between mb-6 sm:mb-0 mt-6">
|
||||
<Header>{intl.formatMessage(messages.searchresults)}</Header>
|
||||
<div className="mt-4 flex md:mt-0 md:ml-4">
|
||||
<span className="relative z-0 inline-flex shadow-sm rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-indigo-900 bg-indigo-500 hover:bg-indigo-400 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
|
||||
>
|
||||
{intl.formatMessage(globalMessages.movies)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="-ml-px relative inline-flex items-center px-4 py-2 rounded-r-md border border-indigo-900 bg-indigo-500 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
|
||||
>
|
||||
{intl.formatMessage(globalMessages.tvshows)}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Header>{intl.formatMessage(messages.searchresults)}</Header>
|
||||
<ListView
|
||||
items={titles}
|
||||
isEmpty={isEmpty}
|
||||
|
||||
@@ -25,7 +25,10 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={setCopied}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCopied();
|
||||
}}
|
||||
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Axios from 'axios';
|
||||
import * as Yup from 'yup';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
|
||||
const messages = defineMessages({
|
||||
save: 'Save Changes',
|
||||
@@ -14,10 +15,13 @@ const messages = defineMessages({
|
||||
webhookUrl: 'Webhook URL',
|
||||
validationWebhookUrlRequired: 'You must provide a webhook URL',
|
||||
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
|
||||
discordsettingssaved: 'Discord notification settings saved!',
|
||||
discordsettingsfailed: 'Discord notification settings failed to save.',
|
||||
});
|
||||
|
||||
const NotificationsDiscord: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR(
|
||||
'/api/v1/settings/notifications/discord'
|
||||
);
|
||||
@@ -49,8 +53,15 @@ const NotificationsDiscord: React.FC = () => {
|
||||
webhookUrl: values.webhookUrl,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO show error
|
||||
addToast(intl.formatMessage(messages.discordsettingsfailed), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Axios from 'axios';
|
||||
import * as Yup from 'yup';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
|
||||
const messages = defineMessages({
|
||||
save: 'Save Changes',
|
||||
@@ -20,10 +21,13 @@ const messages = defineMessages({
|
||||
enableSsl: 'Enable SSL',
|
||||
authUser: 'Auth User',
|
||||
authPass: 'Auth Pass',
|
||||
emailsettingssaved: 'Email notification settings saved!',
|
||||
emailsettingsfailed: 'Email notification settings failed to save.',
|
||||
});
|
||||
|
||||
const NotificationsEmail: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR(
|
||||
'/api/v1/settings/notifications/email'
|
||||
);
|
||||
@@ -65,14 +69,21 @@ const NotificationsEmail: React.FC = () => {
|
||||
options: {
|
||||
emailFrom: values.emailFrom,
|
||||
smtpHost: values.smtpHost,
|
||||
smtpPort: values.smtpPort,
|
||||
smtpPort: Number(values.smtpPort),
|
||||
secure: values.secure,
|
||||
authUser: values.authUser,
|
||||
authPass: values.authPass,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO show error
|
||||
addToast(intl.formatMessage(messages.emailsettingsfailed), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
const messages = defineMessages({
|
||||
createradarr: 'Create New Radarr Server',
|
||||
editradarr: 'Edit Radarr Server',
|
||||
validationNameRequired: 'You must provide a server name',
|
||||
validationHostnameRequired: 'You must provide a hostname/IP',
|
||||
validationPortRequired: 'You must provide a port',
|
||||
validationApiKeyRequired: 'You must provide an API key',
|
||||
validationRootFolderRequired: 'You must select a root folder',
|
||||
validationProfileRequired: 'You must select a profile',
|
||||
validationMinimumAvailabilityRequired: 'You must select minimum availability',
|
||||
toastRadarrTestSuccess: 'Radarr connection established!',
|
||||
toastRadarrTestFailure: 'Failed to connect to Radarr Server',
|
||||
saving: 'Saving...',
|
||||
@@ -40,6 +42,10 @@ const messages = defineMessages({
|
||||
selectQualityProfile: 'Select a Quality Profile',
|
||||
selectRootFolder: 'Select a Root Folder',
|
||||
selectMinimumAvailability: 'Select minimum availability',
|
||||
loadingprofiles: 'Loading quality profiles…',
|
||||
testFirstQualityProfiles: 'Test your connection to load quality profiles',
|
||||
loadingrootfolders: 'Loading root folders…',
|
||||
testFirstRootFolders: 'Test your connection to load root folders',
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
@@ -74,6 +80,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
rootFolders: [],
|
||||
});
|
||||
const RadarrSettingsSchema = Yup.object().shape({
|
||||
name: Yup.string().required(
|
||||
intl.formatMessage(messages.validationNameRequired)
|
||||
),
|
||||
hostname: Yup.string().required(
|
||||
intl.formatMessage(messages.validationHostnameRequired)
|
||||
),
|
||||
@@ -81,10 +90,15 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
intl.formatMessage(messages.validationPortRequired)
|
||||
),
|
||||
apiKey: Yup.string().required(intl.formatMessage(messages.apiKey)),
|
||||
rootFolder: Yup.string().required(intl.formatMessage(messages.rootfolder)),
|
||||
rootFolder: Yup.string().required(
|
||||
intl.formatMessage(messages.validationRootFolderRequired)
|
||||
),
|
||||
activeProfileId: Yup.string().required(
|
||||
intl.formatMessage(messages.validationProfileRequired)
|
||||
),
|
||||
minimumAvailability: Yup.string().required(
|
||||
intl.formatMessage(messages.validationMinimumAvailabilityRequired)
|
||||
),
|
||||
});
|
||||
|
||||
const testConnection = useCallback(
|
||||
@@ -108,7 +122,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
{
|
||||
hostname,
|
||||
apiKey,
|
||||
port,
|
||||
port: Number(port),
|
||||
baseUrl,
|
||||
useSsl,
|
||||
}
|
||||
@@ -171,7 +185,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
baseUrl: radarr?.baseUrl,
|
||||
activeProfileId: radarr?.activeProfileId,
|
||||
rootFolder: radarr?.activeDirectory,
|
||||
minimumAvailability: radarr?.minimumAvailability,
|
||||
minimumAvailability: radarr?.minimumAvailability ?? 'released',
|
||||
isDefault: radarr?.isDefault ?? false,
|
||||
is4k: radarr?.is4k ?? false,
|
||||
}}
|
||||
@@ -185,11 +199,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
const submission = {
|
||||
name: values.name,
|
||||
hostname: values.hostname,
|
||||
port: values.port,
|
||||
port: Number(values.port),
|
||||
apiKey: values.apiKey,
|
||||
useSsl: values.ssl,
|
||||
baseUrl: values.baseUrl,
|
||||
activeProfileId: values.activeProfileId,
|
||||
activeProfileId: Number(values.activeProfileId),
|
||||
activeProfileName: profileName,
|
||||
activeDirectory: values.rootFolder,
|
||||
is4k: values.is4k,
|
||||
@@ -218,6 +232,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -250,7 +265,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
secondaryDisabled={
|
||||
!values.apiKey || !values.hostname || !values.port || isTesting
|
||||
}
|
||||
okDisabled={!isValidated || isSubmitting || isTesting}
|
||||
okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
|
||||
onOk={() => handleSubmit()}
|
||||
title={
|
||||
!radarr
|
||||
@@ -312,6 +327,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-600 text-gray-100 sm:text-sm cursor-default">
|
||||
{values.ssl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
@@ -321,7 +339,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
setIsValidated(false);
|
||||
setFieldValue('hostname', e.target.value);
|
||||
}}
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname && touched.hostname && (
|
||||
@@ -442,10 +460,17 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
as="select"
|
||||
id="activeProfileId"
|
||||
name="activeProfileId"
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
|
||||
disabled={!isValidated || isTesting}
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectQualityProfile)}
|
||||
{isTesting
|
||||
? intl.formatMessage(messages.loadingprofiles)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstQualityProfiles
|
||||
)
|
||||
: intl.formatMessage(messages.selectQualityProfile)}
|
||||
</option>
|
||||
{testResponse.profiles.length > 0 &&
|
||||
testResponse.profiles.map((profile) => (
|
||||
@@ -478,10 +503,15 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
as="select"
|
||||
id="rootFolder"
|
||||
name="rootFolder"
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
|
||||
disabled={!isValidated || isTesting}
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectRootFolder)}
|
||||
{isTesting
|
||||
? intl.formatMessage(messages.loadingrootfolders)
|
||||
: !isValidated
|
||||
? intl.formatMessage(messages.testFirstRootFolders)
|
||||
: intl.formatMessage(messages.selectRootFolder)}
|
||||
</option>
|
||||
{testResponse.rootFolders.length > 0 &&
|
||||
testResponse.rootFolders.map((folder) => (
|
||||
@@ -516,17 +546,18 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
name="minimumAvailability"
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(
|
||||
messages.selectMinimumAvailability
|
||||
)}
|
||||
</option>
|
||||
<option value="announced">Announced</option>
|
||||
<option value="inCinemas">In Cinemas</option>
|
||||
<option value="released">Released</option>
|
||||
<option value="preDB">PreDB</option>
|
||||
</Field>
|
||||
</div>
|
||||
{errors.minimumAvailability &&
|
||||
touched.minimumAvailability && (
|
||||
<div className="text-red-500 mt-2">
|
||||
{errors.minimumAvailability}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||
|
||||
186
src/components/Settings/SettingsAbout/Releases/index.tsx
Normal file
186
src/components/Settings/SettingsAbout/Releases/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import Alert from '../../../Common/Alert';
|
||||
import Badge from '../../../Common/Badge';
|
||||
import Button from '../../../Common/Button';
|
||||
import Modal from '../../../Common/Modal';
|
||||
import Transition from '../../../Transition';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
releases: 'Releases',
|
||||
releasedataMissing: 'Release data missing. Is GitHub down?',
|
||||
versionChangelog: 'Version Changelog',
|
||||
viewongithub: 'View on GitHub',
|
||||
latestversion: 'Latest Version',
|
||||
currentversion: 'Current Version',
|
||||
viewchangelog: 'View Changelog',
|
||||
runningDevelop: 'You are running a develop version of Overseerr!',
|
||||
runningDevelopMessage:
|
||||
'The changes in your version will not be available below. Please look at the <GithubLink>GitHub repository</GithubLink> for latest updates.',
|
||||
});
|
||||
|
||||
const REPO_RELEASE_API =
|
||||
'https://api.github.com/repos/sct/overseerr/releases?per_page=20';
|
||||
|
||||
interface GitHubRelease {
|
||||
url: string;
|
||||
assets_url: string;
|
||||
upload_url: string;
|
||||
html_url: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
tarball_url: string;
|
||||
zipball_url: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface ReleaseProps {
|
||||
release: GitHubRelease;
|
||||
isLatest: boolean;
|
||||
currentVersion: string;
|
||||
}
|
||||
|
||||
const Release: React.FC<ReleaseProps> = ({
|
||||
currentVersion,
|
||||
release,
|
||||
isLatest,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-md flex flex-col sm:flex-row px-4 py-2">
|
||||
<Transition
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={isModalOpen}
|
||||
>
|
||||
<Modal
|
||||
onCancel={() => setModalOpen(false)}
|
||||
iconSvg={
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
title={intl.formatMessage(messages.versionChangelog)}
|
||||
cancelText={intl.formatMessage(globalMessages.close)}
|
||||
okText={intl.formatMessage(messages.viewongithub)}
|
||||
onOk={() => {
|
||||
window.open(release.html_url, '_blank');
|
||||
}}
|
||||
>
|
||||
<div className="prose">
|
||||
<ReactMarkdown>{release.body}</ReactMarkdown>
|
||||
</div>
|
||||
</Modal>
|
||||
</Transition>
|
||||
<div className="flex mb-4 sm:mb-0 items-center justify-center sm:justify-start">
|
||||
<span className="text-xl">{release.name}</span>
|
||||
{isLatest && (
|
||||
<span className="ml-2">
|
||||
<Badge badgeType="primary">
|
||||
{intl.formatMessage(messages.latestversion)}
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
{release.name.includes(currentVersion) && (
|
||||
<span className="ml-2">
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(messages.currentversion)}
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 text-center sm:text-right">
|
||||
<Button buttonType="primary" onClick={() => setModalOpen(true)}>
|
||||
{intl.formatMessage(messages.viewchangelog)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ReleasesProps {
|
||||
currentVersion: string;
|
||||
}
|
||||
|
||||
const Releases: React.FC<ReleasesProps> = ({ currentVersion }) => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useSWR<GitHubRelease[]>(REPO_RELEASE_API);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="text-gray-300">
|
||||
{intl.formatMessage(messages.releasedataMissing)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="text-xl pb-4 mb-4 border-b border-gray-800">
|
||||
{intl.formatMessage(messages.releases)}
|
||||
</div>
|
||||
{currentVersion.startsWith('develop-') && (
|
||||
<Alert title={intl.formatMessage(messages.runningDevelop)}>
|
||||
{intl.formatMessage(messages.runningDevelopMessage, {
|
||||
GithubLink: function GithubLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/sct/overseerr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-yellow-100 underline hover:text-white transition duration-300"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
{data?.map((release, index) => {
|
||||
return (
|
||||
<div key={`release-${release.id}`} className="mb-2">
|
||||
<Release
|
||||
release={release}
|
||||
currentVersion={currentVersion}
|
||||
isLatest={index === 0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Releases;
|
||||
108
src/components/Settings/SettingsAbout/index.tsx
Normal file
108
src/components/Settings/SettingsAbout/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import useSWR from 'swr';
|
||||
import Error from '../../../pages/_error';
|
||||
import List from '../../Common/List';
|
||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||
import { SettingsAboutResponse } from '../../../../server/interfaces/api/settingsInterfaces';
|
||||
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
|
||||
import Releases from './Releases';
|
||||
|
||||
const messages = defineMessages({
|
||||
overseerrinformation: 'Overseerr Information',
|
||||
version: 'Version',
|
||||
totalmedia: 'Total Media',
|
||||
totalrequests: 'Total Requests',
|
||||
gettingsupport: 'Getting Support',
|
||||
githubdiscussions: 'GitHub Discussions',
|
||||
clickheretojoindiscord: 'Click here to join our Discord server.',
|
||||
timezone: 'Timezone',
|
||||
supportoverseerr: 'Support Overseerr',
|
||||
helppaycoffee: 'Help pay for coffee',
|
||||
});
|
||||
|
||||
const SettingsAbout: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useSWR<SettingsAboutResponse>(
|
||||
'/api/v1/settings/about'
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
}
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<List title={intl.formatMessage(messages.overseerrinformation)}>
|
||||
<List.Item title={intl.formatMessage(messages.version)}>
|
||||
{data.version}
|
||||
</List.Item>
|
||||
<List.Item title={intl.formatMessage(messages.totalmedia)}>
|
||||
<FormattedNumber value={data.totalMediaItems} />
|
||||
</List.Item>
|
||||
<List.Item title={intl.formatMessage(messages.totalrequests)}>
|
||||
<FormattedNumber value={data.totalRequests} />
|
||||
</List.Item>
|
||||
{data.tz && (
|
||||
<List.Item title={intl.formatMessage(messages.timezone)}>
|
||||
{data.tz}
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<List title={intl.formatMessage(messages.gettingsupport)}>
|
||||
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
|
||||
<a
|
||||
href="https://github.com/sct/overseerr/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://github.com/sct/overseerr/discussions
|
||||
</a>
|
||||
</List.Item>
|
||||
<List.Item title="Discord">
|
||||
<a
|
||||
href="https://discord.gg/PkCWJSeCk7"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
{intl.formatMessage(messages.clickheretojoindiscord)}
|
||||
</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<List title={intl.formatMessage(messages.supportoverseerr)}>
|
||||
<List.Item
|
||||
title={`☕️ ${intl.formatMessage(messages.helppaycoffee)}`}
|
||||
>
|
||||
<a
|
||||
href="https://patreon.com/overseerr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://patreon.com/overseerr
|
||||
</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<Releases currentVersion={data.version} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsAbout;
|
||||
@@ -46,7 +46,7 @@ const SettingsJobs: React.FC = () => {
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
<Button buttonType="primary">
|
||||
<Button buttonType="primary" disabled>
|
||||
{intl.formatMessage(messages.runnow)}
|
||||
</Button>
|
||||
</Table.TD>
|
||||
|
||||
17
src/components/Settings/SettingsLogs/index.tsx
Normal file
17
src/components/Settings/SettingsLogs/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
// We will localize this file when the complete version is released.
|
||||
|
||||
const SettingsLogs: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="leading-loose text-gray-300 text-sm">
|
||||
Logs page is still being built. For now, you can access your logs
|
||||
directly in <code>stdout</code> (container logs) or looking in{' '}
|
||||
<code>/app/config/logs/overseerr.logs</code>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsLogs;
|
||||
@@ -8,6 +8,7 @@ import axios from 'axios';
|
||||
import Button from '../Common/Button';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useUser, Permission } from '../../hooks/useUser';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
|
||||
const messages = defineMessages({
|
||||
generalsettings: 'General Settings',
|
||||
@@ -17,15 +18,37 @@ const messages = defineMessages({
|
||||
saving: 'Saving...',
|
||||
apikey: 'API Key',
|
||||
applicationurl: 'Application URL',
|
||||
toastApiKeySuccess: 'New API Key generated!',
|
||||
toastApiKeyFailure: 'Something went wrong generating a new API Key.',
|
||||
toastSettingsSuccess: 'Settings saved.',
|
||||
toastSettingsFailure: 'Something went wrong saving settings.',
|
||||
});
|
||||
|
||||
const SettingsMain: React.FC = () => {
|
||||
const { addToast } = useToasts();
|
||||
const { hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
const { data, error, revalidate } = useSWR<MainSettings>(
|
||||
'/api/v1/settings/main'
|
||||
);
|
||||
|
||||
const regenerate = async () => {
|
||||
try {
|
||||
await axios.get('/api/v1/settings/main/regenerate');
|
||||
|
||||
revalidate();
|
||||
addToast(intl.formatMessage(messages.toastApiKeySuccess), {
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.toastApiKeyFailure), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
@@ -50,8 +73,16 @@ const SettingsMain: React.FC = () => {
|
||||
await axios.post('/api/v1/settings/main', {
|
||||
applicationUrl: values.applicationUrl,
|
||||
});
|
||||
|
||||
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO show error
|
||||
addToast(intl.formatMessage(messages.toastSettingsFailure), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
@@ -77,8 +108,17 @@ const SettingsMain: React.FC = () => {
|
||||
value={data?.apiKey}
|
||||
readOnly
|
||||
/>
|
||||
<CopyButton textToCopy={data?.apiKey ?? ''} />
|
||||
<button className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium rounded-r-md text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<CopyButton
|
||||
textToCopy={data?.apiKey ?? ''}
|
||||
key={data?.apiKey}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
regenerate();
|
||||
}}
|
||||
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium rounded-r-md text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -2,21 +2,23 @@ import React, { useState } from 'react';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import type { PlexSettings } from '../../../server/lib/settings';
|
||||
import useSWR from 'swr';
|
||||
import { useFormik } from 'formik';
|
||||
import { Formik, Field } from 'formik';
|
||||
import Button from '../Common/Button';
|
||||
import axios from 'axios';
|
||||
import LibraryItem from './LibraryItem';
|
||||
import Badge from '../Common/Badge';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
plexsettings: 'Plex Settings',
|
||||
plexsettingsDescription:
|
||||
'Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.',
|
||||
servername: 'Server Name (Automatically Set)',
|
||||
servername: 'Server Name (Automatically set after you save)',
|
||||
servernamePlaceholder: 'Plex Server Name',
|
||||
hostname: 'Hostname/IP',
|
||||
port: 'Port',
|
||||
ssl: 'SSL',
|
||||
save: 'Save Changes',
|
||||
saving: 'Saving...',
|
||||
plexlibraries: 'Plex Libraries',
|
||||
@@ -32,6 +34,8 @@ const messages = defineMessages({
|
||||
librariesRemaining: 'Libraries Remaining: {count}',
|
||||
startscan: 'Start Scan',
|
||||
cancelscan: 'Cancel Scan',
|
||||
validationHostnameRequired: 'You must provide a hostname/IP',
|
||||
validationPortRequired: 'You must provide a port',
|
||||
});
|
||||
|
||||
interface Library {
|
||||
@@ -64,33 +68,15 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
||||
}
|
||||
);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
hostname: data?.ip,
|
||||
port: data?.port,
|
||||
},
|
||||
enableReinitialize: true,
|
||||
onSubmit: async (values) => {
|
||||
setSubmitError(null);
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await axios.post('/api/v1/settings/plex', {
|
||||
ip: values.hostname,
|
||||
port: values.port,
|
||||
} as PlexSettings);
|
||||
|
||||
revalidate();
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
setSubmitError(e.response.data.message);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
},
|
||||
const PlexSettingsSchema = Yup.object().shape({
|
||||
hostname: Yup.string().required(
|
||||
intl.formatMessage(messages.validationHostnameRequired)
|
||||
),
|
||||
port: Yup.number().required(
|
||||
intl.formatMessage(messages.validationPortRequired)
|
||||
),
|
||||
});
|
||||
|
||||
const activeLibraries =
|
||||
@@ -164,91 +150,157 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
||||
<FormattedMessage {...messages.plexsettingsDescription} />
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<div className="mt-6 sm:mt-5">
|
||||
{submitError && (
|
||||
<div className="bg-red-700 text-white p-4 rounded-md mb-6">
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
<FormattedMessage {...messages.servername} />
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.servernamePlaceholder
|
||||
)}
|
||||
value={data?.name}
|
||||
readOnly
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
<Formik
|
||||
initialValues={{
|
||||
hostname: data?.ip,
|
||||
port: data?.port,
|
||||
useSsl: data?.useSsl,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={PlexSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
setSubmitError(null);
|
||||
try {
|
||||
await axios.post('/api/v1/settings/plex', {
|
||||
ip: values.hostname,
|
||||
port: Number(values.port),
|
||||
useSsl: values.useSsl,
|
||||
} as PlexSettings);
|
||||
|
||||
revalidate();
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
setSubmitError(e.response.data.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
}) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-6 sm:mt-5">
|
||||
{submitError && (
|
||||
<div className="bg-red-700 text-white p-4 rounded-md mb-6">
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
<FormattedMessage {...messages.servername} />
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.servernamePlaceholder
|
||||
)}
|
||||
value={data?.name}
|
||||
readOnly
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||
<label
|
||||
htmlFor="hostname"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
<FormattedMessage {...messages.hostname} />
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-800 text-gray-100 sm:text-sm cursor-default">
|
||||
{values.useSsl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
type="text"
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
placeholder="127.0.0.1"
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname && touched.hostname && (
|
||||
<div className="text-red-500 mt-2">{errors.hostname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||
<label
|
||||
htmlFor="port"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
<FormattedMessage {...messages.port} />
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
|
||||
<Field
|
||||
type="text"
|
||||
id="port"
|
||||
name="port"
|
||||
placeholder="32400"
|
||||
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
{errors.port && touched.port && (
|
||||
<div className="text-red-500 mt-2">{errors.port}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||
<label
|
||||
htmlFor="hostname"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
<FormattedMessage {...messages.hostname} />
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
placeholder="127.0.0.1"
|
||||
value={formik.values.hostname}
|
||||
onChange={formik.handleChange}
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||
<label
|
||||
htmlFor="ssl"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
{intl.formatMessage(messages.ssl)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="useSsl"
|
||||
name="useSsl"
|
||||
onChange={() => {
|
||||
setFieldValue('useSsl', !values.useSsl);
|
||||
}}
|
||||
className="form-checkbox h-6 w-6 rounded-md text-indigo-600 transition duration-150 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||
<label
|
||||
htmlFor="port"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
<FormattedMessage {...messages.port} />
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
|
||||
<input
|
||||
type="text"
|
||||
id="port"
|
||||
name="port"
|
||||
placeholder="32400"
|
||||
value={formik.values.port}
|
||||
onChange={formik.handleChange}
|
||||
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting
|
||||
? intl.formatMessage(messages.saving)
|
||||
: intl.formatMessage(messages.save)}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
<Button buttonType="primary" type="submit" disabled={isUpdating}>
|
||||
{isUpdating
|
||||
? intl.formatMessage(messages.saving)
|
||||
: intl.formatMessage(messages.save)}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
<div className="mt-10">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-200">
|
||||
<FormattedMessage {...messages.plexlibraries} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import Badge from '../Common/Badge';
|
||||
import Button from '../Common/Button';
|
||||
import useSWR from 'swr';
|
||||
@@ -13,6 +13,7 @@ import Modal from '../Common/Modal';
|
||||
import Transition from '../Transition';
|
||||
import axios from 'axios';
|
||||
import SonarrModal from './SonarrModal';
|
||||
import Alert from '../Common/Alert';
|
||||
|
||||
const messages = defineMessages({
|
||||
radarrsettings: 'Radarr Settings',
|
||||
@@ -31,6 +32,10 @@ const messages = defineMessages({
|
||||
activeProfile: 'Active Profile',
|
||||
addradarr: 'Add Radarr Server',
|
||||
addsonarr: 'Add Sonarr Server',
|
||||
nodefault: 'No default server selected!',
|
||||
nodefaultdescription:
|
||||
'At least one server must be marked as default before any requests will make it to your services.',
|
||||
no4kimplemented: '(Default 4K servers are not currently implemented)',
|
||||
});
|
||||
|
||||
interface ServerInstanceProps {
|
||||
@@ -148,6 +153,7 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
|
||||
};
|
||||
|
||||
const SettingsServices: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
data: radarrData,
|
||||
error: radarrError,
|
||||
@@ -249,51 +255,64 @@ const SettingsServices: React.FC = () => {
|
||||
<div className="mt-6 sm:mt-5">
|
||||
{!radarrData && !radarrError && <LoadingSpinner />}
|
||||
{radarrData && !radarrError && (
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{radarrData.map((radarr) => (
|
||||
<ServerInstance
|
||||
key={`radarr-config-${radarr.id}`}
|
||||
name={radarr.name}
|
||||
address={radarr.hostname}
|
||||
profileName={radarr.activeProfileName}
|
||||
isSSL={radarr.useSsl}
|
||||
isDefault={radarr.isDefault && !radarr.is4k}
|
||||
isDefault4K={radarr.is4k && radarr.isDefault}
|
||||
onEdit={() => setEditRadarrModal({ open: true, radarr })}
|
||||
onDelete={() =>
|
||||
setDeleteServerModal({
|
||||
open: true,
|
||||
serverId: radarr.id,
|
||||
type: 'radarr',
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setEditRadarrModal({ open: true, radarr: null })
|
||||
<>
|
||||
{radarrData.length > 0 &&
|
||||
!radarrData.some(
|
||||
(radarr) => radarr.isDefault && !radarr.is4k
|
||||
) && (
|
||||
<Alert title={intl.formatMessage(messages.nodefault)}>
|
||||
<p>{intl.formatMessage(messages.nodefaultdescription)}</p>
|
||||
<p className="mt-2">
|
||||
{intl.formatMessage(messages.no4kimplemented)}
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{radarrData.map((radarr) => (
|
||||
<ServerInstance
|
||||
key={`radarr-config-${radarr.id}`}
|
||||
name={radarr.name}
|
||||
address={radarr.hostname}
|
||||
profileName={radarr.activeProfileName}
|
||||
isSSL={radarr.useSsl}
|
||||
isDefault={radarr.isDefault && !radarr.is4k}
|
||||
isDefault4K={radarr.is4k && radarr.isDefault}
|
||||
onEdit={() => setEditRadarrModal({ open: true, radarr })}
|
||||
onDelete={() =>
|
||||
setDeleteServerModal({
|
||||
open: true,
|
||||
serverId: radarr.id,
|
||||
type: 'radarr',
|
||||
})
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
/>
|
||||
))}
|
||||
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setEditRadarrModal({ open: true, radarr: null })
|
||||
}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.addradarr} />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<svg
|
||||
className="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.addradarr} />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
@@ -307,52 +326,65 @@ const SettingsServices: React.FC = () => {
|
||||
<div className="mt-6 sm:mt-5">
|
||||
{!sonarrData && !sonarrError && <LoadingSpinner />}
|
||||
{sonarrData && !sonarrError && (
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sonarrData.map((sonarr) => (
|
||||
<ServerInstance
|
||||
key={`sonarr-config-${sonarr.id}`}
|
||||
name={sonarr.name}
|
||||
address={sonarr.hostname}
|
||||
profileName={sonarr.activeProfileName}
|
||||
isSSL={sonarr.useSsl}
|
||||
isSonarr
|
||||
isDefault4K={sonarr.isDefault && sonarr.is4k}
|
||||
isDefault={sonarr.isDefault && !sonarr.is4k}
|
||||
onEdit={() => setEditSonarrModal({ open: true, sonarr })}
|
||||
onDelete={() =>
|
||||
setDeleteServerModal({
|
||||
open: true,
|
||||
serverId: sonarr.id,
|
||||
type: 'sonarr',
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setEditSonarrModal({ open: true, sonarr: null })
|
||||
<>
|
||||
{sonarrData.length > 0 &&
|
||||
!sonarrData.some(
|
||||
(sonarr) => sonarr.isDefault && !sonarr.is4k
|
||||
) && (
|
||||
<Alert title={intl.formatMessage(messages.nodefault)}>
|
||||
<p>{intl.formatMessage(messages.nodefaultdescription)}</p>
|
||||
<p className="mt-2">
|
||||
{intl.formatMessage(messages.no4kimplemented)}
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sonarrData.map((sonarr) => (
|
||||
<ServerInstance
|
||||
key={`sonarr-config-${sonarr.id}`}
|
||||
name={sonarr.name}
|
||||
address={sonarr.hostname}
|
||||
profileName={sonarr.activeProfileName}
|
||||
isSSL={sonarr.useSsl}
|
||||
isSonarr
|
||||
isDefault4K={sonarr.isDefault && sonarr.is4k}
|
||||
isDefault={sonarr.isDefault && !sonarr.is4k}
|
||||
onEdit={() => setEditSonarrModal({ open: true, sonarr })}
|
||||
onDelete={() =>
|
||||
setDeleteServerModal({
|
||||
open: true,
|
||||
serverId: sonarr.id,
|
||||
type: 'sonarr',
|
||||
})
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
/>
|
||||
))}
|
||||
<li className="col-span-1 border-2 border-dashed border-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setEditSonarrModal({ open: true, sonarr: null })
|
||||
}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.addsonarr} />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<svg
|
||||
className="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.addsonarr} />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useIntl, defineMessages } from 'react-intl';
|
||||
const messages = defineMessages({
|
||||
createsonarr: 'Create New Sonarr Server',
|
||||
editsonarr: 'Edit Sonarr Server',
|
||||
validationNameRequired: 'You must provide a server name',
|
||||
validationHostnameRequired: 'You must provide a hostname/IP',
|
||||
validationPortRequired: 'You must provide a port',
|
||||
validationApiKeyRequired: 'You must provide an API key',
|
||||
@@ -35,10 +36,16 @@ const messages = defineMessages({
|
||||
baseUrlPlaceholder: 'Example: /sonarr',
|
||||
qualityprofile: 'Quality Profile',
|
||||
rootfolder: 'Root Folder',
|
||||
animequalityprofile: 'Anime Quality Profile',
|
||||
animerootfolder: 'Anime Root Folder',
|
||||
seasonfolders: 'Season Folders',
|
||||
server4k: '4K Server',
|
||||
selectQualityProfile: 'Select a Quality Profile',
|
||||
selectRootFolder: 'Select a Root Folder',
|
||||
loadingprofiles: 'Loading quality profiles…',
|
||||
testFirstQualityProfiles: 'Test your connection to load quality profiles',
|
||||
loadingrootfolders: 'Loading root folders…',
|
||||
testFirstRootFolders: 'Test your connection to load root folders',
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
@@ -73,6 +80,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
rootFolders: [],
|
||||
});
|
||||
const SonarrSettingsSchema = Yup.object().shape({
|
||||
name: Yup.string().required(
|
||||
intl.formatMessage(messages.validationNameRequired)
|
||||
),
|
||||
hostname: Yup.string().required(
|
||||
intl.formatMessage(messages.validationHostnameRequired)
|
||||
),
|
||||
@@ -111,7 +121,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
{
|
||||
hostname,
|
||||
apiKey,
|
||||
port,
|
||||
port: Number(port),
|
||||
baseUrl,
|
||||
useSsl,
|
||||
}
|
||||
@@ -174,6 +184,8 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
baseUrl: sonarr?.baseUrl,
|
||||
activeProfileId: sonarr?.activeProfileId,
|
||||
rootFolder: sonarr?.activeDirectory,
|
||||
activeAnimeProfileId: sonarr?.activeAnimeProfileId,
|
||||
activeAnimeRootFolder: sonarr?.activeAnimeDirectory,
|
||||
isDefault: sonarr?.isDefault ?? false,
|
||||
is4k: sonarr?.is4k ?? false,
|
||||
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
||||
@@ -184,17 +196,25 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
const profileName = testResponse.profiles.find(
|
||||
(profile) => profile.id === Number(values.activeProfileId)
|
||||
)?.name;
|
||||
const animeProfileName = testResponse.profiles.find(
|
||||
(profile) => profile.id === Number(values.activeAnimeProfileId)
|
||||
)?.name;
|
||||
|
||||
const submission = {
|
||||
name: values.name,
|
||||
hostname: values.hostname,
|
||||
port: values.port,
|
||||
port: Number(values.port),
|
||||
apiKey: values.apiKey,
|
||||
useSsl: values.ssl,
|
||||
baseUrl: values.baseUrl,
|
||||
activeProfileId: values.activeProfileId,
|
||||
activeProfileId: Number(values.activeProfileId),
|
||||
activeProfileName: profileName,
|
||||
activeDirectory: values.rootFolder,
|
||||
activeAnimeProfileId: values.activeAnimeProfileId
|
||||
? Number(values.activeAnimeProfileId)
|
||||
: undefined,
|
||||
activeAnimeProfileName: animeProfileName ?? undefined,
|
||||
activeAnimeDirectory: values.activeAnimeRootFolder,
|
||||
is4k: values.is4k,
|
||||
isDefault: values.isDefault,
|
||||
enableSeasonFolders: values.enableSeasonFolders,
|
||||
@@ -221,6 +241,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -253,7 +274,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
secondaryDisabled={
|
||||
!values.apiKey || !values.hostname || !values.port || isTesting
|
||||
}
|
||||
okDisabled={!isValidated || isSubmitting || isTesting}
|
||||
okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
|
||||
onOk={() => handleSubmit()}
|
||||
title={
|
||||
!sonarr
|
||||
@@ -315,6 +336,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-600 text-gray-100 sm:text-sm cursor-default">
|
||||
{values.ssl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
@@ -324,7 +348,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
setIsValidated(false);
|
||||
setFieldValue('hostname', e.target.value);
|
||||
}}
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname && touched.hostname && (
|
||||
@@ -445,10 +469,17 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
as="select"
|
||||
id="activeProfileId"
|
||||
name="activeProfileId"
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
|
||||
disabled={!isValidated || isTesting}
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectQualityProfile)}
|
||||
{isTesting
|
||||
? intl.formatMessage(messages.loadingprofiles)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstQualityProfiles
|
||||
)
|
||||
: intl.formatMessage(messages.selectQualityProfile)}
|
||||
</option>
|
||||
{testResponse.profiles.length > 0 &&
|
||||
testResponse.profiles.map((profile) => (
|
||||
@@ -481,10 +512,15 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
as="select"
|
||||
id="rootFolder"
|
||||
name="rootFolder"
|
||||
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
|
||||
disabled={!isValidated || isTesting}
|
||||
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectRootFolder)}
|
||||
{isTesting
|
||||
? intl.formatMessage(messages.loadingrootfolders)
|
||||
: !isValidated
|
||||
? intl.formatMessage(messages.testFirstRootFolders)
|
||||
: intl.formatMessage(messages.selectRootFolder)}
|
||||
</option>
|
||||
{testResponse.rootFolders.length > 0 &&
|
||||
testResponse.rootFolders.map((folder) => (
|
||||
@@ -504,6 +540,92 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||
<label
|
||||
htmlFor="activeAnimeProfileId"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
{intl.formatMessage(messages.animequalityprofile)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<Field
|
||||
as="select"
|
||||
id="activeAnimeProfileId"
|
||||
name="activeAnimeProfileId"
|
||||
disabled={!isValidated || isTesting}
|
||||
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{isTesting
|
||||
? intl.formatMessage(messages.loadingprofiles)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstQualityProfiles
|
||||
)
|
||||
: intl.formatMessage(messages.selectQualityProfile)}
|
||||
</option>
|
||||
{testResponse.profiles.length > 0 &&
|
||||
testResponse.profiles.map((profile) => (
|
||||
<option
|
||||
key={`loaded-profile-${profile.id}`}
|
||||
value={profile.id}
|
||||
>
|
||||
{profile.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.activeAnimeProfileId &&
|
||||
touched.activeAnimeProfileId && (
|
||||
<div className="text-red-500 mt-2">
|
||||
{errors.activeAnimeProfileId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||
<label
|
||||
htmlFor="activeAnimeRootFolder"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||
>
|
||||
{intl.formatMessage(messages.animerootfolder)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||
<Field
|
||||
as="select"
|
||||
id="activeAnimeRootFolder"
|
||||
name="activeAnimeRootFolder"
|
||||
disabled={!isValidated || isTesting}
|
||||
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{isTesting
|
||||
? intl.formatMessage(messages.loadingrootfolders)
|
||||
: !isValidated
|
||||
? intl.formatMessage(messages.testFirstRootFolders)
|
||||
: intl.formatMessage(messages.selectRootFolder)}
|
||||
</option>
|
||||
{testResponse.rootFolders.length > 0 &&
|
||||
testResponse.rootFolders.map((folder) => (
|
||||
<option
|
||||
key={`loaded-profile-${folder.id}`}
|
||||
value={folder.path}
|
||||
>
|
||||
{folder.path}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.activeAnimeRootFolder &&
|
||||
touched.activeAnimeRootFolder && (
|
||||
<div className="text-red-500 mt-2">
|
||||
{errors.rootFolder}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||
<label
|
||||
htmlFor="is4k"
|
||||
|
||||
@@ -8,6 +8,8 @@ import LoginWithPlex from './LoginWithPlex';
|
||||
import SetupSteps from './SetupSteps';
|
||||
import axios from 'axios';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import Badge from '../Common/Badge';
|
||||
import LanguagePicker from '../Layout/LanguagePicker';
|
||||
|
||||
const messages = defineMessages({
|
||||
finish: 'Finish Setup',
|
||||
@@ -16,6 +18,9 @@ const messages = defineMessages({
|
||||
loginwithplex: 'Login with Plex',
|
||||
configureplex: 'Configure Plex',
|
||||
configureservices: 'Configure Services',
|
||||
tip: 'Tip',
|
||||
syncingbackground:
|
||||
'Syncing will run in the background. You can continue the setup process in the meantime.',
|
||||
});
|
||||
|
||||
const Setup: React.FC = () => {
|
||||
@@ -47,7 +52,10 @@ const Setup: React.FC = () => {
|
||||
'/images/rotate4.jpg',
|
||||
]}
|
||||
/>
|
||||
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-4xl relative z-50">
|
||||
<div className="absolute top-4 right-4 z-50">
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-4xl relative z-40">
|
||||
<img
|
||||
src="/logo.png"
|
||||
className="mx-auto max-h-32 w-auto mb-10"
|
||||
@@ -85,6 +93,12 @@ const Setup: React.FC = () => {
|
||||
{currentStep === 2 && (
|
||||
<div>
|
||||
<SettingsPlex onComplete={() => setPlexSettingsComplete(true)} />
|
||||
<div className="mt-4 text-gray-500 text-sm">
|
||||
<span className="mr-2">
|
||||
<Badge>{intl.formatMessage(messages.tip)}</Badge>
|
||||
</span>
|
||||
{intl.formatMessage(messages.syncingbackground)}
|
||||
</div>
|
||||
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -10,17 +10,18 @@ import Link from 'next/link';
|
||||
import { MediaStatus } from '../../../server/constants/media';
|
||||
import RequestModal from '../RequestModal';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIsTouch } from '../../hooks/useIsTouch';
|
||||
|
||||
const messages = defineMessages({
|
||||
movie: 'MOVIE',
|
||||
tvshow: 'SERIES',
|
||||
movie: 'Movie',
|
||||
tvshow: 'Series',
|
||||
});
|
||||
|
||||
interface TitleCardProps {
|
||||
id: number;
|
||||
image?: string;
|
||||
summary?: string;
|
||||
year: string;
|
||||
year?: string;
|
||||
title: string;
|
||||
userScore: number;
|
||||
mediaType: MediaType;
|
||||
@@ -38,6 +39,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
||||
mediaType,
|
||||
canExpand = false,
|
||||
}) => {
|
||||
const isTouch = useIsTouch();
|
||||
const intl = useIntl();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [currentStatus, setCurrentStatus] = useState(status);
|
||||
@@ -74,9 +76,13 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
||||
<div
|
||||
className="titleCard outline-none cursor-default"
|
||||
style={{
|
||||
backgroundImage: `url(//image.tmdb.org/t/p/w600_and_h900_bestv2${image})`,
|
||||
backgroundImage: `url(//image.tmdb.org/t/p/w300_and_h450_face${image})`,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!isTouch) {
|
||||
setShowDetail(true);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setShowDetail(true)}
|
||||
onMouseLeave={() => setShowDetail(false)}
|
||||
onClick={() => setShowDetail(true)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -146,158 +152,162 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
||||
|
||||
<Transition
|
||||
show={!image || showDetail || showRequestModal}
|
||||
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||
enter="transition transform opacity-0"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in-out duration-300 transform opacity-100"
|
||||
leave="transition transform opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="absolute w-full text-left top-0 right-0 left-0 bottom-0 rounded-lg overflow-hidden"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(45, 55, 72, 0.4) 0%, rgba(45, 55, 72, 0.9) 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute bottom-0 w-full left-0 right-0">
|
||||
<div className="px-2 text-white">
|
||||
<div className="text-sm">{year}</div>
|
||||
<Link href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}>
|
||||
<a
|
||||
className="absolute w-full text-left top-0 right-0 left-0 bottom-0 rounded-lg overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(45, 55, 72, 0.4) 0%, rgba(45, 55, 72, 0.9) 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute bottom-0 w-full left-0 right-0">
|
||||
<div className="px-2 text-white">
|
||||
{year && <div className="text-sm">{year}</div>}
|
||||
|
||||
<h1 className="text-xl leading-tight whitespace-normal">
|
||||
{title}
|
||||
</h1>
|
||||
<div
|
||||
className="text-xs whitespace-normal"
|
||||
style={{
|
||||
WebkitLineClamp: 3,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{summary}
|
||||
<h1 className="text-xl leading-tight whitespace-normal">
|
||||
{title}
|
||||
</h1>
|
||||
<div
|
||||
className="text-xs whitespace-normal"
|
||||
style={{
|
||||
WebkitLineClamp: 3,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between left-0 bottom-0 right-0 top-0 px-2 py-2">
|
||||
<Link
|
||||
href={
|
||||
mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`
|
||||
}
|
||||
>
|
||||
<a className="cursor-pointer flex w-full h-7 text-center text-white bg-indigo-500 rounded-sm hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150">
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
{(!currentStatus ||
|
||||
currentStatus === MediaStatus.UNKNOWN) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowRequestModal(true);
|
||||
}}
|
||||
className="w-full h-7 text-center text-white bg-indigo-500 rounded-sm ml-2 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150"
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{currentStatus === MediaStatus.PENDING && (
|
||||
<button
|
||||
className="w-full h-7 text-center text-yellow-500 border border-yellow-500 rounded-sm ml-2 cursor-default"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{currentStatus === MediaStatus.PROCESSING && (
|
||||
<button
|
||||
className="w-full h-7 text-center text-red-500 border border-red-500 rounded-sm ml-2 cursor-default"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{(currentStatus === MediaStatus.AVAILABLE ||
|
||||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
|
||||
<button
|
||||
className="w-full h-7 text-center text-green-400 border border-green-400 rounded-sm ml-2 cursor-default"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between left-0 bottom-0 right-0 top-0 px-2 py-2">
|
||||
<Link
|
||||
href={
|
||||
mediaType === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'
|
||||
}
|
||||
as={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}
|
||||
>
|
||||
<a className="cursor-pointer flex w-full h-7 text-center text-white bg-indigo-500 rounded-sm hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150">
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
{(!currentStatus ||
|
||||
currentStatus === MediaStatus.UNKNOWN) && (
|
||||
<button
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
className="w-full h-7 text-center text-white bg-indigo-500 rounded-sm ml-2 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 transition ease-in-out duration-150"
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{currentStatus === MediaStatus.PENDING && (
|
||||
<button
|
||||
className="w-full h-7 text-center text-yellow-500 border border-yellow-500 rounded-sm ml-2 cursor-default"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{currentStatus === MediaStatus.PROCESSING && (
|
||||
<button
|
||||
className="w-full h-7 text-center text-red-500 border border-red-500 rounded-sm ml-2 cursor-default"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{(currentStatus === MediaStatus.AVAILABLE ||
|
||||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
|
||||
<button
|
||||
className="w-full h-7 text-center text-green-400 border border-green-400 rounded-sm ml-2 cursor-default"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,8 @@ import RTAudRotten from '../../assets/rt_aud_rotten.svg';
|
||||
import type { RTRating } from '../../../server/api/rottentomatoes';
|
||||
import Head from 'next/head';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb';
|
||||
import ExternalLinkBlock from '../ExternalLinkBlock';
|
||||
|
||||
const messages = defineMessages({
|
||||
userrating: 'User Rating',
|
||||
@@ -56,6 +58,9 @@ const messages = defineMessages({
|
||||
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
|
||||
approve: 'Approve',
|
||||
decline: 'Decline',
|
||||
showtype: 'Show Type',
|
||||
anime: 'Anime',
|
||||
network: 'Network',
|
||||
});
|
||||
|
||||
interface TvDetailsProps {
|
||||
@@ -133,6 +138,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isComplete =
|
||||
data.seasons.filter((season) => season.seasonNumber !== 0).length <=
|
||||
(
|
||||
data.mediaInfo?.seasons.filter(
|
||||
(season) => season.status === MediaStatus.AVAILABLE
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-cover bg-center -mx-4 -mt-2 px-4 sm:px-8 pt-4 "
|
||||
@@ -196,14 +209,14 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
)}
|
||||
</SlideOver>
|
||||
<div className="flex flex-col items-center md:flex-row md:items-end pt-4">
|
||||
<div className="mr-4 flex-shrink-0">
|
||||
<div className="md:mr-4 flex-shrink-0">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
|
||||
alt=""
|
||||
className="rounded md:rounded-lg shadow md:shadow-2xl w-32 md:w-52"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
|
||||
<div className="text-white flex flex-col md:mr-4 mt-4 md:mt-0 text-center md:text-left">
|
||||
<div className="mb-2">
|
||||
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
|
||||
<Badge badgeType="success">
|
||||
@@ -227,8 +240,12 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-4xl">
|
||||
{data.name}{' '}
|
||||
<span className="text-2xl">({data.firstAirDate.slice(0, 4)})</span>
|
||||
<span>{data.name}</span>
|
||||
{data.firstAirDate && (
|
||||
<span className="text-2xl ml-2">
|
||||
({data.firstAirDate.slice(0, 4)})
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<span className="text-xs md:text-base mt-1 md:mt-0">
|
||||
{data.genres.map((g) => g.name).join(', ')}
|
||||
@@ -258,93 +275,95 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
<FormattedMessage {...messages.request} />
|
||||
</Button>
|
||||
)}
|
||||
{data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && (
|
||||
<ButtonWithDropdown
|
||||
dropdownIcon={
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
text={
|
||||
<>
|
||||
{data.mediaInfo &&
|
||||
data.mediaInfo.status !== MediaStatus.UNKNOWN &&
|
||||
!isComplete && (
|
||||
<ButtonWithDropdown
|
||||
dropdownIcon={
|
||||
<svg
|
||||
className="w-4 mr-1"
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.requestmore} />
|
||||
</>
|
||||
}
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
activeRequests &&
|
||||
activeRequests.length > 0 && (
|
||||
}
|
||||
text={
|
||||
<>
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => modifyRequests('approve')}
|
||||
<svg
|
||||
className="w-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
className="w-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage
|
||||
{...messages.approverequests}
|
||||
values={{ requestCount: activeRequests.length }}
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</ButtonWithDropdown.Item>
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => modifyRequests('decline')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage
|
||||
{...messages.declinerequests}
|
||||
values={{ requestCount: activeRequests.length }}
|
||||
/>
|
||||
</ButtonWithDropdown.Item>
|
||||
</svg>
|
||||
<FormattedMessage {...messages.requestmore} />
|
||||
</>
|
||||
)}
|
||||
</ButtonWithDropdown>
|
||||
)}
|
||||
}
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
activeRequests &&
|
||||
activeRequests.length > 0 && (
|
||||
<>
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => modifyRequests('approve')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage
|
||||
{...messages.approverequests}
|
||||
values={{ requestCount: activeRequests.length }}
|
||||
/>
|
||||
</ButtonWithDropdown.Item>
|
||||
<ButtonWithDropdown.Item
|
||||
onClick={() => modifyRequests('decline')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedMessage
|
||||
{...messages.declinerequests}
|
||||
values={{ requestCount: activeRequests.length }}
|
||||
/>
|
||||
</ButtonWithDropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</ButtonWithDropdown>
|
||||
)}
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="default"
|
||||
className="ml-2"
|
||||
className="ml-2 first:ml-0"
|
||||
onClick={() => setShowManager(true)}
|
||||
>
|
||||
<svg
|
||||
@@ -387,34 +406,36 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
<div className="bg-gray-900 rounded-lg shadow border border-gray-800">
|
||||
{(data.voteCount > 0 || ratingData) && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0 items-center justify-center">
|
||||
{ratingData?.criticsRating && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.criticsRating === 'Rotten' ? (
|
||||
<RTRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.criticsScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ratingData?.audienceRating && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.audienceRating === 'Spilled' ? (
|
||||
<RTAudRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTAudFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.audienceScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ratingData?.criticsRating &&
|
||||
(ratingData?.criticsScore ?? 0) > 0 && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.criticsRating === 'Rotten' ? (
|
||||
<RTRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.criticsScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{ratingData?.audienceRating &&
|
||||
(ratingData?.audienceScore ?? 0) > 0 && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
{ratingData.audienceRating === 'Spilled' ? (
|
||||
<RTAudRotten className="w-6 mr-1" />
|
||||
) : (
|
||||
<RTAudFresh className="w-6 mr-1" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm mr-4 last:mr-0">
|
||||
{ratingData.audienceScore}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{data.voteCount > 0 && (
|
||||
<>
|
||||
<span className="text-sm">
|
||||
@@ -427,6 +448,18 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{data.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
) && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
{intl.formatMessage(messages.showtype)}
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{intl.formatMessage(messages.anime)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.status} />
|
||||
@@ -435,14 +468,40 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
{data.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.originallanguage} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{data.originalLanguage}
|
||||
</span>
|
||||
</div>
|
||||
{data.spokenLanguages.some(
|
||||
(lng) => lng.iso_639_1 === data.originalLanguage
|
||||
) && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.originallanguage} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{
|
||||
data.spokenLanguages.find(
|
||||
(lng) => lng.iso_639_1 === data.originalLanguage
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.networks.length > 0 && (
|
||||
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
|
||||
<span className="text-sm">
|
||||
<FormattedMessage {...messages.network} />
|
||||
</span>
|
||||
<span className="flex-1 text-right text-gray-400 text-sm">
|
||||
{data.networks.map((n) => n.name).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<ExternalLinkBlock
|
||||
mediaType="tv"
|
||||
tmdbId={data.id}
|
||||
imdbId={data.externalIds.imdbId}
|
||||
rtUrl={ratingData?.url}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,13 +23,12 @@ const messages = defineMessages({
|
||||
'Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.',
|
||||
settings: 'Manage Settings',
|
||||
settingsDescription:
|
||||
'Grants permission to modify all Overseerr settings. User must have this permission to be able to grant it to others.',
|
||||
'Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.',
|
||||
managerequests: 'Manage Requests',
|
||||
managerequestsDescription:
|
||||
'Grants permission to manage Overseerr requests. This includes approving and denying requests.',
|
||||
request: 'Request',
|
||||
requestDescription:
|
||||
'Grants permission to make requests for movies or tv shows.',
|
||||
requestDescription: 'Grants permission to request movies and series.',
|
||||
vote: 'Vote',
|
||||
voteDescription:
|
||||
'Grants permission to vote on requests (voting not yet implemented)',
|
||||
@@ -38,7 +37,7 @@ const messages = defineMessages({
|
||||
'Grants auto approval for any requests made by this user.',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
usersaved: 'User succesfully saved',
|
||||
usersaved: 'User saved',
|
||||
userfail: 'Something went wrong saving the user.',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import type { User } from '../../../server/entity/User';
|
||||
@@ -10,6 +10,11 @@ import { Permission } from '../../hooks/useUser';
|
||||
import { useRouter } from 'next/router';
|
||||
import Header from '../Common/Header';
|
||||
import Table from '../Common/Table';
|
||||
import Transition from '../Transition';
|
||||
import Modal from '../Common/Modal';
|
||||
import axios from 'axios';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
userlist: 'User List',
|
||||
@@ -24,12 +29,46 @@ const messages = defineMessages({
|
||||
admin: 'Admin',
|
||||
user: 'User',
|
||||
plexuser: 'Plex User',
|
||||
deleteuser: 'Delete User',
|
||||
userdeleted: 'User deleted',
|
||||
userdeleteerror: 'Something went wrong deleting the user',
|
||||
deleteconfirm:
|
||||
'Are you sure you want to delete this user? All existing request data from this user will be removed.',
|
||||
});
|
||||
|
||||
const UserList: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const router = useRouter();
|
||||
const { data, error } = useSWR<User[]>('/api/v1/user');
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR<User[]>('/api/v1/user');
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState<{
|
||||
isOpen: boolean;
|
||||
user?: User;
|
||||
}>({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
const deleteUser = async () => {
|
||||
setDeleting(true);
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/v1/user/${deleteModal.user?.id}`);
|
||||
|
||||
addToast(intl.formatMessage(messages.userdeleted), {
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
});
|
||||
setDeleteModal({ isOpen: false });
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.userdeleteerror), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -37,6 +76,46 @@ const UserList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={deleteModal.isOpen}
|
||||
>
|
||||
<Modal
|
||||
onOk={() => deleteUser()}
|
||||
okText={
|
||||
isDeleting
|
||||
? intl.formatMessage(globalMessages.deleting)
|
||||
: intl.formatMessage(globalMessages.delete)
|
||||
}
|
||||
okDisabled={isDeleting}
|
||||
okButtonType="danger"
|
||||
onCancel={() => setDeleteModal({ isOpen: false })}
|
||||
title={intl.formatMessage(messages.deleteuser)}
|
||||
iconSvg={
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(messages.deleteconfirm)}
|
||||
</Modal>
|
||||
</Transition>
|
||||
<Header extraMargin={4}>{intl.formatMessage(messages.userlist)}</Header>
|
||||
<Table>
|
||||
<thead>
|
||||
@@ -104,7 +183,11 @@ const UserList: React.FC = () => {
|
||||
>
|
||||
{intl.formatMessage(messages.edit)}
|
||||
</Button>
|
||||
<Button buttonType="danger">
|
||||
<Button
|
||||
buttonType="danger"
|
||||
disabled={hasPermission(Permission.ADMIN, user.permissions)}
|
||||
onClick={() => setDeleteModal({ isOpen: true, user })}
|
||||
>
|
||||
{intl.formatMessage(messages.delete)}
|
||||
</Button>
|
||||
</Table.TD>
|
||||
|
||||
20
src/context/InteractionContext.tsx
Normal file
20
src/context/InteractionContext.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import useInteraction from '../hooks/useInteraction';
|
||||
|
||||
interface InteractionContextProps {
|
||||
isTouch: boolean;
|
||||
}
|
||||
|
||||
export const InteractionContext = React.createContext<InteractionContextProps>({
|
||||
isTouch: false,
|
||||
});
|
||||
|
||||
export const InteractionProvider: React.FC = ({ children }) => {
|
||||
const isTouch = useInteraction();
|
||||
|
||||
return (
|
||||
<InteractionContext.Provider value={{ isTouch }}>
|
||||
{children}
|
||||
</InteractionContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,12 @@ export type AvailableLocales =
|
||||
| 'nb-NO'
|
||||
| 'de'
|
||||
| 'ru'
|
||||
| 'nl';
|
||||
| 'nl'
|
||||
| 'es'
|
||||
| 'it'
|
||||
| 'pt-BR'
|
||||
| 'sr'
|
||||
| 'sv';
|
||||
|
||||
interface LanguageContextProps {
|
||||
locale: AvailableLocales;
|
||||
|
||||
@@ -15,7 +15,7 @@ const useClickOutside = (
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
const handleBodyClick = (e: MouseEvent) => {
|
||||
if (!ref.current?.contains(e.target as Node)) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
78
src/hooks/useInteraction.ts
Normal file
78
src/hooks/useInteraction.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const INTERACTION_TYPE = {
|
||||
MOUSE: 'mouse',
|
||||
PEN: 'pen',
|
||||
TOUCH: 'touch',
|
||||
};
|
||||
|
||||
const UPDATE_INTERVAL = 1000; // Throttle updates to the type to prevent flip flopping
|
||||
|
||||
const useInteraction = (): boolean => {
|
||||
const [isTouch, setIsTouch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasTapEvent = 'ontouchstart' in window;
|
||||
setIsTouch(hasTapEvent);
|
||||
|
||||
let localTouch = hasTapEvent;
|
||||
let lastTouchUpdate = Date.now();
|
||||
|
||||
const shouldUpdate = (): boolean =>
|
||||
lastTouchUpdate + UPDATE_INTERVAL < Date.now();
|
||||
|
||||
const onMouseMove = (): void => {
|
||||
if (localTouch && shouldUpdate()) {
|
||||
setTimeout(() => {
|
||||
if (shouldUpdate()) {
|
||||
setIsTouch(false);
|
||||
localTouch = false;
|
||||
}
|
||||
}, UPDATE_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchStart = (): void => {
|
||||
lastTouchUpdate = Date.now();
|
||||
|
||||
if (!localTouch) {
|
||||
setIsTouch(true);
|
||||
localTouch = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent): void => {
|
||||
const { pointerType } = e;
|
||||
|
||||
switch (pointerType) {
|
||||
case INTERACTION_TYPE.TOUCH:
|
||||
case INTERACTION_TYPE.PEN:
|
||||
return onTouchStart();
|
||||
default:
|
||||
return onMouseMove();
|
||||
}
|
||||
};
|
||||
|
||||
if (hasTapEvent) {
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('touchstart', onTouchStart);
|
||||
} else {
|
||||
window.addEventListener('pointerdown', onPointerMove);
|
||||
window.addEventListener('pointermove', onPointerMove);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hasTapEvent) {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('touchstart', onTouchStart);
|
||||
} else {
|
||||
window.removeEventListener('pointerdown', onPointerMove);
|
||||
window.removeEventListener('pointermove', onPointerMove);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isTouch;
|
||||
};
|
||||
|
||||
export default useInteraction;
|
||||
7
src/hooks/useIsTouch.ts
Normal file
7
src/hooks/useIsTouch.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { InteractionContext } from '../context/InteractionContext';
|
||||
|
||||
export const useIsTouch = (): boolean => {
|
||||
const { isTouch } = useContext(InteractionContext);
|
||||
return isTouch;
|
||||
};
|
||||
@@ -7,6 +7,10 @@ import type { Nullable } from '../utils/typeHelpers';
|
||||
|
||||
type Url = string | UrlObject;
|
||||
|
||||
const encodeURIExtraParams = (string: string): string => {
|
||||
return encodeURIComponent(string).replace(/!/g, '%21');
|
||||
};
|
||||
|
||||
interface SearchObject {
|
||||
searchValue: string;
|
||||
searchOpen: boolean;
|
||||
@@ -35,14 +39,17 @@ const useSearchInput = (): SearchObject => {
|
||||
if (router.pathname.startsWith('/search')) {
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, query: debouncedValue },
|
||||
query: {
|
||||
...router.query,
|
||||
query: encodeURIExtraParams(debouncedValue),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setLastRoute(router.asPath);
|
||||
router
|
||||
.push({
|
||||
pathname: '/search',
|
||||
query: { query: debouncedValue },
|
||||
query: { query: encodeURIExtraParams(debouncedValue) },
|
||||
})
|
||||
.then(() => window.scrollTo(0, 0));
|
||||
}
|
||||
@@ -85,8 +92,12 @@ const useSearchInput = (): SearchObject => {
|
||||
* is on /search
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (router.query.query !== debouncedValue) {
|
||||
setSearchValue((router.query.query as string) ?? '');
|
||||
if (router.query.query !== encodeURIExtraParams(debouncedValue)) {
|
||||
setSearchValue(
|
||||
router.query.query
|
||||
? decodeURIComponent(router.query.query as string)
|
||||
: ''
|
||||
);
|
||||
|
||||
if (!router.pathname.startsWith('/search') && !router.query.query) {
|
||||
setIsOpen(false);
|
||||
|
||||
@@ -14,6 +14,8 @@ const globalMessages = defineMessages({
|
||||
approve: 'Approve',
|
||||
decline: 'Decline',
|
||||
delete: 'Delete',
|
||||
deleting: 'Deleting…',
|
||||
close: 'Close',
|
||||
});
|
||||
|
||||
export default globalMessages;
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"components.Discover.discovermovies": "Beliebte Filme",
|
||||
"components.Discover.discovertv": "Beliebte Serie",
|
||||
"components.Discover.nopending": "Keine ausstehenden Anträge",
|
||||
"components.Discover.discovertv": "Beliebte Serien",
|
||||
"components.Discover.nopending": "Keine ausstehenden Anfragen",
|
||||
"components.Discover.popularmovies": "Beliebte Filme",
|
||||
"components.Discover.populartv": "Beliebte Serie",
|
||||
"components.Discover.populartv": "Beliebte Serien",
|
||||
"components.Discover.recentlyAdded": "Kürzlich hinzugefügt",
|
||||
"components.Discover.recentrequests": "Letzte Anträge",
|
||||
"components.Discover.recentrequests": "Aktuelle Anfragen",
|
||||
"components.Discover.trending": "Trends",
|
||||
"components.Discover.upcoming": "Bald erscheinende Filme",
|
||||
"components.Discover.upcomingmovies": "Bald erscheinende Filme",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Sprache ändern",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Nach Filme und Fernsehsendungen suchen",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Nach Filmen und Serien suchen",
|
||||
"components.Layout.Sidebar.dashboard": "Entdecken",
|
||||
"components.Layout.Sidebar.requests": "Anträge",
|
||||
"components.Layout.Sidebar.settings": "Einstellungen",
|
||||
"components.Layout.Sidebar.users": "Benutzer",
|
||||
"components.Layout.UserDropdown.signout": "Abmelden",
|
||||
"components.Layout.alphawarning": "Dies ist ALPHA-Software. Fast alles muss fast kaputt und/oder instabil sein. Bitte melden Sie Probleme auf der GitHub-Seite!",
|
||||
"components.Layout.alphawarning": "Dies ist ALPHA-Software. Fast alles kann kaputt und/oder instabil sein. Bitte melde Probleme auf der GitHub-Seite!",
|
||||
"components.Login.signinplex": "Melden Sie sich an, um fortzufahren",
|
||||
"components.MovieDetails.approve": "Genehmigen",
|
||||
"components.MovieDetails.available": "Verfügbar",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.cancelrequest": "Antrag abbrechen",
|
||||
"components.MovieDetails.cancelrequest": "Anfrage zurücknehmen",
|
||||
"components.MovieDetails.cast": "Besetzung",
|
||||
"components.MovieDetails.decline": "Ablehnen",
|
||||
"components.MovieDetails.manageModalClearMedia": "Alle Mediendaten löschen",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Dadurch werden alle Mediendaten einschließlich aller Anträge für dieses Element entfernt. Diese Aktion ist irreversibel. Wenn dieses Element in Ihrer Plex-Bibliothek vorhanden ist, werden die Medieninformationen bei der nächsten Synchronisierung neu erstellt.",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Dadurch werden alle Mediendaten einschließlich aller Anfragen für dieses Element entfernt. Diese Aktion ist irreversibel. Wenn dieses Element in Ihrer Plex-Bibliothek vorhanden ist, werden die Medieninformationen bei der nächsten Synchronisierung neu erstellt.",
|
||||
"components.MovieDetails.manageModalNoRequests": "Keine Anträge",
|
||||
"components.MovieDetails.manageModalRequests": "Anträge",
|
||||
"components.MovieDetails.manageModalTitle": "Filme verwalten",
|
||||
"components.MovieDetails.manageModalRequests": "Anfragen",
|
||||
"components.MovieDetails.manageModalTitle": "Film verwalten",
|
||||
"components.MovieDetails.originallanguage": "Originalsprache",
|
||||
"components.MovieDetails.overview": "Übersicht",
|
||||
"components.MovieDetails.overviewunavailable": "Übersicht nicht verfügbar",
|
||||
"components.MovieDetails.pending": "Ausstehend",
|
||||
"components.MovieDetails.recommendations": "Empfehlungen",
|
||||
"components.MovieDetails.recommendationssubtext": "Wenn Ihnen {title} gefallen hat, könnte Ihnen auch gefallen …",
|
||||
"components.MovieDetails.recommendationssubtext": "Wenn dir {title} gefallen hat, könnte dir auch gefallen …",
|
||||
"components.MovieDetails.releasedate": "Erscheinungsdatum",
|
||||
"components.MovieDetails.request": "Antrag",
|
||||
"components.MovieDetails.request": "Anfrage",
|
||||
"components.MovieDetails.revenue": "Einnahmen",
|
||||
"components.MovieDetails.runtime": "{minutes} Minuten",
|
||||
"components.MovieDetails.similar": "Ähnliche Titel",
|
||||
@@ -44,7 +44,7 @@
|
||||
"components.MovieDetails.status": "Status",
|
||||
"components.MovieDetails.unavailable": "Nicht verfügbar",
|
||||
"components.MovieDetails.userrating": "Benutzerbewertung",
|
||||
"components.MovieDetails.viewrequest": "Antrag anzeigen",
|
||||
"components.MovieDetails.viewrequest": "Anfrage anzeigen",
|
||||
"components.PersonDetails.appearsin": "Erscheint in",
|
||||
"components.PersonDetails.ascharacter": "als {character}",
|
||||
"components.PersonDetails.nobiography": "Keine Biografie verfügbar.",
|
||||
@@ -53,43 +53,43 @@
|
||||
"components.PlexLoginButton.loginwithplex": "Anmeldung mit Plex",
|
||||
"components.RequestBlock.seasons": "Staffeln",
|
||||
"components.RequestCard.all": "Alle",
|
||||
"components.RequestCard.requestedby": "Antrag von {username}",
|
||||
"components.RequestCard.requestedby": "Anfrage von {username}",
|
||||
"components.RequestCard.seasons": "Staffeln",
|
||||
"components.RequestList.RequestItem.notavailable": "entf.",
|
||||
"components.RequestList.RequestItem.requestedby": "Antrag von {username}",
|
||||
"components.RequestList.RequestItem.requestedby": "Angefragt von {username}",
|
||||
"components.RequestList.RequestItem.seasons": "Staffeln",
|
||||
"components.RequestList.mediaInfo": "Medieninformationen",
|
||||
"components.RequestList.mediaInfo": "Medieninfos",
|
||||
"components.RequestList.modifiedBy": "Zuletzt geändert von",
|
||||
"components.RequestList.next": "Nächste",
|
||||
"components.RequestList.previous": "Vorherige",
|
||||
"components.RequestList.requestedAt": "Antrag erstellt am",
|
||||
"components.RequestList.requests": "Anträge",
|
||||
"components.RequestList.requestedAt": "Angefragt am",
|
||||
"components.RequestList.requests": "Anfragen",
|
||||
"components.RequestList.showingresults": "Anzeigen von <strong>{from}</strong> bis <strong> {to} </strong> von <strong> {total} </strong> Ergebnissen",
|
||||
"components.RequestList.status": "Status",
|
||||
"components.RequestModal.cancel": "Antrag abbrechen",
|
||||
"components.RequestModal.cancel": "Anfrage abbrechen",
|
||||
"components.RequestModal.cancelling": "Abbrechen …",
|
||||
"components.RequestModal.cancelrequest": "Dadurch wird Ihre Antrag entfernt. Sind Sie sicher, dass Sie weitermachen wollen?",
|
||||
"components.RequestModal.cancelrequest": "Dadurch wird deine Anfrage entfernt. Bist du dir sicher, dass du weitermachen willst?",
|
||||
"components.RequestModal.close": "Schließen",
|
||||
"components.RequestModal.extras": "Extras",
|
||||
"components.RequestModal.notrequested": "Nicht angefordert",
|
||||
"components.RequestModal.notrequested": "Nicht angefragt",
|
||||
"components.RequestModal.numberofepisodes": "Anzahl der Folgen",
|
||||
"components.RequestModal.pendingrequest": "Ausstehender Antrag für {title}",
|
||||
"components.RequestModal.request": "Antrag",
|
||||
"components.RequestModal.requestCancel": "Antrag für <strong>{title}</strong> storniert",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> angefordert.",
|
||||
"components.RequestModal.requestadmin": "Ihr Antrag wird sofort genehmigt.",
|
||||
"components.RequestModal.requestfrom": "Derzeit ist ein Antrag von {username} ausstehend",
|
||||
"components.RequestModal.requesting": "Antrag wird erstellt …",
|
||||
"components.RequestModal.requestseasons": "{seasonCount} {seasonCount, plural, one {Season} other {Seasons}} anfordern",
|
||||
"components.RequestModal.requesttitle": "{title} anfordern",
|
||||
"components.RequestModal.pendingrequest": "Ausstehende Anfrage für {title}",
|
||||
"components.RequestModal.request": "Anfrage",
|
||||
"components.RequestModal.requestCancel": "Anfrage für <strong>{title}</strong> abgebrochen",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> angefragt.",
|
||||
"components.RequestModal.requestadmin": "Deine Anfrage wird direkt genehmigt.",
|
||||
"components.RequestModal.requestfrom": "Derzeit steht eine Anfrage von {username} aus",
|
||||
"components.RequestModal.requesting": "Wird angefragt…",
|
||||
"components.RequestModal.requestseasons": "{seasonCount} {seasonCount, plural, one {Season} other {Seasons}} anfragen",
|
||||
"components.RequestModal.requesttitle": "{title} anfragen",
|
||||
"components.RequestModal.season": "Staffel",
|
||||
"components.RequestModal.seasonnumber": "Staffel {number}",
|
||||
"components.RequestModal.selectseason": "Staffel(n) auswählen",
|
||||
"components.RequestModal.status": "Status",
|
||||
"components.Search.searchresults": "Suchergebnisse",
|
||||
"components.Settings.Notifications.agentenabled": "Agent aktiviert",
|
||||
"components.Settings.Notifications.authPass": "Auth Pass",
|
||||
"components.Settings.Notifications.authUser": "Auth User",
|
||||
"components.Settings.Notifications.authPass": "Passwort",
|
||||
"components.Settings.Notifications.authUser": "Benutzername",
|
||||
"components.Settings.Notifications.emailsender": "E-Mail-Absenderadresse",
|
||||
"components.Settings.Notifications.enableSsl": "SSL aktivieren",
|
||||
"components.Settings.Notifications.save": "Änderungen speichern",
|
||||
@@ -205,7 +205,7 @@
|
||||
"components.Settings.runnow": "Jetzt ausführen",
|
||||
"components.Settings.save": "Änderungen speichern",
|
||||
"components.Settings.saving": "Speichern …",
|
||||
"components.Settings.servername": "Servername (automatisch festgelegt)",
|
||||
"components.Settings.servername": "Servername (Wird nach dem Speichern automatisch festgelegt)",
|
||||
"components.Settings.servernamePlaceholder": "Plex-Servername",
|
||||
"components.Settings.sonarrSettingsDescription": "Richten Sie unten Ihre Sonarr-Verbindung ein. Sie können jederzeit mehrere, aber nur zwei standardmäßig aktiv haben (eine für Standard-HD und eine für 4K). Administratoren können überschreiben, welcher Server für neue Anforderungen verwendet wird.",
|
||||
"components.Settings.sonarrsettings": "Sonarr-Einstellungen",
|
||||
@@ -300,9 +300,53 @@
|
||||
"pages.internalServerError": "{statusCode} – Interner Serverfehler",
|
||||
"pages.oops": "Hoppla",
|
||||
"pages.pageNotFound": "404 – Seite nicht gefunden",
|
||||
"pages.returnHome": "Zur Starseite",
|
||||
"pages.returnHome": "Zur Startseite",
|
||||
"pages.serviceUnavailable": "{statusCode} – Dienst nicht verfügbar",
|
||||
"pages.somethingWentWrong": "{statusCode} – Es ist ein Fehler aufgetreten",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Vollserienbesetzung",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Vollständige Besetzung"
|
||||
"components.MovieDetails.MovieCast.fullcast": "Vollständige Besetzung",
|
||||
"components.Settings.Notifications.emailsettingssaved": "E-Mail-Benachrichtigungseinstellungen gespeichert!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Fehler beim Speichern der E-Mail-Benachrichtigungseinstellungen.",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Discord-Benachrichtigungseinstellungen gespeichert!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Fehler beim Speichern der Discord-Benachrichtigungseinstellungen.",
|
||||
"components.Settings.validationPortRequired": "Sie müssen einen Port angeben",
|
||||
"components.Settings.validationHostnameRequired": "Sie müssen einen Hostnamen/IP angeben",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Sie müssen einen Servernamen angeben",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Anträge insgesamt",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Medien insgesamt",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr-Informationen",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub-Diskussionen",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Hilfe erhalten",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Klicken Sie hier, um unserem Discord-Server beizutreten.",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Sie müssen einen Servernamen angeben",
|
||||
"components.Setup.tip": "Tipp",
|
||||
"components.Setup.syncingbackground": "Die Synchronisierung wird im Hintergrund ausgeführt. Sie können die Konfiguration in der Zwischenzeit fortsetzen.",
|
||||
"i18n.deleting": "Löschen …",
|
||||
"components.UserList.userdeleteerror": "Beim Löschen des Benutzers ist ein Fehler aufgetreten",
|
||||
"components.UserList.userdeleted": "Benutzer gelöscht",
|
||||
"components.UserList.deleteuser": "Benutzer löschen",
|
||||
"components.UserList.deleteconfirm": "Wollen Sie diesen Benutzer wirklich löschen? Alle vorhandenen Anforderungsdaten dieses Benutzers werden entfernt.",
|
||||
"components.Settings.nodefaultdescription": "Mindestens ein Server muss als Standard markiert sein, bevor Anforderungen an Ihre Dienste weitergeleitet werden.",
|
||||
"components.Settings.nodefault": "Kein Standardserver ausgewählt!",
|
||||
"components.Settings.no4kimplemented": "(Standard-4K-Server sind derzeit nicht implementiert.)",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Testen Sie Ihre Verbindung, um Stammordner zu laden",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Testen Sie Ihre Verbindung, um Qualitätsprofile zu laden",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Laden von Stammordnern…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Qualitätsprofile werden geladen…",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Sie müssen die Mindestverfügbarkeit auswählen",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Testen Sie Ihre Verbindung, um Stammordner zu laden",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Testen Sie Ihre Verbindung zu Lastqualitätsprofilen",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Laden von Stammordnern…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Qualitätsprofile werden geladen…",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.Settings.toastApiKeySuccess": "Neuer API-Schlüssel generiert!",
|
||||
"components.TvDetails.showtype": "Serientyp",
|
||||
"components.TvDetails.network": "Netzwerk",
|
||||
"components.Settings.toastSettingsSuccess": "Einstellungen gespeichert.",
|
||||
"components.Settings.toastSettingsFailure": "Beim Speichern der Einstellungen ging etwas schief.",
|
||||
"components.Settings.toastApiKeyFailure": "Bei der Generierung eines neuen API-Schlüssels kam es zu einem Fehler.",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Animestammverzeichnis",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Animequalitätsprofil",
|
||||
"components.MovieDetails.studio": "Studio"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"components.MovieDetails.overviewunavailable": "Overview unavailable",
|
||||
"components.MovieDetails.pending": "Pending",
|
||||
"components.MovieDetails.recommendations": "Recommendations",
|
||||
"components.MovieDetails.recommendationssubtext": "If you liked {title}, you might also like...",
|
||||
"components.MovieDetails.recommendationssubtext": "If you liked {title}, you might also like…",
|
||||
"components.MovieDetails.releasedate": "Release Date",
|
||||
"components.MovieDetails.request": "Request",
|
||||
"components.MovieDetails.revenue": "Revenue",
|
||||
@@ -43,14 +43,15 @@
|
||||
"components.MovieDetails.similar": "Similar Titles",
|
||||
"components.MovieDetails.similarsubtext": "Other movies similar to {title}",
|
||||
"components.MovieDetails.status": "Status",
|
||||
"components.MovieDetails.studio": "Studio",
|
||||
"components.MovieDetails.unavailable": "Unavailable",
|
||||
"components.MovieDetails.userrating": "User Rating",
|
||||
"components.MovieDetails.viewrequest": "View Request",
|
||||
"components.PersonDetails.appearsin": "Appears in",
|
||||
"components.PersonDetails.ascharacter": "as {character}",
|
||||
"components.PersonDetails.nobiography": "No biography available.",
|
||||
"components.PlexLoginButton.loading": "Loading...",
|
||||
"components.PlexLoginButton.loggingin": "Logging in...",
|
||||
"components.PlexLoginButton.loading": "Loading…",
|
||||
"components.PlexLoginButton.loggingin": "Logging in…",
|
||||
"components.PlexLoginButton.loginwithplex": "Login with Plex",
|
||||
"components.RequestBlock.seasons": "Seasons",
|
||||
"components.RequestCard.all": "All",
|
||||
@@ -68,7 +69,7 @@
|
||||
"components.RequestList.showingresults": "Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
|
||||
"components.RequestList.status": "Status",
|
||||
"components.RequestModal.cancel": "Cancel Request",
|
||||
"components.RequestModal.cancelling": "Cancelling...",
|
||||
"components.RequestModal.cancelling": "Cancelling…",
|
||||
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
|
||||
"components.RequestModal.close": "Close",
|
||||
"components.RequestModal.extras": "Extras",
|
||||
@@ -91,10 +92,14 @@
|
||||
"components.Settings.Notifications.agentenabled": "Agent Enabled",
|
||||
"components.Settings.Notifications.authPass": "Auth Pass",
|
||||
"components.Settings.Notifications.authUser": "Auth User",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved!",
|
||||
"components.Settings.Notifications.emailsender": "Email Sender Address",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved!",
|
||||
"components.Settings.Notifications.enableSsl": "Enable SSL",
|
||||
"components.Settings.Notifications.save": "Save Changes",
|
||||
"components.Settings.Notifications.saving": "Saving...",
|
||||
"components.Settings.Notifications.saving": "Saving…",
|
||||
"components.Settings.Notifications.smtpHost": "SMTP Host",
|
||||
"components.Settings.Notifications.smtpPort": "SMTP Port",
|
||||
"components.Settings.Notifications.validationFromRequired": "You must provide an email sender address",
|
||||
@@ -112,12 +117,14 @@
|
||||
"components.Settings.RadarrModal.defaultserver": "Default Server",
|
||||
"components.Settings.RadarrModal.editradarr": "Edit Radarr Server",
|
||||
"components.Settings.RadarrModal.hostname": "Hostname",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Loading quality profiles…",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Loading root folders…",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Minimum Availability",
|
||||
"components.Settings.RadarrModal.port": "Port",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Quality Profile",
|
||||
"components.Settings.RadarrModal.rootfolder": "Root Folder",
|
||||
"components.Settings.RadarrModal.save": "Save Changes",
|
||||
"components.Settings.RadarrModal.saving": "Saving...",
|
||||
"components.Settings.RadarrModal.saving": "Saving…",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Select minimum availability",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Select a Quality Profile",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Select a Root Folder",
|
||||
@@ -126,15 +133,40 @@
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "A Radarr Server",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.test": "Test",
|
||||
"components.Settings.RadarrModal.testing": "Testing...",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test your connection to load quality profiles",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Test your connection to load root folders",
|
||||
"components.Settings.RadarrModal.testing": "Testing…",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Failed to connect to Radarr Server",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "You must provide a hostname/IP",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "You must select minimum availability",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "You must provide a server name",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "You must provide a port",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "You must select a profile",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "You must select a root folder",
|
||||
"components.Settings.SettingsAbout.Releases.currentversion": "Current Version",
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Latest Version",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Release data missing. Is GitHub down?",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Releases",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelop": "You are running a develop version of Overseerr!",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "The changes in your version will not be available below. Please look at the <GithubLink>GitHub repository</GithubLink> for latest updates.",
|
||||
"components.Settings.SettingsAbout.Releases.versionChangelog": "Version Changelog",
|
||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "View Changelog",
|
||||
"components.Settings.SettingsAbout.Releases.viewongithub": "View on GitHub",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Click here to join our Discord server.",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Getting Support",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Help pay for coffee",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr Information",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||
"components.Settings.SettingsAbout.timezone": "Timezone",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SonarrModal.add": "Add Server",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Anime Quality Profile",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Anime Root Folder",
|
||||
"components.Settings.SonarrModal.apiKey": "API Key",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "Your Sonarr API Key",
|
||||
"components.Settings.SonarrModal.baseUrl": "Base URL",
|
||||
@@ -143,11 +175,13 @@
|
||||
"components.Settings.SonarrModal.defaultserver": "Default Server",
|
||||
"components.Settings.SonarrModal.editsonarr": "Edit Sonarr Server",
|
||||
"components.Settings.SonarrModal.hostname": "Hostname",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Loading quality profiles…",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…",
|
||||
"components.Settings.SonarrModal.port": "Port",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Quality Profile",
|
||||
"components.Settings.SonarrModal.rootfolder": "Root Folder",
|
||||
"components.Settings.SonarrModal.save": "Save Changes",
|
||||
"components.Settings.SonarrModal.saving": "Saving...",
|
||||
"components.Settings.SonarrModal.saving": "Saving…",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Season Folders",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Select a Quality Profile",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Select a Root Folder",
|
||||
@@ -156,11 +190,14 @@
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "A Sonarr Server",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.test": "Test",
|
||||
"components.Settings.SonarrModal.testing": "Testing...",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test your connection to load quality profiles",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Test your connection to load root folders",
|
||||
"components.Settings.SonarrModal.testing": "Testing…",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Could not connect to Sonarr Server",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "You must provide an API key",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "You must provide a hostname/IP",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "You must provide a server name",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "You must provide a port",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "You must select a profile",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder",
|
||||
@@ -193,6 +230,9 @@
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuServices": "Services",
|
||||
"components.Settings.nextexecution": "Next Execution",
|
||||
"components.Settings.no4kimplemented": "(Default 4K servers are not currently implemented)",
|
||||
"components.Settings.nodefault": "No default server selected!",
|
||||
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
||||
"components.Settings.notificationsettings": "Notification Settings",
|
||||
"components.Settings.notificationsettingsDescription": "Here you can pick and choose what types of notifications to send and through what types of services.",
|
||||
"components.Settings.notrunning": "Not Running",
|
||||
@@ -205,8 +245,8 @@
|
||||
"components.Settings.radarrsettings": "Radarr Settings",
|
||||
"components.Settings.runnow": "Run Now",
|
||||
"components.Settings.save": "Save Changes",
|
||||
"components.Settings.saving": "Saving...",
|
||||
"components.Settings.servername": "Server Name (Automatically Set)",
|
||||
"components.Settings.saving": "Saving…",
|
||||
"components.Settings.servername": "Server Name (Automatically set after you save)",
|
||||
"components.Settings.servernamePlaceholder": "Plex Server Name",
|
||||
"components.Settings.sonarrSettingsDescription": "Set up your Sonarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD and one for 4K). Administrators can override which server is used for new requests.",
|
||||
"components.Settings.sonarrsettings": "Sonarr Settings",
|
||||
@@ -214,18 +254,27 @@
|
||||
"components.Settings.startscan": "Start Scan",
|
||||
"components.Settings.sync": "Sync Plex Libraries",
|
||||
"components.Settings.syncing": "Syncing…",
|
||||
"components.Settings.toastApiKeyFailure": "Something went wrong generating a new API Key.",
|
||||
"components.Settings.toastApiKeySuccess": "New API Key generated!",
|
||||
"components.Settings.toastSettingsFailure": "Something went wrong saving settings.",
|
||||
"components.Settings.toastSettingsSuccess": "Settings saved.",
|
||||
"components.Settings.validationHostnameRequired": "You must provide a hostname/IP",
|
||||
"components.Settings.validationPortRequired": "You must provide a port",
|
||||
"components.Setup.configureplex": "Configure Plex",
|
||||
"components.Setup.configureservices": "Configure Services",
|
||||
"components.Setup.continue": "Continue",
|
||||
"components.Setup.finish": "Finish Setup",
|
||||
"components.Setup.finishing": "Finishing...",
|
||||
"components.Setup.finishing": "Finishing…",
|
||||
"components.Setup.loginwithplex": "Login with Plex",
|
||||
"components.Setup.signinMessage": "Get started by logging in with your Plex account",
|
||||
"components.Setup.syncingbackground": "Syncing will run in the background. You can continue the setup process in the meantime.",
|
||||
"components.Setup.tip": "Tip",
|
||||
"components.Setup.welcome": "Welcome to Overseerr",
|
||||
"components.Slider.noresults": "No Results",
|
||||
"components.TitleCard.movie": "Movie",
|
||||
"components.TitleCard.tvshow": "Series",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.approve": "Approve",
|
||||
"components.TvDetails.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.available": "Available",
|
||||
@@ -238,14 +287,16 @@
|
||||
"components.TvDetails.manageModalNoRequests": "No Requests",
|
||||
"components.TvDetails.manageModalRequests": "Requests",
|
||||
"components.TvDetails.manageModalTitle": "Manage Series",
|
||||
"components.TvDetails.network": "Network",
|
||||
"components.TvDetails.originallanguage": "Original Language",
|
||||
"components.TvDetails.overview": "Overview",
|
||||
"components.TvDetails.overviewunavailable": "Overview unavailable",
|
||||
"components.TvDetails.pending": "Pending",
|
||||
"components.TvDetails.recommendations": "Recommendations",
|
||||
"components.TvDetails.recommendationssubtext": "If you liked {title}, you might also like...",
|
||||
"components.TvDetails.recommendationssubtext": "If you liked {title}, you might also like…",
|
||||
"components.TvDetails.request": "Request",
|
||||
"components.TvDetails.requestmore": "Request More",
|
||||
"components.TvDetails.showtype": "Show Type",
|
||||
"components.TvDetails.similar": "Similar Series",
|
||||
"components.TvDetails.similarsubtext": "Other series similar to {title}",
|
||||
"components.TvDetails.status": "Status",
|
||||
@@ -264,7 +315,7 @@
|
||||
"components.UserEdit.request": "Request",
|
||||
"components.UserEdit.requestDescription": "Grants permission to request movies and series.",
|
||||
"components.UserEdit.save": "Save",
|
||||
"components.UserEdit.saving": "Saving...",
|
||||
"components.UserEdit.saving": "Saving…",
|
||||
"components.UserEdit.settings": "Manage Settings",
|
||||
"components.UserEdit.settingsDescription": "Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.",
|
||||
"components.UserEdit.userfail": "Something went wrong saving the user.",
|
||||
@@ -277,12 +328,16 @@
|
||||
"components.UserList.admin": "Admin",
|
||||
"components.UserList.created": "Created",
|
||||
"components.UserList.delete": "Delete",
|
||||
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All existing request data from this user will be removed.",
|
||||
"components.UserList.deleteuser": "Delete User",
|
||||
"components.UserList.edit": "Edit",
|
||||
"components.UserList.lastupdated": "Last Updated",
|
||||
"components.UserList.plexuser": "Plex User",
|
||||
"components.UserList.role": "Role",
|
||||
"components.UserList.totalrequests": "Total Requests",
|
||||
"components.UserList.user": "User",
|
||||
"components.UserList.userdeleted": "User deleted",
|
||||
"components.UserList.userdeleteerror": "Something went wrong deleting the user",
|
||||
"components.UserList.userlist": "User List",
|
||||
"components.UserList.username": "Username",
|
||||
"components.UserList.usertype": "User Type",
|
||||
@@ -290,9 +345,11 @@
|
||||
"i18n.approved": "Approved",
|
||||
"i18n.available": "Available",
|
||||
"i18n.cancel": "Cancel",
|
||||
"i18n.close": "Close",
|
||||
"i18n.decline": "Decline",
|
||||
"i18n.declined": "Declined",
|
||||
"i18n.delete": "Delete",
|
||||
"i18n.deleting": "Deleting…",
|
||||
"i18n.movies": "Movies",
|
||||
"i18n.partiallyavailable": "Partially Available",
|
||||
"i18n.pending": "Pending",
|
||||
|
||||
342
src/i18n/locale/es.json
Normal file
342
src/i18n/locale/es.json
Normal file
@@ -0,0 +1,342 @@
|
||||
{
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Error al connectar al Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.testing": "Comprobando…",
|
||||
"components.Settings.SonarrModal.test": "Comprobar",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "Un servidor Sonarr",
|
||||
"components.Settings.SonarrModal.servername": "Nombre del Servidor",
|
||||
"components.Settings.SonarrModal.server4k": "Servidor 4K",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Selecciona la Carpeta Raíz",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Selecciona un Perfil de Calidad",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Carpetas por Temporada",
|
||||
"components.Settings.SonarrModal.saving": "Guardando…",
|
||||
"components.Settings.SonarrModal.save": "Guardar Cambios",
|
||||
"components.Settings.SonarrModal.rootfolder": "Carpeta Raíz",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Perfil de Calidad",
|
||||
"components.Settings.SonarrModal.port": "Puerto",
|
||||
"components.Settings.SonarrModal.hostname": "Nombre de Host",
|
||||
"components.Settings.SonarrModal.editsonarr": "Editar Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.defaultserver": "Servidor por Defecto",
|
||||
"components.Settings.SonarrModal.createsonarr": "Crear Nuevo Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "Ejemplo: /sonarr",
|
||||
"components.Settings.SonarrModal.baseUrl": "URL Base",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "Tu Clave API de Sonarr",
|
||||
"components.Settings.SonarrModal.apiKey": "Clave API",
|
||||
"components.Settings.SonarrModal.add": "Agregar Servidor",
|
||||
"components.Settings.SettingsAbout.version": "Versión",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Peticiones Totales",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Contenido Total",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Información de Overseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Discursiones en GitHub",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Soporte",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Click aquín para unirte a nuestro servidor de Discord.",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Debes seleccionar una carpeta raíz",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "Debes seleccionar un perfil",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "Debes proporcionar un puerto",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Debes proporcionar un nombre de servidor",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Debes proporcionar un hostname/IP",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "Debes proporcionar la clave API",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "¡Conexión con Radarr establecida!",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Error al connectar al Servidor Radarr",
|
||||
"components.Settings.RadarrModal.testing": "Comprobando…",
|
||||
"components.Settings.RadarrModal.test": "Comprobar",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "Un Servidor Radarr",
|
||||
"components.Settings.RadarrModal.servername": "Nombre del Servidor",
|
||||
"components.Settings.RadarrModal.server4k": "Servidor 4K",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Selecciona la Carpeta Raíz",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Selecciona un Perfil de Calidad",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Selecciona Disponibilidad Mínima",
|
||||
"components.Settings.RadarrModal.saving": "Guardando…",
|
||||
"components.Settings.RadarrModal.save": "Guardar Cambios",
|
||||
"components.Settings.RadarrModal.rootfolder": "Carpeta Raíz",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Perfil de Calidad",
|
||||
"components.Settings.RadarrModal.port": "Puerto",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Disponibilidad Mínima",
|
||||
"components.Settings.RadarrModal.hostname": "Nombre de Host",
|
||||
"components.Settings.RadarrModal.editradarr": "Editar Servidor Radarr",
|
||||
"components.Settings.RadarrModal.defaultserver": "Servidor por Defecto",
|
||||
"components.Settings.RadarrModal.createradarr": "Crear Nuevo Servidor Radarr",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "Ejemplo: /radarr",
|
||||
"components.Settings.RadarrModal.baseUrl": "URL Base",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "Tu clave API de Radarr",
|
||||
"components.Settings.RadarrModal.apiKey": "Clave API",
|
||||
"components.Settings.RadarrModal.add": "Agregar Servidor",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "Ajustes del servidor -> Integraciones -> Webhooks",
|
||||
"components.Settings.Notifications.webhookUrl": "URL de Webhook",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "Debes proporcionar una URL del webhook",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Debes proporcionar un puerto SMTP",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Debes proporcionar un host SMTP",
|
||||
"components.Settings.Notifications.validationFromRequired": "Debes proporcionar una dirección de envío de email",
|
||||
"components.Settings.Notifications.smtpPort": "Puerto SMTP",
|
||||
"components.Settings.Notifications.smtpHost": "Host SMTP",
|
||||
"components.Settings.Notifications.saving": "Guardando…",
|
||||
"components.Settings.Notifications.save": "Guardar Cambios",
|
||||
"components.Settings.Notifications.enableSsl": "Activar SSL",
|
||||
"components.Settings.Notifications.emailsettingssaved": "¡Ajustes de notificación de Email guardados!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Fallo al guardar ajustes de notificación de Email.",
|
||||
"components.Settings.Notifications.emailsender": "Dirección del Remitente de Email",
|
||||
"components.Settings.Notifications.discordsettingssaved": "¡Ajustes de notificación de Discord guardados!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Fallo al guardar ajustes de notificación de Discord.",
|
||||
"components.Settings.Notifications.authUser": "Usuario",
|
||||
"components.Settings.Notifications.authPass": "Contraseña",
|
||||
"components.Settings.Notifications.agentenabled": "Agente Activado",
|
||||
"components.Search.searchresults": "Resultado de la búsqueda",
|
||||
"components.RequestModal.status": "Estado",
|
||||
"components.RequestModal.selectseason": "Seleccionar temporada(s)",
|
||||
"components.RequestModal.seasonnumber": "Temporada {number}",
|
||||
"components.RequestModal.season": "Temporada",
|
||||
"components.RequestModal.requesttitle": "Solicitar {title}",
|
||||
"components.RequestModal.requestseasons": "Solicitar {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
|
||||
"components.RequestModal.requesting": "Solicitando…",
|
||||
"components.RequestModal.requestfrom": "Hay una petición pendiente de {username}",
|
||||
"components.RequestModal.requestadmin": "Tu petición será aprovada inmediatamente.",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> solicitado.",
|
||||
"components.RequestList.showingresults": "Mostrando <strong>{from}</strong> a <strong>{to}</strong> de <strong>{total}</strong> resultados",
|
||||
"components.RequestModal.requestCancel": "Solicitud para <strong>{title}</strong> cancelada",
|
||||
"components.RequestModal.request": "Solicitar",
|
||||
"components.RequestModal.pendingrequest": "Solicitud pendiente para {title}",
|
||||
"components.RequestModal.numberofepisodes": "# de Episodios",
|
||||
"components.RequestModal.notrequested": "No Solicitado",
|
||||
"components.RequestModal.extras": "Extras",
|
||||
"components.RequestModal.close": "Cerrar",
|
||||
"components.RequestModal.cancelrequest": "Esto borrará tu petición. ¿Quieres continuar?",
|
||||
"components.RequestModal.cancelling": "Cancelando…",
|
||||
"components.RequestModal.cancel": "Cancelar Petición",
|
||||
"components.RequestList.status": "Estado",
|
||||
"components.RequestList.requests": "Peticiones",
|
||||
"components.RequestList.requestedAt": "Solicitado",
|
||||
"components.RequestList.previous": "Anterior",
|
||||
"components.RequestList.next": "Siguiente",
|
||||
"components.RequestList.modifiedBy": "Última Edición Por",
|
||||
"components.RequestList.mediaInfo": "Información",
|
||||
"components.RequestList.RequestItem.seasons": "Temporadas",
|
||||
"components.RequestList.RequestItem.requestedby": "Solicitado por {username}",
|
||||
"components.RequestList.RequestItem.notavailable": "N/D",
|
||||
"components.RequestCard.seasons": "Temporadas",
|
||||
"components.RequestCard.requestedby": "Solicitado por {username}",
|
||||
"components.RequestCard.all": "Todas",
|
||||
"components.RequestBlock.seasons": "Temporadas",
|
||||
"components.PlexLoginButton.loginwithplex": "Iniciar Sesión con Plex",
|
||||
"components.PlexLoginButton.loggingin": "Iniciando Sesión…",
|
||||
"components.PlexLoginButton.loading": "Cargando…",
|
||||
"components.PersonDetails.nobiography": "No hay biografía disponible.",
|
||||
"components.PersonDetails.ascharacter": "como {character}",
|
||||
"components.PersonDetails.appearsin": "Aparece en",
|
||||
"components.MovieDetails.viewrequest": "Ver Petición",
|
||||
"components.MovieDetails.userrating": "Puntuación de los Usuarios",
|
||||
"components.MovieDetails.unavailable": "No Disponible",
|
||||
"components.MovieDetails.status": "Estado",
|
||||
"components.MovieDetails.similarsubtext": "Otras películas parecidas a {title}",
|
||||
"components.MovieDetails.similar": "Títulos Similares",
|
||||
"components.MovieDetails.runtime": "{minutes} minutos",
|
||||
"components.MovieDetails.revenue": "Recaudado",
|
||||
"components.MovieDetails.request": "Solicitar",
|
||||
"components.MovieDetails.releasedate": "Fecha de Lanzamiento",
|
||||
"components.MovieDetails.recommendationssubtext": "Si te gustó {title}, también te puede gustar…",
|
||||
"components.MovieDetails.cast": "Reparto",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Reparto Completo",
|
||||
"components.MovieDetails.recommendations": "Recomendaciones",
|
||||
"components.MovieDetails.pending": "Pendiente",
|
||||
"components.MovieDetails.overviewunavailable": "Resumen no disponible",
|
||||
"components.MovieDetails.overview": "Resumen",
|
||||
"components.MovieDetails.originallanguage": "Idioma Original",
|
||||
"components.MovieDetails.manageModalTitle": "Gestionar Película",
|
||||
"components.MovieDetails.manageModalRequests": "Peticiones",
|
||||
"components.MovieDetails.manageModalNoRequests": "Sin Peticiones",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Esto borrará todos los datos de los medios, incluyendo las peticiones. Si el elemento existe en tu librería de Plex, la información del elemento se recreará en la siguiente sincronización.",
|
||||
"components.MovieDetails.manageModalClearMedia": "Borrar todos los datos de medios",
|
||||
"components.MovieDetails.decline": "Rechazar",
|
||||
"components.MovieDetails.cancelrequest": "Cancelar Petición",
|
||||
"components.MovieDetails.budget": "Presupuesto",
|
||||
"components.MovieDetails.available": "Disponible",
|
||||
"components.MovieDetails.approve": "Aprobar",
|
||||
"components.Login.signinplex": "Inicia Sesión para continuar",
|
||||
"components.Layout.UserDropdown.signout": "Cerrar Sesión",
|
||||
"components.Layout.Sidebar.users": "Usuarios",
|
||||
"components.Layout.Sidebar.settings": "Ajustes",
|
||||
"components.Layout.Sidebar.requests": "Peticiones",
|
||||
"components.Layout.Sidebar.dashboard": "Descubrir",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Buscar Películas y Series",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Cambiar Idioma",
|
||||
"components.Discover.upcomingmovies": "Próximas Películas",
|
||||
"components.Discover.upcoming": "Próximas Películas",
|
||||
"components.Discover.trending": "Tendencias",
|
||||
"components.Discover.recentrequests": "Peticiones Recientes",
|
||||
"components.Discover.recentlyAdded": "Agregado Recientemente",
|
||||
"components.Discover.populartv": "Series Populares",
|
||||
"components.Discover.popularmovies": "Películas Populares",
|
||||
"components.Discover.nopending": "Sin Peticiones Pendientes",
|
||||
"components.Discover.discovertv": "Series Populares",
|
||||
"components.Discover.discovermovies": "Películas Populares",
|
||||
"components.Layout.alphawarning": "Este software está en fase ALFA. Muchas cosas pueden ser inestables o fallar. ¡Por favor reporta estos problemas en el GitHub de Overseerr!",
|
||||
"components.Settings.addsonarr": "Agregar servidor Sonarr",
|
||||
"components.Settings.address": "Dirección",
|
||||
"components.Settings.addradarr": "Agregar servidor Radarr",
|
||||
"components.Settings.activeProfile": "Perfil activo",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "Debes seleccionar una carpeta raíz",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "Debes seleccionar un perfil",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "Debes proporcionar un puerto",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Debes proporcionar un nombre de servidor",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "Debes proporcionar un hostname/IP",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "Debes proporcionar la clave API",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "¡Conexión con Sonarr establecida!",
|
||||
"components.Settings.menuLogs": "Registro",
|
||||
"pages.somethingWentWrong": "{statusCode} – Algo salió mal",
|
||||
"pages.serviceUnavailable": "{statusCode}: Servicio no Disponible",
|
||||
"pages.returnHome": "Volver al Inicio",
|
||||
"pages.pageNotFound": "404 – Página no encontrada",
|
||||
"pages.oops": "Ups",
|
||||
"pages.internalServerError": "{statusCode}: Error interno del servidor",
|
||||
"i18n.unavailable": "No Disponible",
|
||||
"i18n.tvshows": "Series",
|
||||
"i18n.processing": "Procesando…",
|
||||
"i18n.pending": "Pendiente",
|
||||
"i18n.partiallyavailable": "Parcialmente Disponible",
|
||||
"i18n.movies": "Películas",
|
||||
"i18n.delete": "Eliminar",
|
||||
"i18n.declined": "Rechazado",
|
||||
"i18n.decline": "Rechazar",
|
||||
"i18n.cancel": "Cancelar",
|
||||
"i18n.available": "Disponible",
|
||||
"i18n.approved": "Aprobado",
|
||||
"i18n.approve": "Aprobar",
|
||||
"components.UserList.usertype": "Tipo de usuario",
|
||||
"components.UserList.username": "Nombre de usuario",
|
||||
"components.UserList.userlist": "Lista de usuarios",
|
||||
"components.UserList.user": "Usuario",
|
||||
"components.UserList.totalrequests": "Solicitudes totales",
|
||||
"components.UserList.role": "Rol",
|
||||
"components.UserList.plexuser": "Usuario de Plex",
|
||||
"components.UserList.lastupdated": "Última actualización",
|
||||
"components.UserList.edit": "Editar",
|
||||
"components.UserList.delete": "Eliminar",
|
||||
"components.UserList.created": "Creado",
|
||||
"components.UserList.admin": "Administrador",
|
||||
"components.UserEdit.voteDescription": "Otorga permiso para votar en las solicitudes (votación aún no implementada)",
|
||||
"components.UserEdit.vote": "Votar",
|
||||
"components.UserEdit.usersaved": "Usuario guardado",
|
||||
"components.UserEdit.usersDescription": "Otorga permiso para administrar usuarios de Overseerr. Los usuarios con este permiso no pueden modificar a los usuarios con privilegios de administrador, o concederlos.",
|
||||
"components.UserEdit.users": "Administrar usuarios",
|
||||
"components.UserEdit.username": "Nombre de usuario",
|
||||
"components.UserEdit.userfail": "Algo salió mal al guardar al usuario.",
|
||||
"components.UserEdit.settingsDescription": "Otorga permiso para modificar todas las configuraciones de Overseerr. El usuario debe tener este permiso para otorgarlo a otros.",
|
||||
"components.UserEdit.settings": "Administrar configuración",
|
||||
"components.UserEdit.saving": "Guardando…",
|
||||
"components.UserEdit.save": "Guardar",
|
||||
"components.UserEdit.requestDescription": "Otorga permiso para solicitar películas y series.",
|
||||
"components.UserEdit.request": "Solicitar",
|
||||
"components.UserEdit.permissions": "Permisos",
|
||||
"components.UserEdit.managerequestsDescription": "Otorga permiso para administrar las solicitudes de Overseerr. Esto incluye aprobar y rechazar solicitudes.",
|
||||
"components.UserEdit.managerequests": "Gestionar solicitudes",
|
||||
"components.UserEdit.email": "Correo electrónico",
|
||||
"components.UserEdit.edituser": "Editar Usuario",
|
||||
"components.UserEdit.avatar": "Avatar",
|
||||
"components.UserEdit.autoapproveDescription": "Otorga aprobación automática para cualquier solicitud de este usuario.",
|
||||
"components.UserEdit.autoapprove": "Aprobación automática",
|
||||
"components.UserEdit.adminDescription": "Acceso total de administrador. Ignoran todas las comprobaciones de permisos.",
|
||||
"components.UserEdit.admin": "Administrador",
|
||||
"components.TvDetails.userrating": "Puntuación de los Usuarios",
|
||||
"components.TvDetails.unavailable": "No Disponible",
|
||||
"components.TvDetails.status": "Estado",
|
||||
"components.TvDetails.similarsubtext": "Otras series similares a {title}",
|
||||
"components.TvDetails.similar": "Series similares",
|
||||
"components.TvDetails.requestmore": "Solicitar más",
|
||||
"components.TvDetails.request": "Solicitar",
|
||||
"components.TvDetails.recommendationssubtext": "Si te gustó {title}, también te puede gustar…",
|
||||
"components.TvDetails.recommendations": "Recomendaciones",
|
||||
"components.TvDetails.pending": "Pendiente",
|
||||
"components.TvDetails.overviewunavailable": "Resumen no disponible",
|
||||
"components.TvDetails.overview": "Resumen",
|
||||
"components.TvDetails.originallanguage": "Idioma original",
|
||||
"components.TvDetails.manageModalTitle": "Gestionar Series",
|
||||
"components.TvDetails.manageModalRequests": "Peticiones",
|
||||
"components.TvDetails.manageModalNoRequests": "Sin Peticiones",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "Esto borrará todos los datos de los medios, incluyendo las peticiones. Si el elemento existe en tu librería de Plex, la información del elemento se recreará en la siguiente sincronización.",
|
||||
"components.TvDetails.manageModalClearMedia": "Borrar todos los datos de medios",
|
||||
"components.TvDetails.declinerequests": "Rechazar {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.decline": "Rechazar",
|
||||
"components.TvDetails.cast": "Reparto",
|
||||
"components.TvDetails.cancelrequest": "Cancelar Petición",
|
||||
"components.TvDetails.available": "Disponible",
|
||||
"components.TvDetails.approverequests": "Aprobar {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.approve": "Aprobar",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Reparto completo de la serie",
|
||||
"components.TitleCard.tvshow": "Series",
|
||||
"components.Settings.jobname": "Nombre de Tarea",
|
||||
"components.TitleCard.movie": "Película",
|
||||
"components.Slider.noresults": "Sin resultados",
|
||||
"components.Setup.welcome": "Bienvenido a Overseerr",
|
||||
"components.Setup.signinMessage": "Comience iniciando sesión con su cuenta de Plex",
|
||||
"components.Setup.loginwithplex": "Iniciar sesión con Plex",
|
||||
"components.Setup.finishing": "Finalizando…",
|
||||
"components.Setup.finish": "Finalizar configuración",
|
||||
"components.Setup.continue": "Continuar",
|
||||
"components.Setup.configureplex": "Configurar Plex",
|
||||
"components.Setup.configureservices": "Configurar servicios",
|
||||
"components.Settings.validationPortRequired": "Debes proporcionar un puerto",
|
||||
"components.Settings.validationHostnameRequired": "Debe proporcionar un nombre de host / IP",
|
||||
"components.Settings.syncing": "Sincronizando…",
|
||||
"components.Settings.sync": "Sincronizar bibliotecas de Plex",
|
||||
"components.Settings.startscan": "Iniciar escaneo",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.sonarrsettings": "Ajustes de Sonarr",
|
||||
"components.Settings.sonarrSettingsDescription": "Configure su conexión Sonarr a continuación. Puede tener varias instancias, pero sólo dos activas como predeterminadas en cualquier momento (una para HD estándar y otro para 4K). Los administradores pueden elegir qué servidor se utiliza para nuevas solicitudes.",
|
||||
"components.Settings.servernamePlaceholder": "Nombre del servidor Plex",
|
||||
"components.Settings.servername": "Nombre del servidor (Establecido automáticamente después de guardar)",
|
||||
"components.Settings.saving": "Guardando…",
|
||||
"components.Settings.save": "Guardar cambios",
|
||||
"components.Settings.runnow": "Ejecutar ahora",
|
||||
"components.Settings.radarrsettings": "Ajustes de Radarr",
|
||||
"components.Settings.radarrSettingsDescription": "Configure su conexión a Radarr a continuación. Puede tener varias instancias, pero sólo dos activas como predeterminadas en cualquier momento (una para HD estándar y otro para 4K). Los administradores pueden elegir qué servidor se utiliza para nuevas solicitudes.",
|
||||
"components.Settings.port": "Puerto",
|
||||
"components.Settings.plexsettingsDescription": "Configure los ajustes de su servidor Plex. Overseerr usa su servidor Plex para escanear su biblioteca en un intervalo y ver qué contenido está disponible.",
|
||||
"components.Settings.plexsettings": "Ajustes de Plex",
|
||||
"components.Settings.plexlibrariesDescription": "Las bibliotecas en las que Overseerr busca títulos. Configure y guarde la configuración de conexión Plex y haga clic en el botón de abajo si no aparece ninguna.",
|
||||
"components.Settings.plexlibraries": "Bibliotecas Plex",
|
||||
"components.Settings.notrunning": "Sin ejecutarse",
|
||||
"components.Settings.notificationsettingsDescription": "Aquí puedes elegir qué tipos de notificaciones enviar y a través de qué tipos de servicios.",
|
||||
"components.Settings.notificationsettings": "Configuración de notificaciones",
|
||||
"components.Settings.nextexecution": "Siguiente ejecución",
|
||||
"components.Settings.menuServices": "Servicios",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuNotifications": "Notificaciones",
|
||||
"components.Settings.menuJobs": "Tareas",
|
||||
"components.Settings.menuGeneralSettings": "Ajustes Generales",
|
||||
"components.Settings.menuAbout": "Acerca de",
|
||||
"components.Settings.manualscanDescription": "Normalmente, esto sólo se ejecutará una vez cada 6 horas. Overseerr comprobará de forma más agresiva los añadidos recientemente de su servidor Plex. ¡Si es la primera vez que configura Plex se recomienda un escaneo manual completo de la biblioteca!",
|
||||
"components.Settings.manualscan": "Escaneo manual de biblioteca",
|
||||
"components.Settings.librariesRemaining": "Bibliotecas restantes: {count}",
|
||||
"components.Settings.hostname": "Nombre de host / IP",
|
||||
"components.Settings.generalsettingsDescription": "Estos son ajustes relacionados con la configuración general de Overseerr.",
|
||||
"components.Settings.generalsettings": "Configuración general",
|
||||
"components.Settings.edit": "Editar",
|
||||
"components.Settings.deleteserverconfirm": "¿Está seguro de que desea eliminar este servidor?",
|
||||
"components.Settings.delete": "Eliminar",
|
||||
"components.Settings.default4k": "4K predeterminado",
|
||||
"components.Settings.default": "Predeterminado",
|
||||
"components.Settings.currentlibrary": "Biblioteca actual: {name}",
|
||||
"components.Settings.copied": "Clave API copiada en el portapapeles",
|
||||
"components.Settings.cancelscan": "Cancelar escaneo",
|
||||
"components.Settings.applicationurl": "URL de la aplicación",
|
||||
"components.Settings.apikey": "Clave API",
|
||||
"components.Setup.tip": "Consejo",
|
||||
"components.Setup.syncingbackground": "La sincronización se ejecutará en segundo plano. Mientras tanto, puede continuar con el proceso de configuración.",
|
||||
"i18n.deleting": "Eliminando…",
|
||||
"components.UserList.userdeleteerror": "Algo salió mal al eliminar al usuario",
|
||||
"components.UserList.userdeleted": "Usuario eliminado",
|
||||
"components.UserList.deleteuser": "Eliminar usuario",
|
||||
"components.UserList.deleteconfirm": "¿Está seguro de que desea eliminar este usuario? Se eliminarán todas las solicitudes existentes de este usuario.",
|
||||
"components.Settings.nodefaultdescription": "Al menos un servidor debe estar marcado como predeterminado antes de que cualquier solicitud llegue a sus servicios.",
|
||||
"components.Settings.nodefault": "¡Ningún servidor predeterminado seleccionado!",
|
||||
"components.Settings.no4kimplemented": "(Los servidores 4K predeterminados no están implementados actualmente)",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Prueba la conexión para cargar carpetas raíz",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Prueba la conexión para cargar perfiles de calidad",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Cargando carpetas raíz…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Cargando perfiles de calidad…",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Debes seleccionar disponibilidad mínima",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Prueba la conexión para cargar carpetas raíz",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Prueba la conexión para cargar perfiles de calidad",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Cargando carpetas raíz…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Cargando perfiles de calidad…"
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"components.MovieDetails.overviewunavailable": "Résumé indisponible",
|
||||
"components.MovieDetails.pending": "En attente",
|
||||
"components.MovieDetails.recommendations": "Recommendations",
|
||||
"components.MovieDetails.recommendationssubtext": "Si vous avez aimé {title}, vous aimerez peut-être...",
|
||||
"components.MovieDetails.recommendationssubtext": "Si vous avez aimé {title}, vous aimerez peut-être…",
|
||||
"components.MovieDetails.releasedate": "Date de sortie",
|
||||
"components.MovieDetails.request": "Demande d'ajout",
|
||||
"components.MovieDetails.revenue": "Revenus",
|
||||
@@ -90,13 +90,13 @@
|
||||
"components.Settings.Notifications.agentenabled": "Agent activé",
|
||||
"components.Settings.Notifications.authPass": "Auth Pass",
|
||||
"components.Settings.Notifications.authUser": "Auth User",
|
||||
"components.Settings.Notifications.emailsender": "Adresse courriel de l'expéditeur",
|
||||
"components.Settings.Notifications.emailsender": "e-mail de l'expéditeur",
|
||||
"components.Settings.Notifications.enableSsl": "Activer le SSL",
|
||||
"components.Settings.Notifications.save": "Sauvegarder les changements",
|
||||
"components.Settings.Notifications.saving": "Sauvegarde en cours…",
|
||||
"components.Settings.Notifications.smtpHost": "Hôte SMTP",
|
||||
"components.Settings.Notifications.smtpPort": "Port SMTP",
|
||||
"components.Settings.Notifications.validationFromRequired": "Vous devez fournir une adresse courriel d'expéditeur",
|
||||
"components.Settings.Notifications.validationFromRequired": "Vous devez fournir une adresse e-mail d'expéditeur",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Vous devez fournir un hôte SSL",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Vous devez fournir un port SMTP",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "Vous devez fournir une URL de webhook",
|
||||
@@ -205,7 +205,7 @@
|
||||
"components.Settings.runnow": "Lancer maintenant",
|
||||
"components.Settings.save": "Sauvegarder les changements",
|
||||
"components.Settings.saving": "Enregistrement en cours…",
|
||||
"components.Settings.servername": "Nom du serveur (défini automatiquement)",
|
||||
"components.Settings.servername": "Nom du serveur (automatiquement défini après enregistrement)",
|
||||
"components.Settings.servernamePlaceholder": "Nom de serveur Plex",
|
||||
"components.Settings.sonarrSettingsDescription": "Configurez votre connexion Sonarr ci-dessous. Vous pouvez avoir plusieurs, mais seulement deux actives par défaut à tout moment (une pour la HD standard et un pour la 4K). Les administrateurs peuvent remplacer le serveur utilisé pour les nouvelles demandes.",
|
||||
"components.Settings.sonarrsettings": "Paramètres Sonarr",
|
||||
@@ -255,7 +255,7 @@
|
||||
"components.UserEdit.autoapproveDescription": "Active la validation automatique de toutes les demandes d'ajouts de cet utilisateur.",
|
||||
"components.UserEdit.avatar": "Avatar",
|
||||
"components.UserEdit.edituser": "Modifier l'utilisateur",
|
||||
"components.UserEdit.email": "Courriel",
|
||||
"components.UserEdit.email": "e-mail",
|
||||
"components.UserEdit.managerequests": "Gérer les demandes d'ajouts",
|
||||
"components.UserEdit.managerequestsDescription": "Donne la permission de gérer les demandes d'ajouts sur Overseerr. Ceci inclut la possibilité de valider ou refuser une demande.",
|
||||
"components.UserEdit.permissions": "Autorisations",
|
||||
@@ -304,5 +304,49 @@
|
||||
"pages.serviceUnavailable": "{statusCode} – Service indisponible",
|
||||
"pages.somethingWentWrong": "{statusCode} – Une erreur est survenue",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Casting complet de la série",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Casting complet"
|
||||
"components.MovieDetails.MovieCast.fullcast": "Casting complet",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Paramètres de notification par courriel enregistrés !",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Les paramètres de notification par courriel n'ont pas pu être enregistrés.",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Paramètres de notification Discord enregistrés !",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Les paramètres de notification Discord n'ont pas pu être enregistrés.",
|
||||
"components.Settings.validationPortRequired": "Vous devez fournir un port",
|
||||
"components.Settings.validationHostnameRequired": "Vous devez fournir un nom d'hôte / une IP",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Vous devez fournir un nom de serveur",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total des demandes",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total des médias",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Informations sur Overseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Discussions GitHub",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Obtenir de l'aide",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Cliquez ici pour rejoindre notre serveur Discord.",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Vous devez fournir un nom de serveur",
|
||||
"components.Setup.tip": "Astuce",
|
||||
"components.Setup.syncingbackground": "La synchronisation s'exécutera en arrière-plan. Vous pouvez continuer le processus de configuration en attendant.",
|
||||
"i18n.deleting": "Suppression…",
|
||||
"components.UserList.userdeleteerror": "Une erreur s'est produite lors de la suppression de l'utilisateur",
|
||||
"components.UserList.userdeleted": "Utilisateur supprimé",
|
||||
"components.UserList.deleteuser": "Supprimer l'utilisateur",
|
||||
"components.UserList.deleteconfirm": "Voulez-vous vraiment supprimer cet utilisateur ? Toutes les données de demande existantes de cet utilisateur seront supprimées.",
|
||||
"components.Settings.nodefaultdescription": "Au moins un serveur doit être marqué par défaut avant que toute demande parvienne à vos services.",
|
||||
"components.Settings.nodefault": "Aucun serveur par défaut sélectionné !",
|
||||
"components.Settings.no4kimplemented": "(Les serveurs 4K par défaut ne sont actuellement pas implémentés)",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Testez votre connexion pour charger les dossiers racine",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Testez votre connexion pour charger des profils qualité",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Chargement des dossiers racine…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Chargement des profils qualité…",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Vous devez sélectionner une disponibilité minimale",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Testez votre connexion pour charger les dossiers racine",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Testez votre connexion pour charger des profils qualité",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Chargement des dossiers racine…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Chargement des profils qualité…",
|
||||
"components.TvDetails.showtype": "Type de série",
|
||||
"components.TvDetails.network": "Réseau",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.Settings.toastSettingsSuccess": "Paramètres enregistrés.",
|
||||
"components.Settings.toastSettingsFailure": "Une erreur s'est produite lors de l'enregistrement des paramètres.",
|
||||
"components.Settings.toastApiKeySuccess": "Nouvelle clé API générée !",
|
||||
"components.Settings.toastApiKeyFailure": "Une erreur s'est produite lors de la génération de la nouvelle clé API.",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Dossier racine pour anime",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Profil qualité pour anime",
|
||||
"components.MovieDetails.studio": "Studio"
|
||||
}
|
||||
|
||||
352
src/i18n/locale/it.json
Normal file
352
src/i18n/locale/it.json
Normal file
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"components.Discover.recentrequests": "Richieste recenti",
|
||||
"components.Discover.recentlyAdded": "Aggiunti di recente",
|
||||
"components.Discover.populartv": "Serie popolari",
|
||||
"components.Discover.popularmovies": "Film popolari",
|
||||
"components.Discover.nopending": "Nessuna richiesta in sospeso",
|
||||
"components.Discover.discovertv": "Serie popolari",
|
||||
"components.Discover.discovermovies": "Film popolari",
|
||||
"components.UserEdit.email": "E-mail",
|
||||
"components.Settings.Notifications.validationFromRequired": "È necessario fornire un indirizzo mittente di posta elettronica",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Impostazioni di notifica tramite posta elettronica salvate!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Impossibile salvare le impostazioni di notifica tramite posta elettronica.",
|
||||
"components.Settings.Notifications.emailsender": "Indirizzo mittente di posta elettronica",
|
||||
"components.Settings.SonarrModal.testing": "Test in corso…",
|
||||
"components.Settings.RadarrModal.testing": "Test in corso…",
|
||||
"components.Settings.address": "Indirizzo",
|
||||
"components.Settings.SonarrModal.test": "Test",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.saving": "Salvataggio…",
|
||||
"components.Settings.SonarrModal.port": "Porta",
|
||||
"components.Settings.SonarrModal.hostname": "Hostname",
|
||||
"components.Settings.SettingsAbout.version": "Versione",
|
||||
"components.Settings.RadarrModal.test": "Test",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.saving": "Salvataggio…",
|
||||
"components.Settings.RadarrModal.port": "Porta",
|
||||
"components.Settings.RadarrModal.hostname": "Hostname",
|
||||
"components.Settings.Notifications.saving": "Salvataggio…",
|
||||
"components.RequestModal.requesting": "Richiesta in corso…",
|
||||
"components.MovieDetails.pending": "In sospeso",
|
||||
"components.Settings.Notifications.agentenabled": "Agente abilitato",
|
||||
"components.Search.searchresults": "Risultati di ricerca",
|
||||
"components.RequestModal.status": "Stato",
|
||||
"components.RequestModal.selectseason": "Seleziona stagioni",
|
||||
"components.RequestModal.seasonnumber": "Stagione {number}",
|
||||
"components.RequestModal.season": "Stagione",
|
||||
"components.RequestModal.requestadmin": "La tua richiesta verrà approvata immediatamente.",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> richiesto.",
|
||||
"components.RequestModal.requestCancel": "Richiesta per <strong>{title}</strong> cancellata",
|
||||
"components.RequestModal.request": "Richiedi",
|
||||
"components.RequestModal.numberofepisodes": "Nº di episodi",
|
||||
"components.RequestModal.notrequested": "Non richiesto",
|
||||
"components.RequestModal.extras": "Extra",
|
||||
"components.RequestModal.close": "Chiudi",
|
||||
"components.RequestModal.cancelling": "Cancellazione…",
|
||||
"components.RequestModal.cancel": "Cancella la richiesta",
|
||||
"components.RequestList.status": "Stato",
|
||||
"components.RequestList.requests": "Richieste",
|
||||
"components.RequestList.requestedAt": "Richiesto",
|
||||
"components.RequestList.previous": "Precedente",
|
||||
"components.RequestList.next": "Successivo",
|
||||
"components.RequestList.modifiedBy": "Ultima modifica da",
|
||||
"components.RequestList.mediaInfo": "Media",
|
||||
"components.RequestList.RequestItem.seasons": "Stagioni",
|
||||
"components.RequestList.RequestItem.requestedby": "Richiesto da {username}",
|
||||
"components.RequestList.RequestItem.notavailable": "n.a.",
|
||||
"components.RequestCard.seasons": "Stagioni",
|
||||
"components.RequestCard.requestedby": "Richiesto da {username}",
|
||||
"components.RequestCard.all": "Tutte",
|
||||
"components.RequestBlock.seasons": "Stagioni",
|
||||
"components.PlexLoginButton.loginwithplex": "Accedi con Plex",
|
||||
"components.PlexLoginButton.loggingin": "Accesso in corso…",
|
||||
"components.PlexLoginButton.loading": "Caricamento…",
|
||||
"components.PersonDetails.nobiography": "Biografia non disponibile.",
|
||||
"components.PersonDetails.ascharacter": "come {character}",
|
||||
"components.PersonDetails.appearsin": "Compare in",
|
||||
"components.MovieDetails.userrating": "Voto pubblico",
|
||||
"components.MovieDetails.unavailable": "Non disponibile",
|
||||
"components.MovieDetails.studio": "Studio",
|
||||
"components.MovieDetails.status": "Stato",
|
||||
"components.MovieDetails.similarsubtext": "Altri film simili a {title}",
|
||||
"components.MovieDetails.similar": "Titoli simili",
|
||||
"components.MovieDetails.runtime": "{minutes} minuti",
|
||||
"components.MovieDetails.revenue": "Incassi",
|
||||
"components.MovieDetails.request": "Richiedi",
|
||||
"components.MovieDetails.releasedate": "Data di uscita",
|
||||
"components.MovieDetails.recommendationssubtext": "Se ti è piaciuto {title}, potrebbe interessarti…",
|
||||
"components.MovieDetails.recommendations": "Consigliati",
|
||||
"components.MovieDetails.overviewunavailable": "Trama non disponibile",
|
||||
"components.MovieDetails.overview": "Trama",
|
||||
"components.MovieDetails.originallanguage": "Lingua originale",
|
||||
"components.MovieDetails.manageModalTitle": "Gestisci film",
|
||||
"components.MovieDetails.manageModalRequests": "Richieste",
|
||||
"components.MovieDetails.manageModalNoRequests": "Nessuna richiesta",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Questo rimuoverà tutti i dati, incluse le richieste per questo elemento in modo irreversibile. Se questo elemento esiste nella tua libreria di Plex, i dati verranno ricreati alla prossima sincronizzazione.",
|
||||
"components.MovieDetails.manageModalClearMedia": "Cancella tutti i dati",
|
||||
"components.MovieDetails.cancelrequest": "Annulla la richiesta",
|
||||
"components.MovieDetails.decline": "Rifiuta",
|
||||
"components.MovieDetails.cast": "Cast",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.available": "Disponibile",
|
||||
"components.MovieDetails.approve": "Approva",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Cast completo",
|
||||
"components.Login.signinplex": "Accedi per continuare",
|
||||
"components.Layout.alphawarning": "Questo software è in ALPHA. Tutto può smettere di funzionare o diventare instabile. Aiutaci riportando i problemi su Overseerr GitHub!",
|
||||
"components.Layout.UserDropdown.signout": "Esci",
|
||||
"components.Layout.Sidebar.users": "Utenti",
|
||||
"components.Layout.Sidebar.settings": "Impostazioni",
|
||||
"components.Layout.Sidebar.requests": "Richieste",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Cerca film e serie",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Cambia la lingua",
|
||||
"components.Discover.upcomingmovies": "Film in uscita",
|
||||
"components.Discover.upcoming": "Film in uscita",
|
||||
"components.Discover.trending": "Di tendenza",
|
||||
"components.UserEdit.avatar": "Avatar",
|
||||
"components.UserEdit.edituser": "Modifica l'utente",
|
||||
"components.UserEdit.settings": "Gestisci le impostazioni",
|
||||
"components.UserEdit.saving": "Salvataggio…",
|
||||
"components.UserEdit.save": "Salva",
|
||||
"components.UserEdit.usersaved": "Utente salvato",
|
||||
"components.UserEdit.usersDescription": "Concede l'autorizzazione per gestire gli utenti di Overseerr. Gli utenti con questa autorizzazione non possono modificare gli utenti con privilegi di amministratore o concederla.",
|
||||
"components.UserEdit.users": "Gestisci gli utenti",
|
||||
"components.UserEdit.username": "Nome utente",
|
||||
"components.UserList.deleteconfirm": "Eliminare l'utente? Tutti i dati di richiesta esistenti da questo utente verranno rimossi.",
|
||||
"components.UserList.delete": "Elimina",
|
||||
"components.UserList.created": "Creato",
|
||||
"components.UserList.admin": "Amministratore",
|
||||
"components.UserList.role": "Ruolo",
|
||||
"components.UserList.plexuser": "Utente Plex",
|
||||
"components.UserList.lastupdated": "Ultimo aggiornamento",
|
||||
"components.UserList.edit": "Modifica",
|
||||
"components.UserList.deleteuser": "Elimina l'utente",
|
||||
"components.UserList.userlist": "Elenco utenti",
|
||||
"components.UserList.userdeleteerror": "Qualcosa è andato storto eliminando l'utente",
|
||||
"components.UserList.userdeleted": "Utente eliminato",
|
||||
"components.UserList.user": "Utente",
|
||||
"components.UserList.totalrequests": "Totale richieste",
|
||||
"i18n.declined": "Rifiutato",
|
||||
"i18n.decline": "Rifiuta",
|
||||
"i18n.cancel": "Annulla",
|
||||
"i18n.available": "Disponibile",
|
||||
"i18n.approved": "Approvato",
|
||||
"i18n.approve": "Approva",
|
||||
"components.UserList.usertype": "Tipo di utente",
|
||||
"components.UserList.username": "Nome utente",
|
||||
"i18n.unavailable": "Non disponibile",
|
||||
"i18n.tvshows": "Serie",
|
||||
"i18n.processing": "Elaborazione…",
|
||||
"i18n.pending": "In sospeso",
|
||||
"i18n.partiallyavailable": "Parzialmente disponibile",
|
||||
"i18n.movies": "Film",
|
||||
"i18n.deleting": "Eliminazione…",
|
||||
"i18n.delete": "Elimina",
|
||||
"pages.oops": "Ops",
|
||||
"components.Settings.RadarrModal.rootfolder": "Cartella radice",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Seleziona una cartella radice",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Seleziona un profilo qualità",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Seleziona la disponibilità minima",
|
||||
"components.Settings.RadarrModal.save": "Salva i cambiamenti",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Profilo qualità",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Disponibilità minima",
|
||||
"components.Settings.RadarrModal.editradarr": "Modifica server Radarr",
|
||||
"components.Settings.RadarrModal.defaultserver": "Server predefinito",
|
||||
"components.Settings.RadarrModal.apiKey": "Chiave API",
|
||||
"components.Settings.RadarrModal.add": "Aggiungi un server",
|
||||
"components.Settings.Notifications.save": "Salva i cambiamenti",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Impossibile salvare le impostazioni di notifica Discord.",
|
||||
"components.RequestModal.requesttitle": "Richiedi {title}",
|
||||
"components.RequestModal.requestfrom": "Al momento è in sospeso una richiesta da {username}",
|
||||
"components.RequestModal.cancelrequest": "Questo rimuoverà la richiesta. Continuare?",
|
||||
"components.RequestModal.pendingrequest": "Richiesta in sospeso per {title}",
|
||||
"components.MovieDetails.viewrequest": "Visualizza la richiesta",
|
||||
"components.Layout.Sidebar.dashboard": "Esplora",
|
||||
"components.TvDetails.manageModalTitle": "Gestisci serie",
|
||||
"components.TvDetails.manageModalRequests": "Richieste",
|
||||
"components.TvDetails.manageModalNoRequests": "Nessuna richiesta",
|
||||
"components.TvDetails.manageModalClearMedia": "Cancella tutti i dati multimediali",
|
||||
"components.TvDetails.declinerequests": "Rifiuta {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.decline": "Rifiuta",
|
||||
"components.TvDetails.cast": "Cast",
|
||||
"components.TvDetails.cancelrequest": "Annulla la richiesta",
|
||||
"components.TvDetails.available": "Disponibile",
|
||||
"components.TvDetails.approverequests": "Approva {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.approve": "Approva",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Cast completo della serie",
|
||||
"components.TitleCard.tvshow": "Serie",
|
||||
"components.Slider.noresults": "Nessun risultato",
|
||||
"components.Setup.welcome": "Benvenuti in Overseerr",
|
||||
"components.TitleCard.movie": "Film",
|
||||
"components.Setup.loginwithplex": "Accedi con Plex",
|
||||
"components.Setup.configureplex": "Configurare Plex",
|
||||
"components.Settings.validationPortRequired": "È necessario fornire una porta",
|
||||
"components.Settings.validationHostnameRequired": "È necessario fornire un nome host/IP",
|
||||
"components.Settings.toastSettingsSuccess": "Impostazioni salvate.",
|
||||
"components.Settings.syncing": "Sincronizzazione…",
|
||||
"components.Settings.startscan": "Avvia la scansione",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.saving": "Salvataggio…",
|
||||
"components.Settings.save": "Salva i cambiamenti",
|
||||
"components.Settings.runnow": "Esegui ora",
|
||||
"components.Settings.radarrsettings": "Impostazioni Radarr",
|
||||
"components.Settings.plexsettings": "Impostazioni Plex",
|
||||
"components.Settings.notrunning": "Non in esecuzione",
|
||||
"components.Settings.notificationsettingsDescription": "Qui puoi scegliere quali tipi di notifiche inviare e attraverso quali tipi di servizi.",
|
||||
"components.Settings.notificationsettings": "Impostazioni di notifica",
|
||||
"components.Settings.nodefaultdescription": "Almeno un server deve essere contrassegnato come predefinito prima che qualsiasi richiesta lo apportare ai servizi.",
|
||||
"components.Settings.nodefault": "Nessun server predefinito selezionato!",
|
||||
"components.Settings.no4kimplemented": "(I server 4K predefiniti non sono attualmente implementati)",
|
||||
"components.Settings.nextexecution": "Esecuzione successiva",
|
||||
"components.Settings.menuServices": "Servizi",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuNotifications": "Notifiche",
|
||||
"components.Settings.menuLogs": "Registri",
|
||||
"components.Settings.menuGeneralSettings": "Impostazioni generali",
|
||||
"components.Settings.menuAbout": "Informazioni",
|
||||
"components.Settings.manualscan": "Scansione manuale della biblioteca",
|
||||
"components.Settings.librariesRemaining": "Biblioteche rimanenti: {count}",
|
||||
"components.Settings.hostname": "Nome host/IP",
|
||||
"components.Settings.generalsettingsDescription": "Queste sono impostazioni relative alla configurazione generale di Overseerr.",
|
||||
"components.Settings.generalsettings": "Impostazioni generali",
|
||||
"components.Settings.edit": "Modifica",
|
||||
"components.Settings.deleteserverconfirm": "Sei sicuro/a di voler eliminare questo server?",
|
||||
"components.Settings.delete": "Elimina",
|
||||
"components.Settings.default4k": "4K predefinito",
|
||||
"components.Settings.default": "Predefinito",
|
||||
"components.Settings.currentlibrary": "Biblioteca corrente: {name}",
|
||||
"components.Settings.copied": "Chiave API copiata negli appunti",
|
||||
"components.Settings.cancelscan": "Annulla l'analisi",
|
||||
"components.Settings.applicationurl": "URL applicazione",
|
||||
"components.Settings.apikey": "Chiave API",
|
||||
"components.Settings.addsonarr": "Aggiungi un server Sonarr",
|
||||
"components.Settings.addradarr": "Aggiungi un server radarr",
|
||||
"components.Settings.activeProfile": "Profilo attivo",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "È necessario selezionare una cartella radice",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "È necessario selezionare un profilo",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "È necessario fornire una porta",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "È necessario fornire un nome di server",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "È necessario fornire un nome host/IP",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "È necessario fornire una chiave API",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Stabilita la connessione Sonarr!",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Impossibile connettersi a Sonarr Server",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Testa la connessione per caricare le cartelle radice",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Testa la connessione per caricare profili di qualità",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "Un server Sonarr",
|
||||
"components.Settings.SonarrModal.servername": "Nome server",
|
||||
"components.Settings.SonarrModal.server4k": "Server 4K",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Seleziona una cartella radice",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Seleziona un profilo qualità",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Cartelle stagione",
|
||||
"components.Settings.SonarrModal.save": "Salva i cambiamenti",
|
||||
"components.Settings.SonarrModal.rootfolder": "Cartella radice",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Profilo qualità",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Caricamento delle cartelle radice in…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Caricamento profili qualità…",
|
||||
"components.Settings.SonarrModal.editsonarr": "Modifica server Sonarr",
|
||||
"components.Settings.SonarrModal.defaultserver": "Server predefinito",
|
||||
"components.Settings.SonarrModal.createsonarr": "Crea nuovo server Sonarr",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "Esempio: /sonarr",
|
||||
"components.Settings.SonarrModal.baseUrl": "URL di base",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "La tua chiave API Sonarr",
|
||||
"components.Settings.SonarrModal.apiKey": "Chiave API",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Cartella radice anime",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Profilo qualità anime",
|
||||
"components.Settings.SonarrModal.add": "Aggiungi un server",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Totale richieste",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Media totali",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Informazioni su Overseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Discussioni su GitHub",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Ottieni aiuto",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Clicca qui per unirti al nostro server Discord.",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "È necessario selezionare una cartella radice",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "È necessario selezionare un profilo",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "È necessario fornire una porta",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "È necessario fornire un nome di server",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "È necessario selezionare la disponibilità minima",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "È necessario fornire un nome host/IP",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "È necessario fornire una chiave API",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Stabilita la connessione Radarr!",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Impossibile connettersi al server Radarr",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Testa la connessione per caricare le cartelle radice",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Testa la connessione per caricare profili qualità",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "Un server Radarr",
|
||||
"components.Settings.RadarrModal.servername": "Nome server",
|
||||
"components.Settings.RadarrModal.server4k": "Server 4K",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Caricamento delle cartelle radice in…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Caricamento profili di qualità…",
|
||||
"components.Settings.RadarrModal.createradarr": "Crea un nuovo server Radarr",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "Esempio: /radarr",
|
||||
"components.Settings.RadarrModal.baseUrl": "URL di base",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "La tua chiave API Radarr",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "Impostazioni server -> integrazioni -> Webhook",
|
||||
"components.Settings.Notifications.webhookUrl": "URL webhook",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "È necessario fornire un URL webhook",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "È necessario fornire una porta SMTP",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "È necessario fornire un host SMTP",
|
||||
"components.Settings.Notifications.smtpPort": "Porta SMTP",
|
||||
"components.Settings.Notifications.smtpHost": "Host SMTP",
|
||||
"components.Settings.Notifications.enableSsl": "Abilita SSL",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Impostazioni di notifica Discord salvate!",
|
||||
"components.Settings.Notifications.authUser": "Nome utente",
|
||||
"components.Settings.Notifications.authPass": "Password",
|
||||
"components.RequestModal.requestseasons": "Richiedi {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
|
||||
"components.RequestList.showingresults": "Visualizzazione dei risultati da <strong>{from}</strong> a <strong>{to}</strong> di <strong>{total}</strong>",
|
||||
"components.UserEdit.userfail": "Qualcosa è andato storto salvando l'utente.",
|
||||
"components.UserEdit.requestDescription": "Concede l'autorizzazione per richiedere film e serie.",
|
||||
"components.UserEdit.request": "Richiesta",
|
||||
"components.UserEdit.permissions": "Autorizzazioni",
|
||||
"components.UserEdit.managerequestsDescription": "Concede l'autorizzazione per gestire le richieste Overseerr. Ciò include l'approvazione e la negazione delle richieste.",
|
||||
"components.UserEdit.managerequests": "Gestisci le richieste",
|
||||
"components.UserEdit.autoapproveDescription": "Concede l'approvazione automatica per tutte le richieste effettuate da questo utente.",
|
||||
"components.UserEdit.autoapprove": "Approvazione automatica",
|
||||
"components.UserEdit.adminDescription": "Accesso completo all'amministratore. Ignora tutti i controlli delle autorizzazioni.",
|
||||
"components.UserEdit.admin": "Amministratore",
|
||||
"components.TvDetails.userrating": "Voto pubblico",
|
||||
"components.TvDetails.unavailable": "Non disponibile",
|
||||
"components.TvDetails.status": "Stato",
|
||||
"components.TvDetails.similarsubtext": "Altre serie simili a {title}",
|
||||
"components.TvDetails.similar": "Serie simile",
|
||||
"components.TvDetails.showtype": "Tipo di serie",
|
||||
"components.TvDetails.requestmore": "Richiedi di più",
|
||||
"components.TvDetails.request": "Richiesta",
|
||||
"components.TvDetails.recommendationssubtext": "Se ti è piaciuto {title}, potrebbe anche piacerti…",
|
||||
"components.TvDetails.recommendations": "Consigli",
|
||||
"components.TvDetails.pending": "In sospeso",
|
||||
"components.TvDetails.overviewunavailable": "Trama non disponibile",
|
||||
"components.TvDetails.overview": "Trama",
|
||||
"components.TvDetails.originallanguage": "Lingua originale",
|
||||
"components.TvDetails.network": "Rete",
|
||||
"components.Setup.finishing": "Finalizzazione…",
|
||||
"components.Settings.menuJobs": "Task",
|
||||
"components.Settings.jobname": "Nome Task",
|
||||
"components.UserEdit.voteDescription": "Concede il permesso di votare sulle richieste (sistema di voto non acora implementato)",
|
||||
"components.UserEdit.vote": "Vota",
|
||||
"components.UserEdit.settingsDescription": "Concede l'autorizzazione a modificare tutte le impostazioni di Overseerr. Un utente deve disporre di questa autorizzazione per concederla ad altri.",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "Questo rimuoverà tutti i dati, incluse le richieste per questo elemento in modo irreversibile. Se questo elemento esiste nella tua libreria di Plex, i dati verranno ricreati alla prossima sincronizzazione.",
|
||||
"components.Setup.syncingbackground": "La sincronizzazione verrà eseguita in background. Nel frattempo puoi continuare il processo di configurazione.",
|
||||
"components.Setup.signinMessage": "Comincia accedendo con il tuo account Plex",
|
||||
"components.Settings.sync": "Sincronizza le librerie di Plex",
|
||||
"components.Settings.sonarrsettings": "Impostazioni Sonarr",
|
||||
"components.Settings.sonarrSettingsDescription": "Imposta la connessione al tuo Sonarr qui sotto. È possibile avere più instanze, ma solo due attive come predefinite nello stesso momento (uno per l'HD standard e uno per 4K). Gli amministratori possono ignorare quale server è utilizzato per le nuove richieste.",
|
||||
"components.Settings.radarrSettingsDescription": "Imposta la connessione al tuo Radarr qui sotto. È possibile avere più instanze, ma solo due attive come predefinite nello stesso momento (uno per l'HD standard e uno per 4K). Gli amministratori possono ignorare quale server è utilizzato per le nuove richieste.",
|
||||
"components.Settings.plexsettingsDescription": "Configura le impostazioni per il tuo server Plex. Overseerr utilizza il tuo server Plex per scansionare la tua libreria a intervealli per vedere quali elementi sono disponibili.",
|
||||
"components.Settings.plexlibrariesDescription": "Le librerie che verranno scansionate da Overseerr alla ricerca di titoli. Se non ci sono librerie, configura e salva le impostazioni di connessione a Plex e fai click sul pulsante qui sotto.",
|
||||
"components.Settings.plexlibraries": "Librerie Plex",
|
||||
"components.Settings.manualscanDescription": "Normalmente, questo verrà eseguito ogni 6 ore. Overseerr controllerà in modo più aggressivo i server Plex aggiunti di recente. Se è la prima volta che configuri Plex, si consiglia una scansione manuale completa della libreria!",
|
||||
"components.Settings.port": "Porta",
|
||||
"components.Settings.servername": "Nome server (impostato automaticamente dopo il salvataggio)",
|
||||
"components.Settings.servernamePlaceholder": "Nome server Plex",
|
||||
"components.Settings.toastSettingsFailure": "Qualcosa è andato storto durante il salvataggio delle impostazioni.",
|
||||
"components.Settings.toastApiKeySuccess": "Nuova chiave API generata!",
|
||||
"components.Settings.toastApiKeyFailure": "Qualcosa è andato storto generando una nuova chiave API.",
|
||||
"components.Setup.configureservices": "Configura i servizi",
|
||||
"components.Setup.finish": "Termina la configurazione",
|
||||
"components.Setup.continue": "Continua",
|
||||
"components.Setup.tip": "Consiglio",
|
||||
"pages.internalServerError": "{statusCode} — Errore interno del server",
|
||||
"pages.somethingWentWrong": "{statusCode} — Qualcosa è andato storto",
|
||||
"pages.serviceUnavailable": "{statusCode} — Servizio non disponibile",
|
||||
"pages.returnHome": "Ritorna alla pagina iniziale",
|
||||
"pages.pageNotFound": "404 — Pagina non trovata"
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"components.MovieDetails.overviewunavailable": "ストーリー情報がありません",
|
||||
"components.MovieDetails.pending": "リクエスト中",
|
||||
"components.MovieDetails.recommendations": "オススメの作品",
|
||||
"components.MovieDetails.recommendationssubtext": "{title}が好きだった人は、こんなのも好きかもしれません...",
|
||||
"components.MovieDetails.recommendationssubtext": "{title}が好きだった人は、こんなのも好きかもしれません…",
|
||||
"components.MovieDetails.releasedate": "公開日",
|
||||
"components.MovieDetails.request": "リクエストする",
|
||||
"components.MovieDetails.revenue": "収益",
|
||||
@@ -67,7 +67,7 @@
|
||||
"components.RequestList.showingresults": "<strong>{total}</strong>中<strong>{from}</strong>~<strong>{to}</strong>件",
|
||||
"components.RequestList.status": "状態",
|
||||
"components.RequestModal.cancel": "キャンセルリクエスト",
|
||||
"components.RequestModal.cancelling": "取り消し中...",
|
||||
"components.RequestModal.cancelling": "取り消し中…",
|
||||
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
|
||||
"components.RequestModal.close": "閉じる",
|
||||
"components.RequestModal.extras": "おまけ",
|
||||
@@ -93,7 +93,7 @@
|
||||
"components.Settings.Notifications.emailsender": "配信元メールアドレス",
|
||||
"components.Settings.Notifications.enableSsl": "SSLを有効にする",
|
||||
"components.Settings.Notifications.save": "変更を保存",
|
||||
"components.Settings.Notifications.saving": "保存中...",
|
||||
"components.Settings.Notifications.saving": "保存中…",
|
||||
"components.Settings.Notifications.smtpHost": "SMTPホスト",
|
||||
"components.Settings.Notifications.smtpPort": "SMTPポート",
|
||||
"components.Settings.Notifications.validationFromRequired": "配信元メールアドレスの入力は必要です",
|
||||
@@ -116,7 +116,7 @@
|
||||
"components.Settings.RadarrModal.qualityprofile": "画質プロファイル",
|
||||
"components.Settings.RadarrModal.rootfolder": "ルートフォルダー",
|
||||
"components.Settings.RadarrModal.save": "変更を保存",
|
||||
"components.Settings.RadarrModal.saving": "保存中...",
|
||||
"components.Settings.RadarrModal.saving": "保存中…",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "最低リリース状況を選ぶ",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "画質プロファイルを選ぶ",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "ルートフォルダーを選ぶ",
|
||||
@@ -125,7 +125,7 @@
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "Radarrサーバー",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.test": "テストする",
|
||||
"components.Settings.RadarrModal.testing": "テスト中...",
|
||||
"components.Settings.RadarrModal.testing": "テスト中…",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Radarrサーバーの接続は失敗しました",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarrサーバーの接続は成功しました!",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "APIキーの入力が必要です",
|
||||
@@ -146,7 +146,7 @@
|
||||
"components.Settings.SonarrModal.qualityprofile": "画質プロファイル",
|
||||
"components.Settings.SonarrModal.rootfolder": "ルートフォルダー",
|
||||
"components.Settings.SonarrModal.save": "変更を保存",
|
||||
"components.Settings.SonarrModal.saving": "保存中...",
|
||||
"components.Settings.SonarrModal.saving": "保存中…",
|
||||
"components.Settings.SonarrModal.seasonfolders": "シーズンフォルダー",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "画質プロファイルを選ぶ",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "ルートフォルダーを選ぶ",
|
||||
@@ -155,7 +155,7 @@
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "Sonarrサーバー",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.test": "テストする",
|
||||
"components.Settings.SonarrModal.testing": "テスト中...",
|
||||
"components.Settings.SonarrModal.testing": "テスト中…",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Sonarrサーバーに接続できませんでした",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarrサーバーの接続が成功しました!",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "APIキーの入力が必要です",
|
||||
@@ -204,8 +204,8 @@
|
||||
"components.Settings.radarrsettings": "Radarr設定",
|
||||
"components.Settings.runnow": "今すぐ実行",
|
||||
"components.Settings.save": "変更を保存",
|
||||
"components.Settings.saving": "保存中...",
|
||||
"components.Settings.servername": "サーバー名 (自動設定)",
|
||||
"components.Settings.saving": "保存中…",
|
||||
"components.Settings.servername": "サーバー名 (保存時に自動で設定されます)",
|
||||
"components.Settings.servernamePlaceholder": "Plexサーバー名",
|
||||
"components.Settings.sonarrSettingsDescription": "Sonarr接続設定。複数のサーバーを繋ぐことができますが、デフォルトで常にアクティブなのは2つのみ(1つは標準HD用、もう1つは4K用)。管理者は、新しいリクエストに使用するサーバーを選択することができます。",
|
||||
"components.Settings.sonarrsettings": "Sonarr設定",
|
||||
@@ -217,7 +217,7 @@
|
||||
"components.Setup.configureservices": "連携サービスの設定",
|
||||
"components.Setup.continue": "続行",
|
||||
"components.Setup.finish": "セットアップを完了",
|
||||
"components.Setup.finishing": "保存中...",
|
||||
"components.Setup.finishing": "保存中…",
|
||||
"components.Setup.loginwithplex": "Plexでログイン",
|
||||
"components.Setup.signinMessage": "Plexアカウントでログインして始める",
|
||||
"components.Setup.welcome": "Overseerrへようこそ",
|
||||
@@ -241,7 +241,7 @@
|
||||
"components.TvDetails.overviewunavailable": "ストーリー情報がありません",
|
||||
"components.TvDetails.pending": "リクエスト中",
|
||||
"components.TvDetails.recommendations": "オススメの作品",
|
||||
"components.TvDetails.recommendationssubtext": "{title}が好きだった人は、こんなのも好きかもしれません...",
|
||||
"components.TvDetails.recommendationssubtext": "{title}が好きだった人は、こんなのも好きかもしれません…",
|
||||
"components.TvDetails.request": "リクエストする",
|
||||
"components.TvDetails.requestmore": "もっとリクエストする",
|
||||
"components.TvDetails.similar": "類似シリーズ",
|
||||
@@ -262,7 +262,7 @@
|
||||
"components.UserEdit.request": "リクエストする",
|
||||
"components.UserEdit.requestDescription": "映画やテレビ番組のリクエスト権限を付与します。",
|
||||
"components.UserEdit.save": "保存",
|
||||
"components.UserEdit.saving": "保存中...",
|
||||
"components.UserEdit.saving": "保存中…",
|
||||
"components.UserEdit.settings": "設定の管理",
|
||||
"components.UserEdit.settingsDescription": "すべてのOverseerrの設定を変更する権限を付与します。他の人にこの権限を付与するには、ユーザーがこの権限を持っていなければなりません。",
|
||||
"components.UserEdit.userfail": "ユーザーの保存する時に問題が発生しました。",
|
||||
@@ -302,5 +302,51 @@
|
||||
"pages.pageNotFound": "404 - ページが見つかりません",
|
||||
"pages.returnHome": "ホームへ戻る",
|
||||
"pages.serviceUnavailable": "{statusCode} - サービスが利用できません",
|
||||
"pages.somethingWentWrong": "{statusCode} - 問題が発生しました"
|
||||
"pages.somethingWentWrong": "{statusCode} - 問題が発生しました",
|
||||
"components.TvDetails.TvCast.fullseriescast": "フルシリーズキャスト",
|
||||
"components.Settings.validationPortRequired": "ポートの入力が必要です",
|
||||
"components.Settings.validationHostnameRequired": "ホスト名/IPの入力が必要です",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "サーバー名を指定してください",
|
||||
"components.Settings.SettingsAbout.version": "バージョン",
|
||||
"components.Settings.SettingsAbout.totalrequests": "総リクエスト数",
|
||||
"components.Settings.SettingsAbout.totalmedia": "総メディア数",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr情報",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHubディスカッション",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "サポート",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Discordサーバーの参加はこちら。",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "サーバー名を指定してください",
|
||||
"components.Settings.Notifications.emailsettingssaved": "メール通知設定が保存されました!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "メール通知設定の保存に失敗しました。",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "ディスコード通知設定の保存に失敗しました。",
|
||||
"components.Settings.Notifications.discordsettingssaved": "ディスコードの通知設定が保存されました!",
|
||||
"components.MovieDetails.MovieCast.fullcast": "フルキャスト",
|
||||
"i18n.deleting": "削除中…",
|
||||
"components.UserList.userdeleteerror": "ユーザーの削除する時に問題が発生しました",
|
||||
"components.UserList.userdeleted": "ユーザーが削除されました",
|
||||
"components.UserList.deleteuser": "ユーザーの削除",
|
||||
"components.UserList.deleteconfirm": "このユーザーを削除しますか?このユーザーの既存のリクエストデータはすべて削除されます。",
|
||||
"components.TvDetails.showtype": "番組タイプ",
|
||||
"components.TvDetails.network": "ネットワーク",
|
||||
"components.TvDetails.anime": "アニメ",
|
||||
"components.Setup.tip": "ヒント",
|
||||
"components.Setup.syncingbackground": "同期はバックグラウンドで実行されます。その間、セットアッププロセスを続行できます。",
|
||||
"components.Settings.toastSettingsSuccess": "設定の変更は保存しました。",
|
||||
"components.Settings.toastSettingsFailure": "設定保存中に問題が発生しました。",
|
||||
"components.Settings.toastApiKeySuccess": "新しい API キーが生成されました。",
|
||||
"components.Settings.toastApiKeyFailure": "新しい API キーの生成中に問題が発生しました。",
|
||||
"components.Settings.nodefaultdescription": "サービスへのリクエストを行う前に、一つのサーバーをデフォルトとして設定する必要があります。",
|
||||
"components.Settings.nodefault": "デフォルトサーバーが選択されていません。",
|
||||
"components.Settings.no4kimplemented": "( 4K サーバーのデフォルト設定は未実装)",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "ルートフォルダーをロードするには先に接続をテストしてください",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "画質プロファイルをロードするには先に接続をテストしてください",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "ルートフォルダー読込中…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "画質プロファイル読込中…",
|
||||
"components.Settings.SonarrModal.animerootfolder": "アニメルートフォルダー",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "アニメ画質プロファイル",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "最低リリース状況を選択してください",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "ルートフォルダーをロードするには先に接続をテストしてください",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "画質プロファイルをロードするには先に接続をテストしてください",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "画質プロファイルの読込中…",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "ルートフォルダー読込中…",
|
||||
"components.MovieDetails.studio": "スタジオ"
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"components.Discover.nopending": "Geen Wachtende Verzoeken",
|
||||
"components.Discover.popularmovies": "Populaire Films",
|
||||
"components.Discover.populartv": "Populaire Series",
|
||||
"components.Discover.recentlyAdded": "RecentelijkToegevoegd",
|
||||
"components.Discover.recentlyAdded": "Recentelijk Toegevoegd",
|
||||
"components.Discover.recentrequests": "Recente Verzoeken",
|
||||
"components.Discover.trending": "Trending",
|
||||
"components.Discover.upcoming": "Verwachte Films",
|
||||
"components.Discover.upcomingmovies": "Verwachte Films",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Wijzig Taal",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Zoek Films & TV",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Zoek Films & Series",
|
||||
"components.Layout.Sidebar.dashboard": "Ontdek",
|
||||
"components.Layout.Sidebar.requests": "Verzoeken",
|
||||
"components.Layout.Sidebar.settings": "Instellingen",
|
||||
@@ -32,10 +32,10 @@
|
||||
"components.MovieDetails.originallanguage": "Originele Taal",
|
||||
"components.MovieDetails.overview": "Overzicht",
|
||||
"components.MovieDetails.overviewunavailable": "Overzicht onbeschikbaar",
|
||||
"components.MovieDetails.pending": "In afwachting",
|
||||
"components.MovieDetails.pending": "Wachtende",
|
||||
"components.MovieDetails.recommendations": "Aanbevelingen",
|
||||
"components.MovieDetails.recommendationssubtext": "Als je {title} leuk vond, vind je dit misschien ook leuk...",
|
||||
"components.MovieDetails.releasedate": "Release Datum",
|
||||
"components.MovieDetails.recommendationssubtext": "Als je {title} leuk vond, vind je dit misschien ook leuk…",
|
||||
"components.MovieDetails.releasedate": "Releasedatum",
|
||||
"components.MovieDetails.request": "Verzoek",
|
||||
"components.MovieDetails.revenue": "Omzet",
|
||||
"components.MovieDetails.runtime": "{minutes} minuten",
|
||||
@@ -48,259 +48,290 @@
|
||||
"components.PersonDetails.appearsin": "Verschijnt in",
|
||||
"components.PersonDetails.ascharacter": "als {character}",
|
||||
"components.PersonDetails.nobiography": "Geen biografie beschikbaar.",
|
||||
"components.PlexLoginButton.loading": "Bezig met laden...",
|
||||
"components.PlexLoginButton.loggingin": "Bezig met inloggen...",
|
||||
"components.PlexLoginButton.loading": "Bezig met laden…",
|
||||
"components.PlexLoginButton.loggingin": "Bezig met inloggen…",
|
||||
"components.PlexLoginButton.loginwithplex": "Login met Plex",
|
||||
"components.RequestBlock.seasons": "Seizoenen",
|
||||
"components.RequestCard.all": "Alle",
|
||||
"components.RequestCard.requestedby": "Verzocht door {username}",
|
||||
"components.RequestCard.seasons": "Seizoenen",
|
||||
"components.RequestList.RequestItem.notavailable": "Nvt",
|
||||
"components.RequestList.RequestItem.requestedby": "",
|
||||
"components.RequestList.RequestItem.seasons": "",
|
||||
"components.RequestList.mediaInfo": "",
|
||||
"components.RequestList.modifiedBy": "",
|
||||
"components.RequestList.next": "",
|
||||
"components.RequestList.previous": "",
|
||||
"components.RequestList.requestedAt": "",
|
||||
"components.RequestList.requests": "",
|
||||
"components.RequestList.showingresults": "",
|
||||
"components.RequestList.status": "",
|
||||
"components.RequestModal.cancel": "",
|
||||
"components.RequestModal.cancelling": "",
|
||||
"components.RequestModal.cancelrequest": "",
|
||||
"components.RequestModal.close": "",
|
||||
"components.RequestModal.extras": "",
|
||||
"components.RequestModal.notrequested": "",
|
||||
"components.RequestModal.numberofepisodes": "",
|
||||
"components.RequestModal.pendingrequest": "",
|
||||
"components.RequestModal.request": "",
|
||||
"components.RequestModal.requestCancel": "",
|
||||
"components.RequestModal.requestSuccess": "",
|
||||
"components.RequestModal.requestadmin": "",
|
||||
"components.RequestModal.requestfrom": "",
|
||||
"components.RequestModal.requesting": "",
|
||||
"components.RequestModal.requestseasons": "",
|
||||
"components.RequestModal.requesttitle": "",
|
||||
"components.RequestModal.season": "",
|
||||
"components.RequestModal.seasonnumber": "",
|
||||
"components.RequestModal.selectseason": "",
|
||||
"components.RequestModal.status": "",
|
||||
"components.Search.searchresults": "",
|
||||
"components.Settings.Notifications.agentenabled": "",
|
||||
"components.Settings.Notifications.authPass": "",
|
||||
"components.Settings.Notifications.authUser": "",
|
||||
"components.Settings.Notifications.emailsender": "",
|
||||
"components.Settings.Notifications.enableSsl": "",
|
||||
"components.Settings.Notifications.save": "",
|
||||
"components.Settings.Notifications.saving": "",
|
||||
"components.Settings.Notifications.smtpHost": "",
|
||||
"components.Settings.Notifications.smtpPort": "",
|
||||
"components.Settings.Notifications.validationFromRequired": "",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "",
|
||||
"components.Settings.Notifications.webhookUrl": "",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "",
|
||||
"components.Settings.RadarrModal.add": "",
|
||||
"components.Settings.RadarrModal.apiKey": "",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "",
|
||||
"components.Settings.RadarrModal.baseUrl": "",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "",
|
||||
"components.Settings.RadarrModal.createradarr": "",
|
||||
"components.Settings.RadarrModal.defaultserver": "",
|
||||
"components.Settings.RadarrModal.editradarr": "",
|
||||
"components.Settings.RadarrModal.hostname": "",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "",
|
||||
"components.Settings.RadarrModal.port": "",
|
||||
"components.Settings.RadarrModal.qualityprofile": "",
|
||||
"components.Settings.RadarrModal.rootfolder": "",
|
||||
"components.Settings.RadarrModal.save": "",
|
||||
"components.Settings.RadarrModal.saving": "",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "",
|
||||
"components.Settings.RadarrModal.server4k": "",
|
||||
"components.Settings.RadarrModal.servername": "",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "",
|
||||
"components.Settings.RadarrModal.ssl": "",
|
||||
"components.Settings.RadarrModal.test": "",
|
||||
"components.Settings.RadarrModal.testing": "",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "",
|
||||
"components.Settings.SonarrModal.add": "",
|
||||
"components.Settings.SonarrModal.apiKey": "",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "",
|
||||
"components.Settings.SonarrModal.baseUrl": "",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "",
|
||||
"components.Settings.SonarrModal.createsonarr": "",
|
||||
"components.Settings.SonarrModal.defaultserver": "",
|
||||
"components.Settings.SonarrModal.editsonarr": "",
|
||||
"components.Settings.SonarrModal.hostname": "",
|
||||
"components.Settings.SonarrModal.port": "",
|
||||
"components.Settings.SonarrModal.qualityprofile": "",
|
||||
"components.Settings.SonarrModal.rootfolder": "",
|
||||
"components.Settings.SonarrModal.save": "",
|
||||
"components.Settings.SonarrModal.saving": "",
|
||||
"components.Settings.SonarrModal.seasonfolders": "",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "",
|
||||
"components.Settings.SonarrModal.server4k": "",
|
||||
"components.Settings.SonarrModal.servername": "",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "",
|
||||
"components.Settings.SonarrModal.ssl": "",
|
||||
"components.Settings.SonarrModal.test": "",
|
||||
"components.Settings.SonarrModal.testing": "",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "",
|
||||
"components.Settings.activeProfile": "",
|
||||
"components.Settings.addradarr": "",
|
||||
"components.Settings.address": "",
|
||||
"components.Settings.addsonarr": "",
|
||||
"components.Settings.apikey": "",
|
||||
"components.Settings.applicationurl": "",
|
||||
"components.Settings.cancelscan": "",
|
||||
"components.Settings.copied": "",
|
||||
"components.Settings.currentlibrary": "",
|
||||
"components.Settings.default": "",
|
||||
"components.Settings.default4k": "",
|
||||
"components.Settings.delete": "",
|
||||
"components.Settings.deleteserverconfirm": "",
|
||||
"components.Settings.edit": "",
|
||||
"components.Settings.generalsettings": "",
|
||||
"components.Settings.generalsettingsDescription": "",
|
||||
"components.Settings.hostname": "",
|
||||
"components.Settings.jobname": "",
|
||||
"components.Settings.librariesRemaining": "",
|
||||
"components.Settings.manualscan": "",
|
||||
"components.Settings.manualscanDescription": "",
|
||||
"components.Settings.menuAbout": "",
|
||||
"components.Settings.menuGeneralSettings": "",
|
||||
"components.Settings.menuJobs": "",
|
||||
"components.Settings.menuLogs": "",
|
||||
"components.Settings.menuNotifications": "",
|
||||
"components.Settings.menuPlexSettings": "",
|
||||
"components.Settings.menuServices": "",
|
||||
"components.Settings.nextexecution": "",
|
||||
"components.Settings.notificationsettings": "",
|
||||
"components.Settings.notificationsettingsDescription": "",
|
||||
"components.Settings.notrunning": "",
|
||||
"components.Settings.plexlibraries": "",
|
||||
"components.Settings.plexlibrariesDescription": "",
|
||||
"components.Settings.plexsettings": "",
|
||||
"components.Settings.plexsettingsDescription": "",
|
||||
"components.Settings.port": "",
|
||||
"components.Settings.radarrSettingsDescription": "",
|
||||
"components.Settings.radarrsettings": "",
|
||||
"components.Settings.runnow": "",
|
||||
"components.Settings.save": "",
|
||||
"components.Settings.saving": "",
|
||||
"components.Settings.servername": "",
|
||||
"components.Settings.servernamePlaceholder": "",
|
||||
"components.Settings.sonarrSettingsDescription": "",
|
||||
"components.Settings.sonarrsettings": "",
|
||||
"components.Settings.ssl": "",
|
||||
"components.Settings.startscan": "",
|
||||
"components.Settings.sync": "",
|
||||
"components.Settings.syncing": "",
|
||||
"components.Setup.configureplex": "",
|
||||
"components.Setup.configureservices": "",
|
||||
"components.Setup.continue": "",
|
||||
"components.Setup.finish": "",
|
||||
"components.Setup.finishing": "",
|
||||
"components.Setup.loginwithplex": "",
|
||||
"components.Setup.signinMessage": "",
|
||||
"components.Setup.welcome": "",
|
||||
"components.Slider.noresults": "",
|
||||
"components.TitleCard.movie": "",
|
||||
"components.TitleCard.tvshow": "",
|
||||
"components.TvDetails.approve": "",
|
||||
"components.TvDetails.approverequests": "",
|
||||
"components.TvDetails.available": "",
|
||||
"components.TvDetails.cancelrequest": "",
|
||||
"components.TvDetails.cast": "",
|
||||
"components.TvDetails.decline": "",
|
||||
"components.TvDetails.declinerequests": "",
|
||||
"components.TvDetails.manageModalClearMedia": "",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "",
|
||||
"components.TvDetails.manageModalNoRequests": "",
|
||||
"components.TvDetails.manageModalRequests": "",
|
||||
"components.TvDetails.manageModalTitle": "",
|
||||
"components.TvDetails.originallanguage": "",
|
||||
"components.TvDetails.overview": "",
|
||||
"components.TvDetails.overviewunavailable": "",
|
||||
"components.TvDetails.pending": "",
|
||||
"components.TvDetails.recommendations": "",
|
||||
"components.TvDetails.recommendationssubtext": "",
|
||||
"components.TvDetails.request": "",
|
||||
"components.TvDetails.requestmore": "",
|
||||
"components.TvDetails.similar": "",
|
||||
"components.TvDetails.similarsubtext": "",
|
||||
"components.TvDetails.status": "",
|
||||
"components.TvDetails.unavailable": "",
|
||||
"components.TvDetails.userrating": "",
|
||||
"components.UserEdit.admin": "",
|
||||
"components.UserEdit.adminDescription": "",
|
||||
"components.UserEdit.autoapprove": "",
|
||||
"components.UserEdit.autoapproveDescription": "",
|
||||
"components.UserEdit.avatar": "",
|
||||
"components.UserEdit.edituser": "",
|
||||
"components.UserEdit.email": "",
|
||||
"components.UserEdit.managerequests": "",
|
||||
"components.UserEdit.managerequestsDescription": "",
|
||||
"components.UserEdit.permissions": "",
|
||||
"components.UserEdit.request": "",
|
||||
"components.UserEdit.requestDescription": "",
|
||||
"components.UserEdit.save": "",
|
||||
"components.UserEdit.saving": "",
|
||||
"components.UserEdit.settings": "",
|
||||
"components.UserEdit.settingsDescription": "",
|
||||
"components.UserEdit.userfail": "",
|
||||
"components.UserEdit.username": "",
|
||||
"components.UserEdit.users": "",
|
||||
"components.UserEdit.usersDescription": "",
|
||||
"components.UserEdit.usersaved": "",
|
||||
"components.UserEdit.vote": "",
|
||||
"components.UserEdit.voteDescription": "",
|
||||
"components.UserList.admin": "",
|
||||
"components.UserList.created": "",
|
||||
"components.UserList.delete": "",
|
||||
"components.UserList.edit": "",
|
||||
"components.UserList.lastupdated": "",
|
||||
"components.UserList.plexuser": "",
|
||||
"components.UserList.role": "",
|
||||
"components.UserList.totalrequests": "",
|
||||
"components.UserList.user": "",
|
||||
"components.UserList.userlist": "",
|
||||
"components.UserList.username": "",
|
||||
"components.UserList.usertype": "",
|
||||
"i18n.approve": "",
|
||||
"i18n.approved": "",
|
||||
"i18n.available": "",
|
||||
"i18n.cancel": "",
|
||||
"i18n.decline": "",
|
||||
"i18n.declined": "",
|
||||
"i18n.delete": "",
|
||||
"i18n.movies": "",
|
||||
"i18n.partiallyavailable": "",
|
||||
"i18n.pending": "",
|
||||
"i18n.processing": "",
|
||||
"i18n.tvshows": "",
|
||||
"i18n.unavailable": "",
|
||||
"pages.internalServerError": "",
|
||||
"pages.oops": "",
|
||||
"pages.pageNotFound": "",
|
||||
"pages.returnHome": "",
|
||||
"pages.serviceUnavailable": "",
|
||||
"pages.somethingWentWrong": ""
|
||||
"components.RequestList.RequestItem.requestedby": "Verzocht door {username}",
|
||||
"components.RequestList.RequestItem.seasons": "Seizoenen",
|
||||
"components.RequestList.mediaInfo": "Media Info",
|
||||
"components.RequestList.modifiedBy": "Laatst Aangepast Door",
|
||||
"components.RequestList.next": "Volgende",
|
||||
"components.RequestList.previous": "Vorige",
|
||||
"components.RequestList.requestedAt": "Aangevraagd Om",
|
||||
"components.RequestList.requests": "Verzoeken",
|
||||
"components.RequestList.showingresults": "<strong>{from}</strong> tot <strong>{to}</strong> van de <strong>{total}</strong> resultaten worden weergegeven",
|
||||
"components.RequestList.status": "Status",
|
||||
"components.RequestModal.cancel": "Verzoek Annuleren",
|
||||
"components.RequestModal.cancelling": "Bezig met annuleren…",
|
||||
"components.RequestModal.cancelrequest": "Hiermee wordt je verzoek verwijderd. Weet je zeker dat je door wilt gaan?",
|
||||
"components.RequestModal.close": "Sluiten",
|
||||
"components.RequestModal.extras": "Extra",
|
||||
"components.RequestModal.notrequested": "Niet Verzocht",
|
||||
"components.RequestModal.numberofepisodes": "Nr afleveringen",
|
||||
"components.RequestModal.pendingrequest": "Wachtend verzoek voor {title}",
|
||||
"components.RequestModal.request": "Verzoek",
|
||||
"components.RequestModal.requestCancel": "Verzoek voor <strong>{title}</strong> is geannuleerd",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> is verzocht.",
|
||||
"components.RequestModal.requestadmin": "Je verzoek zal onmiddelijk goedgekeurd worden.",
|
||||
"components.RequestModal.requestfrom": "Er is een afwachtend verzoek van {user}",
|
||||
"components.RequestModal.requesting": "Bezig met verzoeken…",
|
||||
"components.RequestModal.requestseasons": "Verzoek {seasonCount} {seasonCount, plural, één {Season} andere {Seasons}}",
|
||||
"components.RequestModal.requesttitle": "Verzoek {title}",
|
||||
"components.RequestModal.season": "Seizoen",
|
||||
"components.RequestModal.seasonnumber": "Seizoen {number}",
|
||||
"components.RequestModal.selectseason": "Selecteer seizoen(en)",
|
||||
"components.RequestModal.status": "Status",
|
||||
"components.Search.searchresults": "Zoekresultaten",
|
||||
"components.Settings.Notifications.agentenabled": "Agent Ingeschakeld",
|
||||
"components.Settings.Notifications.authPass": "Wachtwoord",
|
||||
"components.Settings.Notifications.authUser": "Gebruikersnaam",
|
||||
"components.Settings.Notifications.emailsender": "E-mailadres Van Afzender",
|
||||
"components.Settings.Notifications.enableSsl": "Schakel SSL in",
|
||||
"components.Settings.Notifications.save": "Wijzigingen Opslaan",
|
||||
"components.Settings.Notifications.saving": "Bezig met opslaan…",
|
||||
"components.Settings.Notifications.smtpHost": "SMTP Host",
|
||||
"components.Settings.Notifications.smtpPort": "SMTP Poort",
|
||||
"components.Settings.Notifications.validationFromRequired": "Je moet een afzenderadres opgeven",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Je moet een SMTP host opgeven",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Je moet een SMTP poort opgeven",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "Je moet een webhook URL opgeven",
|
||||
"components.Settings.Notifications.webhookUrl": "Webhook URL",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "Server instellingen -> Integraties -> Webhooks",
|
||||
"components.Settings.RadarrModal.add": "Server Toevoegen",
|
||||
"components.Settings.RadarrModal.apiKey": "API Key",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "Je Radarr API Key",
|
||||
"components.Settings.RadarrModal.baseUrl": "Basis URL",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "Voorbeeld: /radarr",
|
||||
"components.Settings.RadarrModal.createradarr": "Maak een nieuwe Radarr-server aan",
|
||||
"components.Settings.RadarrModal.defaultserver": "Standaard Server",
|
||||
"components.Settings.RadarrModal.editradarr": "Wijzig Radarr Server",
|
||||
"components.Settings.RadarrModal.hostname": "Hostnaam",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Minimale Beschikbaarheid",
|
||||
"components.Settings.RadarrModal.port": "Poort",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Kwaliteitsprofiel",
|
||||
"components.Settings.RadarrModal.rootfolder": "Hoofdmap",
|
||||
"components.Settings.RadarrModal.save": "Wijzigingen Opslaan",
|
||||
"components.Settings.RadarrModal.saving": "Bezig met opslaan…",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Selecteer minimale beschikbaarheid",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Selecteer een kwaliteitsprofiel",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Selecteer een Hoofdmap",
|
||||
"components.Settings.RadarrModal.server4k": "4K Server",
|
||||
"components.Settings.RadarrModal.servername": "Servernaam",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "Een Radarr Server",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.test": "Test",
|
||||
"components.Settings.RadarrModal.testing": "Bezig met testen…",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Kon niet verbinden met de Radarr Server",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Verbonden met de Radarr server!",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "Je moet een API key opgeven",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Je moet een hostnaam/IP opgeven",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "Je moet een poort opgeven",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "Je moet een profiel selecteren",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Je moet een hoofdmap selecteren",
|
||||
"components.Settings.SonarrModal.add": "Server Toevoegen",
|
||||
"components.Settings.SonarrModal.apiKey": "API Key",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "Je Sonarr API Key",
|
||||
"components.Settings.SonarrModal.baseUrl": "Basis URL",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "Voorbeeld: /sonarr",
|
||||
"components.Settings.SonarrModal.createsonarr": "Maak een Nieuwe Sonar Server aan",
|
||||
"components.Settings.SonarrModal.defaultserver": "Standaard Server",
|
||||
"components.Settings.SonarrModal.editsonarr": "Wijzig Sonarr Server",
|
||||
"components.Settings.SonarrModal.hostname": "Hostnaam",
|
||||
"components.Settings.SonarrModal.port": "Poort",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Kwaliteitsprofiel",
|
||||
"components.Settings.SonarrModal.rootfolder": "Basisfolder",
|
||||
"components.Settings.SonarrModal.save": "Wijzigingen Opslaan",
|
||||
"components.Settings.SonarrModal.saving": "Bezig met opslaan…",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Seizoenmappen",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Selecteer een Kwaliteitsprofiel",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Selecteer een hoofdfolder",
|
||||
"components.Settings.SonarrModal.server4k": "4K Server",
|
||||
"components.Settings.SonarrModal.servername": "Servernaam",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "Een Sonarr Server",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.test": "Test",
|
||||
"components.Settings.SonarrModal.testing": "Bezig met testen…",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Kon niet verbinden met de Sonarr Server",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Verbonden met de Sonarr server!",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "Je moet een API key opgeven",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "Je moet een hostnaam/IP opgeven",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "Je moet een poort opgeven",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "Je moet een profiel selecteren",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "Je moet een hoofdfolder selecteren",
|
||||
"components.Settings.activeProfile": "Actief profiel",
|
||||
"components.Settings.addradarr": "Radarr Server Toevoegen",
|
||||
"components.Settings.address": "Adres",
|
||||
"components.Settings.addsonarr": "Voeg een Sonarr Server Toe",
|
||||
"components.Settings.apikey": "API Key",
|
||||
"components.Settings.applicationurl": "Applicatie URL",
|
||||
"components.Settings.cancelscan": "Scan Annuleren",
|
||||
"components.Settings.copied": "API key gekopieerd naar het klembord",
|
||||
"components.Settings.currentlibrary": "Huidige Bibliotheek: {name}",
|
||||
"components.Settings.default": "Standaard",
|
||||
"components.Settings.default4k": "Standaard 4K",
|
||||
"components.Settings.delete": "Verwijderen",
|
||||
"components.Settings.deleteserverconfirm": "Weet je zeker dat je deze server wil verwijderen?",
|
||||
"components.Settings.edit": "Wijzig",
|
||||
"components.Settings.generalsettings": "Algemene Instellingen",
|
||||
"components.Settings.generalsettingsDescription": "Deze instellingen zijn gerelateerd aan de algemene Overseer configuratie.",
|
||||
"components.Settings.hostname": "Hostnaam/IP",
|
||||
"components.Settings.jobname": "Taaknaam",
|
||||
"components.Settings.librariesRemaining": "Resterende Bibliotheken: {count}",
|
||||
"components.Settings.manualscan": "Handmatige Bibliotheek Scan",
|
||||
"components.Settings.manualscanDescription": "Normaal gesproken wordt dit eens elke 6 uur gedraaid. Overseerr controleert de recentelijk toegoevoegde items van je Plex Server aggresiever. Als dit je eerst keer is om Plex te configuren raden wij aan om eenmaal een volledige bibliotheek scan handmatig te draaien!",
|
||||
"components.Settings.menuAbout": "Over",
|
||||
"components.Settings.menuGeneralSettings": "Algemene Instellingen",
|
||||
"components.Settings.menuJobs": "Taken",
|
||||
"components.Settings.menuLogs": "Logboeken",
|
||||
"components.Settings.menuNotifications": "Notificaties",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuServices": "Diensten",
|
||||
"components.Settings.nextexecution": "Volgende Uitvoering",
|
||||
"components.Settings.notificationsettings": "Notificatie Instellingen",
|
||||
"components.Settings.notificationsettingsDescription": "Hier kun je kiezen welke soort notificaties er worden verstuurd en door welke type diensten.",
|
||||
"components.Settings.notrunning": "Draait niet",
|
||||
"components.Settings.plexlibraries": "Plex Bibliotheken",
|
||||
"components.Settings.plexlibrariesDescription": "The bibliotheken die Overseerr scanned voor titels. Stel je Plex connectie instellingen in en sla ze op. Klik op de onderstaande knop als er geen connecties staan.",
|
||||
"components.Settings.plexsettings": "Plex Instellingen",
|
||||
"components.Settings.plexsettingsDescription": "Configureer de instellingen voor je Plex server. Overseerr gebruikt je Plex server om je bibliotheek met een interval te scannen om te zien welke content beschikbaar is.",
|
||||
"components.Settings.port": "Poort",
|
||||
"components.Settings.radarrSettingsDescription": "Stel hier onder je Radarr connectie in. Je kan er meerdere hebben, maar slechts twee actief hebben als standaard (één voor standaard HD, en één voor 4K). Beheerders kunnen bepalen welke server gebruikt wordt voor nieuwe verzoeken.",
|
||||
"components.Settings.radarrsettings": "Radarr Instellingen",
|
||||
"components.Settings.runnow": "Nu Starten",
|
||||
"components.Settings.save": "Wijzigingen Opslaan",
|
||||
"components.Settings.saving": "Bezig met opslaan…",
|
||||
"components.Settings.servername": "Servernaam (Automatisch ingesteld na opslaan)",
|
||||
"components.Settings.servernamePlaceholder": "Plex Servernaam",
|
||||
"components.Settings.sonarrSettingsDescription": "Stel hier onder je Sonarr connectie in. Je kan er meerdere hebben, maar slechts twee actief hebben als standaard (één voor standaard HD, en één voor 4K). Beheerders kunnen bepalen welke server gebruikt wordt voor nieuwe verzoeken.",
|
||||
"components.Settings.sonarrsettings": "Sonarr Instellingen",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.startscan": "Scan Starten",
|
||||
"components.Settings.sync": "Sync Plex Bibliotheken",
|
||||
"components.Settings.syncing": "Bezig met synchronizeren…",
|
||||
"components.Setup.configureplex": "Configureer Plex",
|
||||
"components.Setup.configureservices": "Configureer Diensten",
|
||||
"components.Setup.continue": "Doorgaan",
|
||||
"components.Setup.finish": "Installatie Voltooien",
|
||||
"components.Setup.finishing": "Bezig met voltooien…",
|
||||
"components.Setup.loginwithplex": "Login met Plex",
|
||||
"components.Setup.signinMessage": "Ga aan de slag door in te loggen met je Plex account",
|
||||
"components.Setup.welcome": "Welcome bij Overseerr",
|
||||
"components.Slider.noresults": "Geen Resultaten",
|
||||
"components.TitleCard.movie": "Film",
|
||||
"components.TitleCard.tvshow": "Series",
|
||||
"components.TvDetails.approve": "Goedkeuren",
|
||||
"components.TvDetails.approverequests": "Keur {requestCount} {requestCount, plural, één {Request} meerdere {Requests}} goed",
|
||||
"components.TvDetails.available": "Beschikbaar",
|
||||
"components.TvDetails.cancelrequest": "Verzoek Annuleren",
|
||||
"components.TvDetails.cast": "Cast",
|
||||
"components.TvDetails.decline": "Weigeren",
|
||||
"components.TvDetails.declinerequests": "Weiger {requestCount} {requestCount, plural, één {Request} meerdere {Requests}}",
|
||||
"components.TvDetails.manageModalClearMedia": "Wis Alle Media Data",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "Dit wist alle media data inclusief alle verzoeken voor dit item zonder herstelmogelijkheden. Als dit item in je Plex bibliotheek bestaat zal alle media info bij de volgende sync hersteld worden.",
|
||||
"components.TvDetails.manageModalNoRequests": "Geen Verzoeken",
|
||||
"components.TvDetails.manageModalRequests": "Verzoeken",
|
||||
"components.TvDetails.manageModalTitle": "Beheer Series",
|
||||
"components.TvDetails.originallanguage": "Originele Taal",
|
||||
"components.TvDetails.overview": "Overzicht",
|
||||
"components.TvDetails.overviewunavailable": "Overzicht onbeschikbaar",
|
||||
"components.TvDetails.pending": "Wachtende",
|
||||
"components.TvDetails.recommendations": "Aanbevelingen",
|
||||
"components.TvDetails.recommendationssubtext": "Als je {title} leuk vond, vind je dit misschien ook leuk…",
|
||||
"components.TvDetails.request": "Verzoek",
|
||||
"components.TvDetails.requestmore": "Verzoek Meer",
|
||||
"components.TvDetails.similar": "Soortgelijke Series",
|
||||
"components.TvDetails.similarsubtext": "Andere series gelijk aan {title}",
|
||||
"components.TvDetails.status": "Status",
|
||||
"components.TvDetails.unavailable": "Onbeschikbaar",
|
||||
"components.TvDetails.userrating": "Gebruikersbeoordeling",
|
||||
"components.UserEdit.admin": "Beheerder",
|
||||
"components.UserEdit.adminDescription": "Volledige beheerderstoegang. Omzeilt alle toestemmingscontroles.",
|
||||
"components.UserEdit.autoapprove": "Automatisch Goedkeuren",
|
||||
"components.UserEdit.autoapproveDescription": "Geeft automatische goedkeuring voor alle verzoeken van deze gebruiker.",
|
||||
"components.UserEdit.avatar": "Avatar",
|
||||
"components.UserEdit.edituser": "Wijzig Gebruiker",
|
||||
"components.UserEdit.email": "E-mail",
|
||||
"components.UserEdit.managerequests": "Beheer Verzoeken",
|
||||
"components.UserEdit.managerequestsDescription": "Geeft toestemming om Overseerr-verzoeken te beheren. Inclusief het goedkeuren en weigeren van verzoeken.",
|
||||
"components.UserEdit.permissions": "Rechten",
|
||||
"components.UserEdit.request": "Verzoek",
|
||||
"components.UserEdit.requestDescription": "Geeft toestemming om films en series aan te vragen.",
|
||||
"components.UserEdit.save": "Sla op",
|
||||
"components.UserEdit.saving": "Bezig met opslaan…",
|
||||
"components.UserEdit.settings": "Instellingen Beheren",
|
||||
"components.UserEdit.settingsDescription": "Geeft toestemming om alle Overseerr-instellingen te wijzigen. Een gebruiker moet deze toestemming hebben om deze aan anderen te verlenen.",
|
||||
"components.UserEdit.userfail": "Er ging iets verkeerd bij het opslaan van de gebruiker.",
|
||||
"components.UserEdit.username": "Gebruikersnaam",
|
||||
"components.UserEdit.users": "Beheer Gebruikers",
|
||||
"components.UserEdit.usersDescription": "Geeft toestemming om Overseerr-gebruikers te beheren. Gebruikers met deze toestemming kunnen gebruikers met beheerdersrechten niet wijzigen of deze toestemming geven.",
|
||||
"components.UserEdit.usersaved": "Gebruiker opgeslagen",
|
||||
"components.UserEdit.vote": "Stem",
|
||||
"components.UserEdit.voteDescription": "Geeft toestemming om te stemmen op verzoeken (stemmen is nog niet geïmplementeerd)",
|
||||
"components.UserList.admin": "Beheerder",
|
||||
"components.UserList.created": "Gemaakt",
|
||||
"components.UserList.delete": "Verwijderen",
|
||||
"components.UserList.edit": "Wijzig",
|
||||
"components.UserList.lastupdated": "Laatst Aangepast",
|
||||
"components.UserList.plexuser": "Plex Gebruiker",
|
||||
"components.UserList.role": "Rol",
|
||||
"components.UserList.totalrequests": "Totale Verzoeken",
|
||||
"components.UserList.user": "Gebruiker",
|
||||
"components.UserList.userlist": "Gebruikerslijst",
|
||||
"components.UserList.username": "Gebruikersnaam",
|
||||
"components.UserList.usertype": "Gebruikerstype",
|
||||
"i18n.approve": "Goedkeuren",
|
||||
"i18n.approved": "Goedgekeurd",
|
||||
"i18n.available": "Beschikbaar",
|
||||
"i18n.cancel": "Annuleer",
|
||||
"i18n.decline": "Weigeren",
|
||||
"i18n.declined": "Geweigerd",
|
||||
"i18n.delete": "Verwijder",
|
||||
"i18n.movies": "Films",
|
||||
"i18n.partiallyavailable": "Gedeeltelijk Beschikbaar",
|
||||
"i18n.pending": "Afwachtende",
|
||||
"i18n.processing": "Bezig met verwerken…",
|
||||
"i18n.tvshows": "Series",
|
||||
"i18n.unavailable": "Onbeschikbaar",
|
||||
"pages.internalServerError": "{statusCode} – Interne Server Error",
|
||||
"pages.oops": "Oeps",
|
||||
"pages.pageNotFound": "404 – Pagina Niet Gevonden",
|
||||
"pages.returnHome": "Terug naar de Startpagina",
|
||||
"pages.serviceUnavailable": "{statusCode} – Service Onbeschikbaar",
|
||||
"pages.somethingWentWrong": "{statusCode} – Er is iets verkeerd gegaan",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Volledige Cast",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Volledige Cast van de Series",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Instellingen voor e-mailmeldingen opgeslagen!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Instellingen voor e-mailmeldingen zijn niet opgeslagen.",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Discord notificatie instellingen zijn opgeslagen!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Discord notificatie instellingen konden niet opgeslagen worden.",
|
||||
"components.Settings.validationPortRequired": "Je moet een poort opgeven",
|
||||
"components.Settings.validationHostnameRequired": "Je moet een hostnaam/IP opgeven",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Je moet een servernaam opgeven",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Je moet een servernaam opgeven",
|
||||
"components.Settings.SettingsAbout.version": "Versie",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Totale Verzoeken",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Totale media",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseer Informatie",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussies",
|
||||
"components.Setup.tip": "Tip",
|
||||
"components.Setup.syncingbackground": "Synchroniseren draait in de achtergrond. Je kan ondertussen door met het setup proces.",
|
||||
"components.Settings.nodefaultdescription": "Tenminste één server moet geselecteerd zijn als standaard voordat verzoeken doorkomen bij je diensten.",
|
||||
"components.Settings.nodefault": "Geen standaard server geselecteerd!",
|
||||
"components.Settings.no4kimplemented": "(Standaard 4K servers zijn momenteel niet geïmplementeerd)",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Test je connectie om hoofdfolders te laden",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test je connectie om kwaliteitsprofielen te laden",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Bezig met laden van hoofdfolders…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Ondersteuning Krijgen",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Klik hier voor onze Discord server.",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Je moet een minimale bechikbaarheid selecteren",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Test je connectie om hoofdfolders te laden",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test je connectie om kwaliteitsprofielen te laden",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Bezig met laden van hoofdfolders…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…"
|
||||
}
|
||||
|
||||
232
src/i18n/locale/pt_BR.json
Normal file
232
src/i18n/locale/pt_BR.json
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"components.RequestList.requestedAt": "Solicitado Em",
|
||||
"components.RequestList.next": "Próxima",
|
||||
"components.RequestList.modifiedBy": "Última Mudança Feita Por",
|
||||
"components.RequestList.mediaInfo": "Títulos",
|
||||
"components.RequestList.RequestItem.requestedby": "Solicitado por {username}",
|
||||
"components.RequestCard.seasons": "Temporadas",
|
||||
"components.RequestCard.requestedby": "Solicitado por {username}",
|
||||
"components.RequestBlock.seasons": "Temporadas",
|
||||
"components.PlexLoginButton.loginwithplex": "Entrar com",
|
||||
"components.PlexLoginButton.loggingin": "Fazendo login...",
|
||||
"components.PlexLoginButton.loading": "Carregando...",
|
||||
"components.PersonDetails.nobiography": "Biografia não disponível.",
|
||||
"components.PersonDetails.ascharacter": "como {character}",
|
||||
"components.PersonDetails.appearsin": "Aparece em",
|
||||
"components.MovieDetails.viewrequest": "Ver solicitação",
|
||||
"components.MovieDetails.userrating": "Avaliação do usuário",
|
||||
"components.MovieDetails.unavailable": "Indisponível",
|
||||
"components.MovieDetails.studio": "Estúdio",
|
||||
"components.MovieDetails.status": "Detalhes",
|
||||
"components.MovieDetails.similarsubtext": "Outros filmes semelhantes a {title}",
|
||||
"components.MovieDetails.similar": "Títulos Semelhantes",
|
||||
"components.MovieDetails.runtime": "{minutes} minutos",
|
||||
"components.MovieDetails.revenue": "Receita",
|
||||
"components.MovieDetails.request": "Solicitar",
|
||||
"components.MovieDetails.releasedate": "Data de Lançamento",
|
||||
"components.MovieDetails.recommendationssubtext": "Se você gostou {title}, você provavelmente irá gostar...",
|
||||
"components.MovieDetails.recommendations": "Recomendações",
|
||||
"components.MovieDetails.pending": "Pendente",
|
||||
"components.MovieDetails.overviewunavailable": "Visão Geral Indisponível",
|
||||
"components.MovieDetails.overview": "Visão Geral",
|
||||
"components.MovieDetails.originallanguage": "Língua original",
|
||||
"components.MovieDetails.manageModalTitle": "Gerenciar Filme",
|
||||
"components.MovieDetails.manageModalRequests": "Solicitações",
|
||||
"components.MovieDetails.manageModalNoRequests": "Nenhuma Solicitação",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Isso irá remover em definitivo todos dados de mídia, incluindo todas 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.MovieDetails.manageModalClearMedia": "Limpar Todos Dados de Mídia",
|
||||
"components.MovieDetails.decline": "Rejeitar",
|
||||
"components.MovieDetails.cast": "Elenco",
|
||||
"components.MovieDetails.cancelrequest": "Cancelar Solicitação",
|
||||
"components.MovieDetails.budget": "Orçamento",
|
||||
"components.MovieDetails.available": "Disponível",
|
||||
"components.MovieDetails.approve": "Aprovar",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Elenco Completo",
|
||||
"components.Login.signinplex": "Faça login para continuar",
|
||||
"components.Layout.alphawarning": "Esta é uma versão Alpha. Quase tudo é instável ou não funciona. Por favor reporte os problemas no GitHub do Overseerr!",
|
||||
"components.Layout.UserDropdown.signout": "Sair",
|
||||
"components.Layout.Sidebar.users": "Usuários",
|
||||
"components.Layout.Sidebar.settings": "Configurações",
|
||||
"components.Layout.Sidebar.requests": "Solicitações",
|
||||
"components.Layout.Sidebar.dashboard": "Descobrir",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Pesquisar Filmes & Séries",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Alterar Idioma",
|
||||
"components.Discover.upcomingmovies": "Em Breve",
|
||||
"components.Discover.upcoming": "Em Breve",
|
||||
"components.Discover.trending": "Em Alta",
|
||||
"components.Discover.recentrequests": "Solicitações Recentes",
|
||||
"components.Discover.recentlyAdded": "Adicionado recentemente",
|
||||
"components.Discover.populartv": "Séries Populares",
|
||||
"components.Discover.popularmovies": "Filmes Populares",
|
||||
"components.Discover.nopending": "Nenhuma Solicitação Pendente",
|
||||
"components.Discover.discovertv": "Séries Populares",
|
||||
"components.Discover.discovermovies": "Filmes Populares",
|
||||
"components.Settings.SonarrModal.testing": "Testando...",
|
||||
"components.Settings.SonarrModal.saving": "Salvando...",
|
||||
"components.Settings.RadarrModal.testing": "Testando...",
|
||||
"components.Settings.RadarrModal.saving": "Salvando...",
|
||||
"components.Settings.Notifications.saving": "Salvando...",
|
||||
"components.RequestModal.cancelling": "Cancelando...",
|
||||
"components.Settings.plexlibraries": "Bibliotecas do Plex",
|
||||
"components.Settings.notrunning": "Parado",
|
||||
"components.Settings.notificationsettingsDescription": "Aqui você pode escolher e selecionar os tipos de notificações e através de quais serviços deseja enviar.",
|
||||
"pages.pageNotFound": "404 - Página Não Encontrada",
|
||||
"components.RequestModal.season": "Temporada",
|
||||
"components.Settings.notificationsettings": "Configurações de Notificação",
|
||||
"components.Settings.nodefaultdescription": "Pelo menos um servidor deve ser selecionado como padrão antes que qualquer solicitação chegue aos seus serviços.",
|
||||
"components.Settings.nodefault": "Nenhum servidor padrão selecionado!",
|
||||
"components.Settings.no4kimplemented": "(Servidores 4K padrões não configurados atualmente)",
|
||||
"components.Settings.nextexecution": "Próxima Execução",
|
||||
"components.Settings.menuServices": "Serviços",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuNotifications": "Notificações",
|
||||
"components.Settings.menuLogs": "Logs",
|
||||
"components.Settings.menuJobs": "Tarefas",
|
||||
"components.Settings.menuGeneralSettings": "Configurações Gerais",
|
||||
"components.Settings.menuAbout": "Sobre",
|
||||
"components.Settings.manualscanDescription": "Normalmente, isso só será executado uma vez a cada 6 horas. Overseerr irá checar em detalhes items recentemente adicionados ao seu servidor Plex. Se essa é a primeira vez que você configura um servidor Plex, é recomendado a varredura completa de sua biblioteca!",
|
||||
"components.Settings.manualscan": "Varredura Manual da Biblioteca",
|
||||
"components.Settings.librariesRemaining": "Bibliotecas Restantes: {count}",
|
||||
"components.Settings.jobname": "Nome da Tarefa",
|
||||
"components.Settings.hostname": "Nome de Servidor/IP",
|
||||
"components.Settings.generalsettingsDescription": "Essas são configurações gerais do Overseerr.",
|
||||
"components.Settings.generalsettings": "Configurações Gerais",
|
||||
"components.Settings.edit": "Editar",
|
||||
"components.Settings.deleteserverconfirm": "Tem certeza que deseja apagar esse servidor?",
|
||||
"components.Settings.delete": "Apagar",
|
||||
"components.UserList.delete": "Apagar",
|
||||
"i18n.deleting": "Apagando…",
|
||||
"i18n.delete": "Apagar",
|
||||
"components.Settings.default4k": "Padrão 4K",
|
||||
"components.Settings.default": "Padrão",
|
||||
"components.Settings.currentlibrary": "Biblioteca Atual: {name}",
|
||||
"components.Settings.copied": "Chave de API copiada",
|
||||
"components.Settings.cancelscan": "Cancelar Escaneamento",
|
||||
"components.Settings.applicationurl": "URL da Aplicação",
|
||||
"components.Settings.apikey": "Chave de API",
|
||||
"components.Settings.addsonarr": "Adicionar Servidor Sonarr",
|
||||
"components.Settings.address": "Endereço",
|
||||
"components.Settings.addradarr": "Adicionar Servidor Radarr",
|
||||
"components.Settings.activeProfile": "Perfil Ativo",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "Você deve selecionar uma pasta raíz",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "Você deve selecionar um perfil",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "Você deve prover uma porta",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Você deve prover o nome do servidor",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "Você deve prover o Nome do Servidor/IP",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "Você deve prover uma chave de API",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Conexão estabelecida com servidor Sonarr!",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Falha ao conectar ao Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Teste sua conexão para carregar as pastas raízes",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Teste sua conexão para carregar perfis de qualidade",
|
||||
"components.Settings.SonarrModal.test": "Testar",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "Meu Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.servername": "Nome do Servidor",
|
||||
"components.Settings.SonarrModal.server4k": "Servidor 4K",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Selecione a Pasta Raíz",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Selecione o Perfil de Qualidade",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Temporadas Em Pastas",
|
||||
"components.Settings.SonarrModal.save": "Salvar Mudanças",
|
||||
"components.Settings.SonarrModal.rootfolder": "Pasta Raíz",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Perfil de Qualidade",
|
||||
"components.Settings.SonarrModal.port": "Porta",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Carregando Pastas Raízes…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Carregando Perfis de Qualidade…",
|
||||
"components.Settings.SonarrModal.hostname": "Nome do Servidor / IP",
|
||||
"components.Settings.SonarrModal.editsonarr": "Editar Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.defaultserver": "Servidor Padrão",
|
||||
"components.Settings.SonarrModal.createsonarr": "Criar Um Novo Servidor Sonarr",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "Exemplo: /sonarr",
|
||||
"components.Settings.SonarrModal.baseUrl": "URL Base",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "Sua Chave de API do Sonarr",
|
||||
"components.Settings.SonarrModal.apiKey": "Chave de API",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Pasta Raíz de Animes",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Perfil de Qualidade Para Animes",
|
||||
"components.Settings.SonarrModal.add": "Adicionar Servidor",
|
||||
"components.Settings.SettingsAbout.version": "Versão",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total de Solicitações",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total de Títulos",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Sobre Overseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Discussões no GitHub",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Obtenha Suporte",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Clique aqui para participar de nosso servidor Discord.",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Você deve selecionar uma pasta raíz",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "Você deve selecionar um perfil",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "Você deve prover uma porta",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Você deve prover o nome do servidor",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Você deve selecionar a disponibilidade mínima",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Você deve prover o Nome do Servidor/IP",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "Você deve prover uma chave de API",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Conexão estabelecida com Radarr!",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Falha ao conectar ao Servidor Radarr",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Teste sua conexão para carregar as pastas raízes",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Teste sua conexão para carregar perfis de qualidade",
|
||||
"components.Settings.RadarrModal.test": "Testar",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "Meu Servidor Radarr",
|
||||
"components.Settings.RadarrModal.servername": "Nome do Servidor",
|
||||
"components.Settings.RadarrModal.server4k": "Servidor 4K",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Selecione a Pasta Raíz",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Selecione o Perfil de Qualidade",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Selecione disponibilidade mínima",
|
||||
"components.Settings.RadarrModal.save": "Salvar Mudanças",
|
||||
"components.Settings.RadarrModal.rootfolder": "Pasta Raíz",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Perfil de Qualidade",
|
||||
"components.Settings.RadarrModal.port": "Porta",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Disponibilidade Mínima",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Carregando Pastas Raízes…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Carregando Perfis de Qualidade…",
|
||||
"components.Settings.RadarrModal.hostname": "Nome do Servidor / IP",
|
||||
"components.Settings.RadarrModal.editradarr": "Editar Servidor Radarr",
|
||||
"components.Settings.RadarrModal.defaultserver": "Servidor Padrão",
|
||||
"components.Settings.RadarrModal.createradarr": "Criar Um Novo Servidor Radarr",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "Exemplo: /radarr",
|
||||
"components.Settings.RadarrModal.baseUrl": "URL Base",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "Sua Chave de API do Radarr",
|
||||
"components.Settings.RadarrModal.apiKey": "Chave de API",
|
||||
"components.Settings.RadarrModal.add": "Adicionar Servidor",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "Configurações do Servidor -> Integrações -> Webhooks",
|
||||
"components.Settings.Notifications.webhookUrl": "URL de Webhook",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "Você deve prover uma URL de webhook",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Você deve prover a porta SMTP",
|
||||
"components.Settings.Notifications.validationFromRequired": "Você deve prover um e-mail de remetente",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Você deve prover um servidor SMTP",
|
||||
"components.Settings.Notifications.smtpPort": "Porta SMTP",
|
||||
"components.Settings.Notifications.smtpHost": "Servidor SMTP",
|
||||
"components.Settings.Notifications.save": "Salvar Mudanças",
|
||||
"components.Settings.Notifications.enableSsl": "Habilitar SSL",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Configurações de notificação via e-mail salvas!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Falha ao salvar configurações de notificação via e-mail.",
|
||||
"components.Settings.Notifications.emailsender": "Email do Remetente",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Configurações de notificação do Discord salvas!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Falha ao salvar configurações de notificação do Discord.",
|
||||
"components.Settings.Notifications.authUser": "Usuário",
|
||||
"components.Settings.Notifications.authPass": "Senha",
|
||||
"components.Settings.Notifications.agentenabled": "Agente Habilitado",
|
||||
"components.Search.searchresults": "Resultados da Pesquisa",
|
||||
"components.TvDetails.status": "Estado",
|
||||
"components.RequestModal.status": "Estado",
|
||||
"components.RequestModal.selectseason": "Selecione temporada(s)",
|
||||
"components.RequestModal.seasonnumber": "Temporada {number}",
|
||||
"components.RequestModal.requesttitle": "Solicitar {title}",
|
||||
"components.RequestModal.requesting": "Solicitando…",
|
||||
"components.RequestModal.requestfrom": "Existe uma solicitação pendende de {username}",
|
||||
"components.RequestModal.requestadmin": "Sua solicitação será aprovada imediatamente.",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> solicitado.",
|
||||
"components.RequestModal.requestCancel": "Solicitação para <strong>{title}</strong> cancelada",
|
||||
"components.RequestModal.request": "Solicitar",
|
||||
"components.RequestModal.pendingrequest": "Solicitação pendente para {title}",
|
||||
"components.RequestModal.numberofepisodes": "# de Episódeos",
|
||||
"components.RequestModal.notrequested": "Não Solicitado",
|
||||
"components.RequestModal.extras": "Extras",
|
||||
"components.RequestModal.close": "Fechar",
|
||||
"components.RequestModal.cancelrequest": "Isso irá cancelar sua solicitação. Você tem certeza que deseja continuar?",
|
||||
"components.RequestModal.cancel": "Cancelar Solicitação",
|
||||
"components.RequestList.status": "Estado",
|
||||
"components.RequestList.showingresults": "Exibindo <strong>{from}</strong> a <strong>{to}</strong> de <strong>{total}</strong> resultados",
|
||||
"components.RequestList.requests": "Solicitações",
|
||||
"components.RequestList.previous": "Anterior",
|
||||
"components.RequestList.RequestItem.seasons": "Temporadas",
|
||||
"components.RequestList.RequestItem.notavailable": "N/A",
|
||||
"components.RequestCard.all": "Todas"
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
"components.PersonDetails.appearsin": "",
|
||||
"components.PersonDetails.ascharacter": "",
|
||||
"components.PersonDetails.nobiography": "",
|
||||
"components.PlexLoginButton.loading": "Загрузка...",
|
||||
"components.PlexLoginButton.loading": "Загрузка…",
|
||||
"components.PlexLoginButton.loggingin": "",
|
||||
"components.PlexLoginButton.loginwithplex": "Войти через Plex",
|
||||
"components.RequestBlock.seasons": "Сезонов",
|
||||
@@ -93,7 +93,7 @@
|
||||
"components.Settings.Notifications.emailsender": "",
|
||||
"components.Settings.Notifications.enableSsl": "Включить SSL",
|
||||
"components.Settings.Notifications.save": "Сохранить изменения",
|
||||
"components.Settings.Notifications.saving": "Сохранение...",
|
||||
"components.Settings.Notifications.saving": "Сохранение…",
|
||||
"components.Settings.Notifications.smtpHost": "",
|
||||
"components.Settings.Notifications.smtpPort": "",
|
||||
"components.Settings.Notifications.validationFromRequired": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"components.Settings.RadarrModal.qualityprofile": "",
|
||||
"components.Settings.RadarrModal.rootfolder": "Корневая папка",
|
||||
"components.Settings.RadarrModal.save": "Сохранить изменения",
|
||||
"components.Settings.RadarrModal.saving": "Сохранение...",
|
||||
"components.Settings.RadarrModal.saving": "Сохранение…",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Выберите корневую папку",
|
||||
@@ -125,7 +125,7 @@
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.test": "Тест",
|
||||
"components.Settings.RadarrModal.testing": "Тестирование...",
|
||||
"components.Settings.RadarrModal.testing": "Тестирование…",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "",
|
||||
|
||||
352
src/i18n/locale/sr.json
Normal file
352
src/i18n/locale/sr.json
Normal file
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"components.RequestList.mediaInfo": "Informacija o fajlu",
|
||||
"components.TvDetails.manageModalClearMedia": "Obriši sve infomacije o fajlu",
|
||||
"components.TvDetails.approverequests": "Odobri {requestCount} {requestCount, plural, {Request} drugih {Requests}}",
|
||||
"components.RequestModal.requestseasons": "Zatraži {seasonCount} {seasonCount, plural, jednu {Season} više {Seasons}}",
|
||||
"components.RequestList.showingresults": "Prikazivanje <strong>{from}</strong> do <strong>{to}</strong> od <strong>{total}</strong> rezultata",
|
||||
"components.MovieDetails.decline": "Poništi",
|
||||
"components.Layout.Sidebar.dashboard": "Pronadji Novo",
|
||||
"pages.somethingWentWrong": "{statusCode} - Nesto se pokvarilo",
|
||||
"pages.serviceUnavailable": "{statusCode} - Servis nije dostupan",
|
||||
"pages.returnHome": "Povratak na glavnu stranicu",
|
||||
"pages.pageNotFound": "404 - Stranica Nije Pronadjena",
|
||||
"pages.oops": "Ups",
|
||||
"pages.internalServerError": "{statusCode} - Greska na serveru",
|
||||
"i18n.unavailable": "Nije dostupno",
|
||||
"i18n.tvshows": "Serije",
|
||||
"i18n.processing": "Obradjuje se…",
|
||||
"i18n.pending": "Na čekanju",
|
||||
"i18n.partiallyavailable": "Polovično Dostupno",
|
||||
"i18n.movies": "Filmovi",
|
||||
"i18n.deleting": "Brisanje u toku…",
|
||||
"i18n.delete": "Izbriši",
|
||||
"i18n.declined": "Odbijeno",
|
||||
"i18n.decline": "Poništi",
|
||||
"i18n.cancel": "Poništi",
|
||||
"i18n.available": "Dostupno",
|
||||
"i18n.approved": "Prihvaćeno",
|
||||
"i18n.approve": "Prihvati",
|
||||
"components.UserList.usertype": "Tip Korisnika",
|
||||
"components.UserList.username": "Korisničko Ime",
|
||||
"components.UserList.userlist": "Lista Korisnika",
|
||||
"components.UserList.userdeleteerror": "Neuspešno brisanje korisnika",
|
||||
"components.UserList.userdeleted": "Korisnik izbrisan",
|
||||
"components.UserList.user": "Korisnik",
|
||||
"components.UserList.totalrequests": "Ukupno Zahteva",
|
||||
"components.UserList.role": "Uloga",
|
||||
"components.UserList.plexuser": "Plex Korisnik",
|
||||
"components.UserList.lastupdated": "Zadnja Promena",
|
||||
"components.UserList.edit": "Edituj",
|
||||
"components.UserList.deleteuser": "Izbriši Korisnika",
|
||||
"components.UserList.deleteconfirm": "Da li ste sigurni da želite da izbrišete ovog korisnika? Svi njegovi zahtevi će biti izbrisani.",
|
||||
"components.UserList.delete": "Izbriši",
|
||||
"components.UserList.created": "Napravljeno",
|
||||
"components.UserList.admin": "Administrator",
|
||||
"components.UserEdit.voteDescription": "Dodaje permisiju da glasa na zahteve (glasanje jos nije implementirano)",
|
||||
"components.UserEdit.vote": "Glasaj",
|
||||
"components.UserEdit.usersaved": "Korisnik sačuvan",
|
||||
"components.UserEdit.usersDescription": "Dodaje permisiju da upravlja Overseerr korisnicima. Korisnici sa ovom permisiju ne mogu da modifikuju korisnike sa Administratorskim statusom, niti da ga dodaju.",
|
||||
"components.UserEdit.users": "Upravljaj Korisnicima",
|
||||
"components.UserEdit.username": "Korisničko ime",
|
||||
"components.UserEdit.userfail": "Neuspešno sačuvavanje korisnika.",
|
||||
"components.UserEdit.settingsDescription": "Dodaje permisiju da menja sva Overseerr podešavanja. Korisnik mora da ima ovu permisiju da bi je dodao drugima.",
|
||||
"components.UserEdit.settings": "Upravljaj Podešavanjima",
|
||||
"components.UserEdit.saving": "Sačuvavanje…",
|
||||
"components.UserEdit.save": "Sačuvaj",
|
||||
"components.UserEdit.requestDescription": "Dodaje permisiju za zahtevanje filmova i serija.",
|
||||
"components.UserEdit.request": "Zahtev",
|
||||
"components.UserEdit.permissions": "Permisije",
|
||||
"components.UserEdit.managerequestsDescription": "Dodaje permisiju za upravljanje Overseerr zahtevima. Ovo uključuje prihvatanje i odbijanje zahteva.",
|
||||
"components.UserEdit.managerequests": "Upravljaj Zahtevima",
|
||||
"components.UserEdit.email": "Email",
|
||||
"components.UserEdit.edituser": "Edituj Korisnika",
|
||||
"components.UserEdit.avatar": "Slika",
|
||||
"components.UserEdit.autoapproveDescription": "Dozvoljava automatsko prihvatanje zahteva za ovog korisnika.",
|
||||
"components.UserEdit.autoapprove": "Automatski Odobri",
|
||||
"components.UserEdit.adminDescription": "Potpuni Administrativni Pristup. Zaobilazi sve provere permisija.",
|
||||
"components.UserEdit.admin": "Admin",
|
||||
"components.TvDetails.userrating": "Korisnički Rejting",
|
||||
"components.TvDetails.unavailable": "Nije dostupno",
|
||||
"components.TvDetails.status": "Status",
|
||||
"components.TvDetails.similarsubtext": "Slične serije kao {title}",
|
||||
"components.TvDetails.similar": "Slične Serije",
|
||||
"components.TvDetails.showtype": "Tip Serije",
|
||||
"components.TvDetails.requestmore": "Zatraži još",
|
||||
"components.TvDetails.request": "Zatraži",
|
||||
"components.TvDetails.recommendationssubtext": "Ako vam se svidja {title}, možda vam se svidi…",
|
||||
"components.TvDetails.recommendations": "Preporuke",
|
||||
"components.TvDetails.pending": "Na čekanju",
|
||||
"components.TvDetails.overviewunavailable": "Pregled nije dostupan",
|
||||
"components.TvDetails.overview": "Pregled",
|
||||
"components.TvDetails.originallanguage": "Originalni Jezik",
|
||||
"components.TvDetails.network": "Mreža",
|
||||
"components.TvDetails.manageModalTitle": "Upravljaj Serijama",
|
||||
"components.TvDetails.manageModalRequests": "Zahtevi",
|
||||
"components.TvDetails.manageModalNoRequests": "Nema Zahteva",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "Ovo će ukloniti sve podatke o mediji uključujući i zahteve za ovaj sadrzaj nepovratno. Ako ovaj sadržaj postoji u vašoj plex biblioteci infomacija o mediji će biti kreirana pri sledećoj sinhronizaciji.",
|
||||
"components.TvDetails.declinerequests": "Odbij {requestCount} {requestCount, plural, jedan {Request} drugih {Requests}}",
|
||||
"components.TvDetails.decline": "Odbij",
|
||||
"components.TvDetails.cast": "Repertoar",
|
||||
"components.TvDetails.cancelrequest": "Otkaži Zahtev",
|
||||
"components.TvDetails.available": "Dostupno",
|
||||
"components.TvDetails.approve": "Odobri",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Ceo Repertoar Serije",
|
||||
"components.TitleCard.tvshow": "Serija",
|
||||
"components.TitleCard.movie": "Film",
|
||||
"components.Slider.noresults": "Nema Rezultata",
|
||||
"components.Setup.welcome": "Dobrodošli u Overseerr",
|
||||
"components.Setup.tip": "Savet",
|
||||
"components.Setup.syncingbackground": "Sinhronizacija će raditi u pozadini. Možete da nastavite podešavanja u toku rada.",
|
||||
"components.Setup.signinMessage": "Započni sa prijavljivanjem sa Vašim Plex profilom",
|
||||
"components.Setup.loginwithplex": "Prijavljivanje sa Plexom",
|
||||
"components.Setup.finishing": "Završavam…",
|
||||
"components.Setup.finish": "Završi Podešavanja",
|
||||
"components.Setup.continue": "Nastavi",
|
||||
"components.Setup.configureservices": "Konfigurišite Servise",
|
||||
"components.Setup.configureplex": "Konfigurišite Plex",
|
||||
"components.Settings.validationPortRequired": "Morate dodati port",
|
||||
"components.Settings.validationHostnameRequired": "Morate dodati hostname ili IP adresu",
|
||||
"components.Settings.toastSettingsSuccess": "Podešavanja sačuvana.",
|
||||
"components.Settings.toastSettingsFailure": "Nešto se pokvarilo pri čuvanju podešavanja.",
|
||||
"components.Settings.toastApiKeySuccess": "Novi API Ključ generisan!",
|
||||
"components.Settings.toastApiKeyFailure": "Nešto nije odradjeno kako treba pri generisanju API ključa.",
|
||||
"components.Settings.syncing": "Sinhronizacija u toku…",
|
||||
"components.Settings.sync": "Sinhronizuj Plex Sadržaj",
|
||||
"components.Settings.startscan": "Pokreni Skeniranje",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.sonarrsettings": "Sonarr Podešavanja",
|
||||
"components.Settings.sonarrSettingsDescription": "Podesite vaš Sonarr server ispod. Možete imati više, ali samo 2 mogu biti aktivna u bilo kom trenutku (jedan za HD, drugi za 4K). Administratori mogu da odaberu koji server je za nove zahteve.",
|
||||
"components.Settings.servernamePlaceholder": "Ime Plex Servera",
|
||||
"components.Settings.servername": "Ime Servera (Automatski se podesi kada sačuvate)",
|
||||
"components.Settings.saving": "Sačuvavanje u toku…",
|
||||
"components.Settings.save": "Sačuvaj Promene",
|
||||
"components.Settings.runnow": "Pokreni Sad",
|
||||
"components.Settings.radarrsettings": "Radarr Podešavanja",
|
||||
"components.Settings.radarrSettingsDescription": "Podesite vaš Radarr server ispod. Možete imati više od jednog, ali samo 2 mogu biti aktivna u bilo kom trenutku (jedan za HD, drugi za 4K). Administratori mogu da odaberu koji server je za nove zahteve.",
|
||||
"components.Settings.port": "Port",
|
||||
"components.Settings.plexsettingsDescription": "Konfigurišite podešavanja za Vaš Plex server. Overseerr koristi vaš Plex server da skenira vaš sadržaj u nekom intervalu da proveri šta je dostupno.",
|
||||
"components.Settings.plexsettings": "Plex Podešavanja",
|
||||
"components.Settings.plexlibrariesDescription": "Overseerr skenira sadržaj za imena. Dodajte informacije o Plex konekciji i kliknite dole dugme ako nijedan nije na listi.",
|
||||
"components.Settings.plexlibraries": "Plex Sadržaj",
|
||||
"components.Settings.notificationsettingsDescription": "Ovde možete odabrati tipove notifikacija koji se šalju i ka kojim servisima.",
|
||||
"components.Settings.notificationsettings": "Podešavanje Notifikacija",
|
||||
"components.Settings.nodefaultdescription": "Barem jedan server mora biti označen kao defaultni da bi se zahtevi poslali ka servisima.",
|
||||
"components.Settings.nodefault": "Nijedan defaultni server nije odabran!",
|
||||
"components.Settings.no4kimplemented": "(Defaultni 4K serveri nisu trenutno implementirani)",
|
||||
"components.Settings.nextexecution": "Sledeće pokretanje",
|
||||
"components.Settings.menuServices": "Servisi",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuNotifications": "Notifikacije",
|
||||
"components.Settings.menuLogs": "Logovi",
|
||||
"components.Settings.menuJobs": "Poslovi",
|
||||
"components.Settings.menuGeneralSettings": "Standardna Podešavanja",
|
||||
"components.Settings.menuAbout": "O nama",
|
||||
"components.Settings.manualscanDescription": "Normalno, ovo će biti pokrenutno na svakih 6 sati. Overseerr će proveriti nedavno dodati Plex sadržaj češće. Ako je ovo prvi put da se Plex konfiguriše, jedan ručni scan sadržaja je preporučen!",
|
||||
"components.Settings.manualscan": "Ručno Skeniranje Sadržaja",
|
||||
"components.Settings.librariesRemaining": "Broj sadržaja koji se obradjuje: {count}",
|
||||
"components.Settings.jobname": "Ime posla",
|
||||
"components.Settings.hostname": "Hostname/IP",
|
||||
"components.Settings.generalsettingsDescription": "Ovo su podešavanja vezana za uobičajenu Overseerr konfiguraciju.",
|
||||
"components.Settings.generalsettings": "Standarna Podešavanja",
|
||||
"components.Settings.edit": "Izmena",
|
||||
"components.Settings.deleteserverconfirm": "Da li ste sigurni da želite da izbrišete ovaj server?",
|
||||
"components.Settings.delete": "Izbriši",
|
||||
"components.Settings.default4k": "Defaultno 4K",
|
||||
"components.Settings.default": "Defaultno",
|
||||
"components.Settings.currentlibrary": "Trenutna Biblioteka: {name}",
|
||||
"components.Settings.copied": "Kopiran API ključ",
|
||||
"components.Settings.cancelscan": "Otkaži skeniranje",
|
||||
"components.Settings.applicationurl": "URL Aplikacije",
|
||||
"components.Settings.apikey": "API Ključ",
|
||||
"components.Settings.addsonarr": "Dodaj Sonarr Server",
|
||||
"components.Settings.address": "Adresa",
|
||||
"components.Settings.addradarr": "Dodaj Radarr Server",
|
||||
"components.Settings.activeProfile": "Aktivni Profil",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "Morate odabrati root folder",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "Morate odabrati profil kvaliteta",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "Morate popuniti port",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Morate popuniti ime servera",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "Morate dodati hostname ili IP adresu",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "Morate dodati API ključ",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Konekcija sa Sonarr serverom uspešna!",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Konekcija sa Sonarr Serverom neuspešna",
|
||||
"components.Settings.SonarrModal.testing": "Testiranje…",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Testirajte Vašu konekciju da učitate root foldere",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Testirajte Vašu konekciju da učitate profile kvaliteta",
|
||||
"components.Settings.SonarrModal.test": "Test",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "Sonarr Server",
|
||||
"components.Settings.SonarrModal.servername": "Ime Servera",
|
||||
"components.Settings.SonarrModal.server4k": "4K Server",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Odaberi Root Folder",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Odaberi Profil Kvaliteta",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Folderi Sezone",
|
||||
"components.Settings.SonarrModal.saving": "Čuvanje…",
|
||||
"components.Settings.SonarrModal.save": "Sačuvaj Promene",
|
||||
"components.Settings.SonarrModal.rootfolder": "Root Folder",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Profil Kvaliteta",
|
||||
"components.Settings.SonarrModal.port": "Port",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Učitavanje root foldera…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Učitavanje profila kvaliteta…",
|
||||
"components.Settings.SonarrModal.hostname": "Hostname",
|
||||
"components.Settings.SonarrModal.editsonarr": "Edituj Sonarr Server",
|
||||
"components.Settings.SonarrModal.defaultserver": "Defaultni Server",
|
||||
"components.Settings.SonarrModal.createsonarr": "Dodajte Nov Sonarr Server",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "Primer: /sonarr",
|
||||
"components.Settings.SonarrModal.baseUrl": "Osnovni URL",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "Vaš Sonarr API Ključ",
|
||||
"components.Settings.SonarrModal.apiKey": "API Ključ",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Root folder za Anime",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Kvalitet Profila za Anime",
|
||||
"components.Settings.SonarrModal.add": "Dodaj Server",
|
||||
"components.Settings.SettingsAbout.version": "Verzija",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Ukupno Zahteva",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Ukupno Medije",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr Informacije",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub rasprave",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Pomoć",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Kliknite ovde da se učlanite u naš Discord server.",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Morate odabrati root folder",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "Morate odabrati profil",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "Morate dodati port",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Morate dodati ime servera",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Morate odabrati minimum dostupnost",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Morate dodati hostname ili IP adresu",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "Morate dodati API ključ",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Konekcija sa Radarr serverom uspostavljena!",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Neuspešna konekcija ka Radarr Serveru",
|
||||
"components.Settings.RadarrModal.testing": "Testiranje…",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Testirajte vašu konekciju da učitate root foldere",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Testirajte vašu konekciju da učitate profile kvaliteta",
|
||||
"components.Settings.notrunning": "Ne Izvršava se",
|
||||
"components.Settings.RadarrModal.test": "Test",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "Radarr Server",
|
||||
"components.Settings.RadarrModal.servername": "Ime Servera",
|
||||
"components.Settings.RadarrModal.server4k": "4K Server",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Odaberi Root Folder",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Odaberi Profil Kvaliteta",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Odaberi minimum dostupnost",
|
||||
"components.Settings.RadarrModal.saving": "Sačuvavanje…",
|
||||
"components.Settings.RadarrModal.save": "Sačuvaj Promene",
|
||||
"components.Settings.RadarrModal.rootfolder": "Root Fascikla",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Profil Kvaliteta",
|
||||
"components.Settings.RadarrModal.port": "Port",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Minimum Dostupnost",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Učitavanje root foldera…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Učitavanje nivoa kvaliteta…",
|
||||
"components.Settings.RadarrModal.hostname": "Hostname",
|
||||
"components.Settings.RadarrModal.editradarr": "Edituj Radarr Server",
|
||||
"components.Settings.RadarrModal.defaultserver": "Standardni Server",
|
||||
"components.Settings.RadarrModal.createradarr": "Napravi Novi Radarr Server",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "Primer: /radarr",
|
||||
"components.Settings.RadarrModal.baseUrl": "Osnovni URL",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "Vaš Radarr API Ključ",
|
||||
"components.Settings.RadarrModal.apiKey": "API Ključ",
|
||||
"components.Settings.RadarrModal.add": "Dodaj Server",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "Podešavanje Servera -> Integracije -> Webhooks",
|
||||
"components.Settings.Notifications.webhookUrl": "Webhook adresa",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "Morate dodati webhook URL",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Morate popuniti SMTP port polje",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Morate popuniti SMTP host polje",
|
||||
"components.Settings.Notifications.validationFromRequired": "Morate dodati email adresu",
|
||||
"components.Settings.Notifications.smtpPort": "SMTP Port",
|
||||
"components.Settings.Notifications.smtpHost": "SMTP Host",
|
||||
"components.Settings.Notifications.saving": "Čuvam…",
|
||||
"components.Settings.Notifications.save": "Sačuvaj Promene",
|
||||
"components.Settings.Notifications.enableSsl": "Uključi SSL",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Email notifikacija uspešno sačuvana!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Email notifikacija nije uspešno sačuvana.",
|
||||
"components.Settings.Notifications.emailsender": "Adresa email pošiljaoca",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Discord notifikacija uspešno sačuvana!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Discord notifikacija za podešavanje nije uspešno sačuvana.",
|
||||
"components.Settings.Notifications.authUser": "Korisničko ime",
|
||||
"components.Settings.Notifications.authPass": "Šifra",
|
||||
"components.Settings.Notifications.agentenabled": "Agent Ukljucen",
|
||||
"components.Search.searchresults": "Rezultati pretrage",
|
||||
"components.RequestModal.status": "Status",
|
||||
"components.RequestModal.selectseason": "Odaberi sezone",
|
||||
"components.RequestModal.seasonnumber": "Sezona {number}",
|
||||
"components.RequestModal.season": "Sezona",
|
||||
"components.RequestModal.requesttitle": "Zatraži {title}",
|
||||
"components.RequestModal.requesting": "Zahtevanje…",
|
||||
"components.RequestModal.requestfrom": "Trenutno postoji zahtev na čekanju od {username}",
|
||||
"components.RequestModal.requestadmin": "Vaš zahtev će odmah biti prihvaćen.",
|
||||
"components.RequestModal.requestSuccess": "poslat zahtev za <strong>{title}</strong> .",
|
||||
"components.RequestModal.requestCancel": "Zahtev za <strong>{title}</strong> otkazan",
|
||||
"components.RequestModal.request": "Zahtev",
|
||||
"components.RequestModal.pendingrequest": "Zahtev za {title} na čekanju",
|
||||
"components.RequestModal.numberofepisodes": "Broj Epizoda",
|
||||
"components.RequestModal.notrequested": "Nije Zahtevano",
|
||||
"components.RequestModal.extras": "Dodatno",
|
||||
"components.RequestModal.close": "Zatvori",
|
||||
"components.RequestModal.cancelrequest": "Ovo će poništiti vaš zahtev. Da li ste sigurni da želite da otkažete?",
|
||||
"components.RequestModal.cancelling": "Otkazivanje…",
|
||||
"components.RequestModal.cancel": "Otkaži Zahtev",
|
||||
"components.RequestList.status": "Status",
|
||||
"components.RequestList.requests": "Zahtevi",
|
||||
"components.RequestList.requestedAt": "Zahtevano",
|
||||
"components.RequestList.previous": "Prethodni",
|
||||
"components.RequestList.next": "Sledeći",
|
||||
"components.RequestList.modifiedBy": "Zadnji put modifikovao",
|
||||
"components.RequestList.RequestItem.seasons": "Sezone",
|
||||
"components.RequestList.RequestItem.requestedby": "Zahtevao {username}",
|
||||
"components.RequestList.RequestItem.notavailable": "Nije dostupno",
|
||||
"components.RequestCard.seasons": "Sezone",
|
||||
"components.RequestCard.requestedby": "Zahtevao {username}",
|
||||
"components.RequestCard.all": "Sve",
|
||||
"components.RequestBlock.seasons": "Sezone",
|
||||
"components.PlexLoginButton.loginwithplex": "Prijavljivanje sa Plexom",
|
||||
"components.PlexLoginButton.loggingin": "Prijavljivanje…",
|
||||
"components.PlexLoginButton.loading": "Učitavanje…",
|
||||
"components.PersonDetails.nobiography": "Biografija nije dostupna.",
|
||||
"components.PersonDetails.ascharacter": "kao {character}",
|
||||
"components.PersonDetails.appearsin": "Pojavljuje se u",
|
||||
"components.MovieDetails.viewrequest": "Pogledaj Zahtev",
|
||||
"components.MovieDetails.userrating": "Korisnički rejting",
|
||||
"components.MovieDetails.unavailable": "Nije dostupno",
|
||||
"components.MovieDetails.studio": "Studio",
|
||||
"components.MovieDetails.status": "Status",
|
||||
"components.MovieDetails.similarsubtext": "Drugi filmovi slični {title}",
|
||||
"components.MovieDetails.similar": "Slični Naslovi",
|
||||
"components.MovieDetails.runtime": "{minutes} minuta",
|
||||
"components.MovieDetails.revenue": "Prihod",
|
||||
"components.MovieDetails.request": "Zahtev",
|
||||
"components.MovieDetails.releasedate": "Datum Izlaska",
|
||||
"components.MovieDetails.recommendationssubtext": "Ako vam se svidja {title}, možda vam se dopadne…",
|
||||
"components.MovieDetails.recommendations": "Predlozi",
|
||||
"components.MovieDetails.pending": "Na čekanju",
|
||||
"components.MovieDetails.overviewunavailable": "Pregled nije dostupan",
|
||||
"components.MovieDetails.overview": "Pregled",
|
||||
"components.MovieDetails.originallanguage": "Originalni Jezik",
|
||||
"components.MovieDetails.manageModalTitle": "Upravljaj Filmom",
|
||||
"components.MovieDetails.manageModalRequests": "Zahtevi",
|
||||
"components.MovieDetails.manageModalNoRequests": "Nema zahteva",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Ovo će ukloniti sve media podatke uključujuci i sve zahteve za ovu stvar nepovratno. Ako ova stvar postoji u Vašoj Plex biblioteci, informacija o mediji će biti kreirana pri sledećoj sinhronizaciji.",
|
||||
"components.MovieDetails.manageModalClearMedia": "Očisti sve media podatke",
|
||||
"components.MovieDetails.cast": "Postava",
|
||||
"components.MovieDetails.cancelrequest": "Otkaži Zahtev",
|
||||
"components.MovieDetails.budget": "Budžet",
|
||||
"components.MovieDetails.available": "Dostupno",
|
||||
"components.MovieDetails.approve": "Odobri",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Kompletna Glumačka Postava",
|
||||
"components.Login.signinplex": "Prijavite se da bi ste nastavili",
|
||||
"components.Layout.alphawarning": "Ovo je softver u ALPHA stanju. Skoro sve je nestabilno. Molimo Vas prijavite sve probleme na Overseer GitHub stranici!",
|
||||
"components.Layout.UserDropdown.signout": "Odjava",
|
||||
"components.Layout.Sidebar.users": "Korisnici",
|
||||
"components.Layout.Sidebar.settings": "Podešavanja",
|
||||
"components.Layout.Sidebar.requests": "Zahtevi",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Pretraži Filmove i Serije",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Promeni Jezik",
|
||||
"components.Discover.upcomingmovies": "Predstojeći Filmovi",
|
||||
"components.Discover.upcoming": "Predstojeći Filmovi",
|
||||
"components.Discover.trending": "Popularno",
|
||||
"components.Discover.recentrequests": "Nedavni Zahtevi",
|
||||
"components.Discover.recentlyAdded": "Nedavno Dodati",
|
||||
"components.Discover.populartv": "Popularne Serije",
|
||||
"components.Discover.popularmovies": "Popularni Filmovi",
|
||||
"components.Discover.nopending": "Nema zahteva na čekanju",
|
||||
"components.Discover.discovertv": "Popularne Serije",
|
||||
"components.Discover.discovermovies": "Popunarni Filmov"
|
||||
}
|
||||
350
src/i18n/locale/sv.json
Normal file
350
src/i18n/locale/sv.json
Normal file
@@ -0,0 +1,350 @@
|
||||
{
|
||||
"components.Setup.finishing": "Slutför…",
|
||||
"components.Setup.finish": "Slutför konfiguration",
|
||||
"components.Setup.continue": "Fortsätt",
|
||||
"components.Setup.configureservices": "Konfigurera Tjänster",
|
||||
"components.Setup.configureplex": "Konfigurera Plex",
|
||||
"components.Settings.validationPortRequired": "Du måste ange en port",
|
||||
"components.Settings.validationHostnameRequired": "Du måste ange hostname/IP",
|
||||
"components.Settings.toastSettingsSuccess": "Inställningar sparade.",
|
||||
"components.Settings.toastSettingsFailure": "Något gick fel när inställningarna skulle sparas.",
|
||||
"components.Settings.toastApiKeySuccess": "Ny API-nyckel skapad!",
|
||||
"components.Settings.toastApiKeyFailure": "Något gick fel vid skapandet av ny API-nyckel.",
|
||||
"components.Settings.syncing": "Synkroniserar…",
|
||||
"components.Settings.sync": "Synkronisera Plex Bibliotek",
|
||||
"components.Settings.startscan": "Starta scanning",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.sonarrsettings": "Sonarrinställningar",
|
||||
"components.Settings.sonarrSettingsDescription": "Konfigurera anslutningen till Sonarr nedan. Du kan ha multipla Sonarr-servrar men enbart två aktiva som standard (en för standard HD-material och en för 4K-material). Administratörer kan åsidosätta vilken server som används för nya förfrågningar.",
|
||||
"components.Settings.servernamePlaceholder": "Plex Servernamn",
|
||||
"components.Settings.servername": "Servernamn (Anges automatiskt efter att du sparat)",
|
||||
"components.Settings.saving": "Sparar…",
|
||||
"components.Settings.save": "Spara ändringar",
|
||||
"components.Settings.runnow": "Kör Nu",
|
||||
"components.Settings.radarrsettings": "Radarrinställningar",
|
||||
"components.Settings.radarrSettingsDescription": "Konfigurera anslutningen till Radarr nedan. Du kan ha multipla Radarr-servrar men enbart två aktiva som standard (en för standard HD-material och en för 4K-material). Administratörer kan åsidosätta vilken server som används för nya förfrågningar.",
|
||||
"components.Settings.port": "Port",
|
||||
"components.Settings.plexsettingsDescription": "Konfigurera inställningarna för din Plex-server. Overseerr använder din Plex-server för att scanna dina mediabibliotek på givna intervaller för att se vilka media som är tillgängliga.",
|
||||
"components.Settings.plexsettings": "Plexinställningar",
|
||||
"components.Settings.plexlibrariesDescription": "Mediabiblioteken Overseer scannar för titlar. Konfigurera och spara dina Plex anslutningsinställningar och klicka på knappen nedan utifall inga är listade.",
|
||||
"components.Settings.plexlibraries": "Plex Bibliotek",
|
||||
"components.Settings.notrunning": "Körs ej",
|
||||
"components.Settings.notificationsettingsDescription": "Här kan du välja vilka typer av notifikationer som skall skickas och med vilken typ av tjänst.",
|
||||
"components.Settings.notificationsettings": "Notifikationsinställningar",
|
||||
"components.Settings.nodefaultdescription": "Minst en server måste markeras som standard före du kan göra förfrågningar.",
|
||||
"components.Settings.nodefault": "Ingen standard-server är vald!",
|
||||
"components.Settings.no4kimplemented": "(Standard 4K servrar är för tillfället inte implementerat)",
|
||||
"components.Settings.nextexecution": "Nästa körning",
|
||||
"components.Settings.menuServices": "Tjänster",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuNotifications": "Notifikationer",
|
||||
"components.Settings.menuLogs": "Loggar",
|
||||
"components.Settings.menuJobs": "Jobb",
|
||||
"components.Settings.menuGeneralSettings": "Generella Inställningar",
|
||||
"components.Settings.menuAbout": "Om",
|
||||
"components.Settings.manualscanDescription": "Normalt körs denna var sjätte timme. Overseerr kommer kontrollera till Plex Servers \"senast tillagda\" oftare. Om detta är första gången du konfigurerar din Plex Server är det starkt rekommenderat att göra en manuell full scan av ditt mediabibliotek!",
|
||||
"components.Settings.manualscan": "Manuell Biblioteksscan",
|
||||
"components.Settings.librariesRemaining": "Kvarvarande Bibliotek: {count}",
|
||||
"components.Settings.jobname": "Jobbnamn",
|
||||
"components.Settings.hostname": "Hostname/IP",
|
||||
"components.Settings.generalsettingsDescription": "Dessa inställningar avser konfiguration av Overseerr.",
|
||||
"components.Settings.generalsettings": "Generella Inställningar",
|
||||
"components.Settings.edit": "Ändra",
|
||||
"components.Settings.deleteserverconfirm": "Är du säker på att du vill radera servern?",
|
||||
"components.Settings.delete": "Radera",
|
||||
"components.Settings.default4k": "Standard 4K",
|
||||
"components.Settings.default": "Standard",
|
||||
"components.Settings.currentlibrary": "Nuvarande Bibliotek: {name}",
|
||||
"components.Settings.copied": "API-nyckel kopierad",
|
||||
"components.Settings.cancelscan": "Avbryt scanning",
|
||||
"components.Settings.applicationurl": "Applikations-URL",
|
||||
"components.Settings.apikey": "API-nyckel",
|
||||
"components.Settings.addsonarr": "Lägg till Sonarr Server",
|
||||
"components.Settings.address": "Adress",
|
||||
"components.Settings.addradarr": "Lägg till Radarr Server",
|
||||
"components.Settings.activeProfile": "Aktiv Profil",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "Du måste ange en root-mapp",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "Du måste ange en profil",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "Du måste ange en port",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Du måste ange ett servernamn",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "Du måste ange hostname/IP",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "Du måste ange en API-nyckel",
|
||||
"components.Settings.SonarrModal.toastRadarrTestFailure": "Anslutningen till Sonarr misslyckades",
|
||||
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Anslutningen till Sonarr lyckades!",
|
||||
"components.Settings.SonarrModal.testing": "Testar…",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Testa din anslutning för att ladda in root-mappar",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Testa din anslutning för att ladda in kvalitetsprofiler",
|
||||
"components.Settings.SonarrModal.test": "Testa",
|
||||
"components.Settings.SonarrModal.ssl": "SSL",
|
||||
"components.Settings.SonarrModal.servernamePlaceholder": "En Sonarr Server",
|
||||
"components.Settings.SonarrModal.servername": "Servernamn",
|
||||
"components.Settings.SonarrModal.server4k": "4K Server",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Välj en root-mapp",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Välj en kvalitetsprofil",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Säsongsmappar",
|
||||
"components.Settings.SonarrModal.saving": "Sparar…",
|
||||
"components.Settings.SonarrModal.save": "Spara ändringar",
|
||||
"components.Settings.SonarrModal.rootfolder": "Root-mapp",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Kvalitetsprofiler",
|
||||
"components.Settings.SonarrModal.port": "Port",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Laddar root-mappar…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Laddar kvalitetsprofiler…",
|
||||
"components.Settings.SonarrModal.hostname": "Hostname/IP",
|
||||
"components.Settings.SonarrModal.editsonarr": "Ändra Sonarr Server",
|
||||
"components.Settings.SonarrModal.defaultserver": "Standard Server",
|
||||
"components.Settings.SonarrModal.createsonarr": "Skapa ny Sonarr Server",
|
||||
"components.Settings.SonarrModal.baseUrlPlaceholder": "Exempel: /sonarr",
|
||||
"components.Settings.SonarrModal.baseUrl": "Bas-URL",
|
||||
"components.Settings.SonarrModal.apiKeyPlaceholder": "Din Sonarr API-nyckel",
|
||||
"components.Settings.SonarrModal.apiKey": "API-nyckel",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Anime Root-mapp",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Anime Kvalitetsprofil",
|
||||
"components.Settings.SonarrModal.add": "Lägg till Server",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Totalt antal förfrågningar",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Totalt antal mediaobjekt",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Information om Overseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Diskussioner",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Få Support",
|
||||
"components.Settings.SettingsAbout.clickheretojoindiscord": "Klicka här för att gå med i vår Discord server.",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Du måste ange en root-mapp",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "Du måste ange en profil",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "Du måste ange en port",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Du måste ange ett servernamn",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Du måste välja minsta tillgänglighet",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Du måste ange hostname/IP",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "Du måste ange en API-nyckel",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Anslutningen till Radarr lyckades!",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Misslyckades att ansluta till Radarr Server",
|
||||
"components.Settings.RadarrModal.testing": "Testar…",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Testa din anslutning för att ladda in root-mappar",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Testa din anslutning för att ladda in kvalitetsprofiler",
|
||||
"components.Settings.RadarrModal.test": "Testa",
|
||||
"components.Settings.RadarrModal.ssl": "SSL",
|
||||
"components.Settings.RadarrModal.servernamePlaceholder": "En Radarr Server",
|
||||
"components.Settings.RadarrModal.servername": "Servernamn",
|
||||
"components.Settings.RadarrModal.server4k": "4K Server",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Välj en root-mapp",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Välj en kvalitetsprofil",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Ange minsta tillgänglighet",
|
||||
"components.Settings.RadarrModal.saving": "Sparar…",
|
||||
"components.Settings.RadarrModal.save": "Spara ändringar",
|
||||
"components.Settings.RadarrModal.rootfolder": "Root-mapp",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Kvalitetsprofiler",
|
||||
"components.Settings.RadarrModal.port": "Port",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Minsta tillgänglighet",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Laddar root-mappar…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Laddar kvalitetsprofiler…",
|
||||
"components.Settings.RadarrModal.hostname": "Hostname/IP",
|
||||
"components.Settings.RadarrModal.editradarr": "Ändra Radarr Server",
|
||||
"components.Settings.RadarrModal.defaultserver": "Standard Server",
|
||||
"components.Settings.RadarrModal.createradarr": "Skapa ny Radarr Server",
|
||||
"components.Settings.RadarrModal.baseUrlPlaceholder": "Exempel: /radarr",
|
||||
"components.Settings.RadarrModal.baseUrl": "Bas-URL",
|
||||
"components.Settings.RadarrModal.apiKeyPlaceholder": "Din Radarr API-Nyckel",
|
||||
"components.Settings.RadarrModal.apiKey": "API-Nyckel",
|
||||
"components.Settings.RadarrModal.add": "Lägg till Server",
|
||||
"components.Settings.Notifications.webhookUrlPlaceholder": "Serverinställningar -> Integrationer -> Webhooks",
|
||||
"components.Settings.Notifications.webhookUrl": "Webhook-URL",
|
||||
"components.Settings.Notifications.validationWebhookUrlRequired": "Du måste ange en webhook URL",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Du måste ange en SMTP Port",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Du måste ange en SMTP Server",
|
||||
"components.Settings.Notifications.validationFromRequired": "Du måste ange en avsändaradress",
|
||||
"components.Settings.Notifications.smtpPort": "SMTP Port",
|
||||
"components.Settings.Notifications.smtpHost": "SMTP Server",
|
||||
"components.Settings.Notifications.saving": "Sparar…",
|
||||
"components.Settings.Notifications.save": "Spara ändringar",
|
||||
"components.Settings.Notifications.enableSsl": "Aktivera SSL",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Notiferingsinställningar för e-post sparade!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Notifieringsinställningar för Discord misslyckades att sparas.",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Notifieringsinställningar för e-post misslyckades att sparas.",
|
||||
"components.Settings.Notifications.emailsender": "E-post avsändaradress",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Notiferingsinställningar för Discord sparade!",
|
||||
"components.Settings.Notifications.authUser": "Användarnamn",
|
||||
"components.Settings.Notifications.authPass": "Lösenord",
|
||||
"components.Settings.Notifications.agentenabled": "Agent Aktiverad",
|
||||
"components.Search.searchresults": "Sökresultat",
|
||||
"components.RequestModal.status": "Status",
|
||||
"components.RequestModal.selectseason": "Välj säsong(er)",
|
||||
"components.RequestModal.seasonnumber": "Säsong {number}",
|
||||
"components.RequestModal.season": "Säsong",
|
||||
"components.RequestModal.requesttitle": "Begär {title}",
|
||||
"components.RequestModal.requesting": "Skapar förfrågan…",
|
||||
"components.RequestModal.requestfrom": "Det finns en väntande förfrågan från {username}",
|
||||
"components.RequestModal.requestadmin": "Din förfrågan kommer bli godkänd omedelbart.",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> begärd.",
|
||||
"components.RequestModal.requestCancel": "Förfrågan för <strong>{title}</strong> avbruten",
|
||||
"components.RequestModal.request": "Förfrågan",
|
||||
"components.RequestModal.pendingrequest": "Väntande förfrågan för {title]",
|
||||
"components.RequestModal.numberofepisodes": "Antal Avsnitt",
|
||||
"components.RequestModal.notrequested": "Ej begärd",
|
||||
"components.RequestModal.extras": "Extramaterial",
|
||||
"components.RequestModal.close": "Stäng",
|
||||
"components.RequestModal.cancelrequest": "Detta kommer ta bort din förfrågan. Är du säker på att du vill fortsätta?",
|
||||
"components.RequestModal.cancelling": "Tar bort…",
|
||||
"components.RequestModal.cancel": "Avbryt förfrågan",
|
||||
"components.RequestList.status": "Status",
|
||||
"components.RequestList.showingresults": "Visar <strong>{from}</strong> till <strong>{to}</strong> av <strong>{total}</strong> sökresultat",
|
||||
"components.RequestList.requests": "Förfrågan",
|
||||
"components.RequestList.requestedAt": "Begärd",
|
||||
"components.RequestList.previous": "Föregående",
|
||||
"components.RequestList.next": "Nästa",
|
||||
"components.RequestList.modifiedBy": "Senast Ändrad Av",
|
||||
"components.RequestList.mediaInfo": "Media Information",
|
||||
"components.RequestList.RequestItem.seasons": "Säsonger",
|
||||
"components.RequestList.RequestItem.requestedby": "Begärd av {username}",
|
||||
"components.RequestList.RequestItem.notavailable": "N/A",
|
||||
"components.RequestCard.seasons": "Säsonger",
|
||||
"components.RequestCard.requestedby": "Begärd av {username}",
|
||||
"components.RequestCard.all": "Alla",
|
||||
"components.RequestBlock.seasons": "Säsonger",
|
||||
"components.PlexLoginButton.loginwithplex": "Logga in med Plex",
|
||||
"components.PlexLoginButton.loggingin": "Loggar in…",
|
||||
"components.PlexLoginButton.loading": "Laddar…",
|
||||
"components.PersonDetails.nobiography": "Ingen biografi tillgänglig.",
|
||||
"components.PersonDetails.ascharacter": "som {character}",
|
||||
"components.PersonDetails.appearsin": "Kan ses i",
|
||||
"components.MovieDetails.viewrequest": "Visa förfrågan",
|
||||
"components.MovieDetails.userrating": "Användarbetyg",
|
||||
"components.MovieDetails.unavailable": "Otillgänglig",
|
||||
"components.MovieDetails.studio": "Studio",
|
||||
"components.MovieDetails.status": "Status",
|
||||
"components.MovieDetails.similarsubtext": "Andra filmer som liknar {title}",
|
||||
"components.MovieDetails.similar": "Liknande Titlar",
|
||||
"components.MovieDetails.runtime": "{minutes} minuter",
|
||||
"components.MovieDetails.revenue": "Inkomster",
|
||||
"components.MovieDetails.request": "Förfrågan",
|
||||
"components.MovieDetails.releasedate": "Utgivningsdatum",
|
||||
"components.MovieDetails.recommendationssubtext": "Om du tycker om {title}, kanske du också tycker om…",
|
||||
"components.MovieDetails.recommendations": "Rekommendationer",
|
||||
"components.MovieDetails.pending": "Väntande",
|
||||
"components.MovieDetails.overview": "Beskrivning",
|
||||
"components.MovieDetails.overviewunavailable": "Beskrivning otillgänglig",
|
||||
"components.MovieDetails.originallanguage": "Ursprungligt Språk",
|
||||
"components.MovieDetails.manageModalTitle": "Hantera Film",
|
||||
"components.MovieDetails.manageModalRequests": "Förfrågningar",
|
||||
"components.MovieDetails.manageModalNoRequests": "Inga förfrågningar",
|
||||
"components.MovieDetails.manageModalClearMediaWarning": "Detta kommer radera all media data inklusive förfrågningar för det här objektet och kan inte ångras. Om det här objektet finns i ditt Plex-bibliotek kommer media information att återskapas vid nästa synkronisering.",
|
||||
"components.MovieDetails.manageModalClearMedia": "Rensa All Media Data",
|
||||
"components.MovieDetails.decline": "Avböj",
|
||||
"components.MovieDetails.cast": "Roller",
|
||||
"components.MovieDetails.cancelrequest": "Avbryt begäran",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.available": "Tillgänglig",
|
||||
"components.MovieDetails.approve": "Godkänn",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Rollista",
|
||||
"components.Login.signinplex": "Logga in för att fortsätta",
|
||||
"components.Layout.alphawarning": "Detta är ALPHA-mjukvara. Nästan allt riskerar att gå sönder/vara ostabilt. Vänligen rapportera problem till Overseerr GitHub!",
|
||||
"components.Layout.UserDropdown.signout": "Logga ut",
|
||||
"components.Layout.Sidebar.users": "Användare",
|
||||
"components.Layout.Sidebar.settings": "Inställningar",
|
||||
"components.Layout.Sidebar.requests": "Förfrågningar",
|
||||
"components.Layout.Sidebar.dashboard": "Upptäck",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Sök Filmer & TV-Serier",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Byt språk",
|
||||
"components.Discover.upcomingmovies": "Kommande Filmer",
|
||||
"components.Discover.upcoming": "Kommande Filmer",
|
||||
"components.Discover.trending": "Trendande",
|
||||
"components.Discover.recentrequests": "Senaste förfrågningar",
|
||||
"components.Discover.recentlyAdded": "Nyligen tillagda",
|
||||
"components.Discover.populartv": "Populära TV-Serier",
|
||||
"components.Discover.popularmovies": "Populära Filmer",
|
||||
"components.Discover.nopending": "Inga väntande förfrågningar",
|
||||
"components.Discover.discovertv": "Populära TV-Serier",
|
||||
"components.Discover.discovermovies": "Populära Filmer",
|
||||
"components.TvDetails.approve": "Godkänn",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Rollista",
|
||||
"components.TitleCard.tvshow": "TV-serier",
|
||||
"components.TitleCard.movie": "Film",
|
||||
"components.Slider.noresults": "Inga resultat",
|
||||
"components.Setup.welcome": "Välkommen till Overseerr",
|
||||
"components.Setup.tip": "Tips",
|
||||
"components.Setup.syncingbackground": "Synkronisering kommer ske i bakgrunden. Du kan fortsätta med konfigurationen under tiden.",
|
||||
"components.Setup.signinMessage": "Kom igång genom att logga in med ditt Plex-konto",
|
||||
"components.UserEdit.email": "E-post",
|
||||
"components.RequestModal.requestseasons": "Förfrågan {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
|
||||
"pages.somethingWentWrong": "{statusCode} - Något gick fel",
|
||||
"pages.serviceUnavailable": "{statusCode} - Tjänsten är otillgänglig",
|
||||
"pages.returnHome": "Gå tillbaka till startsidan",
|
||||
"pages.pageNotFound": "404 - Sidan kunde ej hittas",
|
||||
"pages.oops": "Ojsan",
|
||||
"pages.internalServerError": "{statusCode} - Internt serverfel",
|
||||
"i18n.unavailable": "Otillgänglig",
|
||||
"i18n.tvshows": "TV-serier",
|
||||
"i18n.processing": "Behandlar…",
|
||||
"i18n.pending": "Väntande",
|
||||
"i18n.partiallyavailable": "Delvis Tillgänglig",
|
||||
"i18n.movies": "Filmer",
|
||||
"i18n.deleting": "Raderar…",
|
||||
"i18n.delete": "Radera",
|
||||
"i18n.declined": "Avböjd",
|
||||
"i18n.decline": "Avböj",
|
||||
"i18n.cancel": "Avbryt",
|
||||
"i18n.available": "Tillgänglig",
|
||||
"i18n.approved": "Godkänd",
|
||||
"i18n.approve": "Godkänn",
|
||||
"components.UserList.usertype": "Användartyp",
|
||||
"components.UserList.username": "Användarnamn",
|
||||
"components.UserList.userlist": "Användarlista",
|
||||
"components.UserList.userdeleteerror": "Något gick fel när användaren skulle tas bort",
|
||||
"components.UserList.userdeleted": "Användare borttagen",
|
||||
"components.UserList.user": "Användare",
|
||||
"components.UserList.totalrequests": "Totalt antal förfrågningar",
|
||||
"components.UserList.role": "Roll",
|
||||
"components.UserList.plexuser": "Plexanvändare",
|
||||
"components.UserList.lastupdated": "Senast uppdaterad",
|
||||
"components.UserList.edit": "Redigera",
|
||||
"components.UserList.deleteuser": "Ta bort användare",
|
||||
"components.UserList.deleteconfirm": "Är du säker på att du vill ta bort användaren? All data för användaren kommer att raderas.",
|
||||
"components.UserList.delete": "Radera",
|
||||
"components.UserList.created": "Skapad",
|
||||
"components.UserList.admin": "Administratör",
|
||||
"components.UserEdit.voteDescription": "Ger behörighet att rösta på förfrågningar (röstning är inte implementerat ännu)",
|
||||
"components.UserEdit.vote": "Rösta",
|
||||
"components.UserEdit.usersaved": "Användare sparad",
|
||||
"components.UserEdit.usersDescription": "Ger behörighet att hantera användare i Overseerr. Användare med denna behörighet kan ge eller ändra användare med administratörstillgång.",
|
||||
"components.UserEdit.users": "Hantera användare",
|
||||
"components.UserEdit.username": "Användarnamn",
|
||||
"components.UserEdit.userfail": "Något gick fel när användaren skulle sparas.",
|
||||
"components.UserEdit.settingsDescription": "Ger behörighet att ändra alla inställningar i Overseerr. En användare måste ha denna behörighet för att kunna ge den till andra.",
|
||||
"components.UserEdit.settings": "Hantera Inställningar",
|
||||
"components.UserEdit.saving": "Sparar…",
|
||||
"components.UserEdit.save": "Spara",
|
||||
"components.UserEdit.requestDescription": "Ge behörighet att skapa förfrågningar för filmer och TV-serier.",
|
||||
"components.UserEdit.request": "Förfrågan",
|
||||
"components.UserEdit.permissions": "Behörigheter",
|
||||
"components.UserEdit.managerequestsDescription": "Ger behörighet att hantera förfrågningar i Overseerr. Detta gäller godkännande och nekande av förfrågningar.",
|
||||
"components.UserEdit.managerequests": "Hantera förfrågningar",
|
||||
"components.UserEdit.edituser": "Redigera användare",
|
||||
"components.UserEdit.avatar": "Avatar",
|
||||
"components.UserEdit.autoapproveDescription": "Automatiskt godkännande av samtliga förfrågningar skapade av denna användare.",
|
||||
"components.UserEdit.autoapprove": "Automatiskt Godkännande",
|
||||
"components.UserEdit.adminDescription": "Full administratörstillgång. Slipper alla behörighetskontroller.",
|
||||
"components.UserEdit.admin": "Administratör",
|
||||
"components.TvDetails.userrating": "Användarbetyg",
|
||||
"components.TvDetails.unavailable": "Otillgänglig",
|
||||
"components.TvDetails.status": "Status",
|
||||
"components.TvDetails.similarsubtext": "Andra TV-serier som liknar {title}",
|
||||
"components.TvDetails.similar": "Liknande TV-serier",
|
||||
"components.TvDetails.showtype": "Visa Typ",
|
||||
"components.TvDetails.requestmore": "Fler förfrågningar",
|
||||
"components.TvDetails.request": "Förfrågan",
|
||||
"components.TvDetails.recommendationssubtext": "Om du tycker om {title}, kanske du också tycker om…",
|
||||
"components.TvDetails.recommendations": "Rekommendationer",
|
||||
"components.TvDetails.pending": "Väntande",
|
||||
"components.TvDetails.overviewunavailable": "Beskrivning otillgänglig",
|
||||
"components.TvDetails.overview": "Beskrivning",
|
||||
"components.TvDetails.originallanguage": "Ursprungligt Språk",
|
||||
"components.TvDetails.network": "Nätverk",
|
||||
"components.TvDetails.manageModalTitle": "Hantera TV-serier",
|
||||
"components.TvDetails.manageModalRequests": "Förfrågningar",
|
||||
"components.TvDetails.manageModalNoRequests": "Inga förfrågningar",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "Detta kommer radera all media data inklusive förfrågningar som finns för objektet. Detta går inte att ångra. Om objektet finns i ditt Plexbibliotek kommer media informationen att återskapas vid nästa synkronisering.",
|
||||
"components.TvDetails.manageModalClearMedia": "Rensa All Media Data",
|
||||
"components.TvDetails.declinerequests": "Avböj {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||
"components.TvDetails.decline": "Avböj",
|
||||
"components.TvDetails.cast": "Roller",
|
||||
"components.TvDetails.cancelrequest": "Avbryt begäran",
|
||||
"components.TvDetails.available": "Tillgänglig"
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { IntlProvider } from 'react-intl';
|
||||
import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
|
||||
import Head from 'next/head';
|
||||
import Toast from '../components/Toast';
|
||||
import { InteractionProvider } from '../context/InteractionContext';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const loadLocaleData = (locale: string): Promise<any> => {
|
||||
@@ -28,6 +29,16 @@ const loadLocaleData = (locale: string): Promise<any> => {
|
||||
return import('../i18n/locale/ru.json');
|
||||
case 'nl':
|
||||
return import('../i18n/locale/nl.json');
|
||||
case 'es':
|
||||
return import('../i18n/locale/es.json');
|
||||
case 'it':
|
||||
return import('../i18n/locale/it.json');
|
||||
case 'pt-BR':
|
||||
return import('../i18n/locale/pt_BR.json');
|
||||
case 'sr':
|
||||
return import('../i18n/locale/sr.json');
|
||||
case 'sv':
|
||||
return import('../i18n/locale/sv.json');
|
||||
default:
|
||||
return import('../i18n/locale/en.json');
|
||||
}
|
||||
@@ -88,12 +99,14 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
defaultLocale="en"
|
||||
messages={loadedMessages}
|
||||
>
|
||||
<ToastProvider components={{ Toast }}>
|
||||
<Head>
|
||||
<title>Overseerr</title>
|
||||
</Head>
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
<InteractionProvider>
|
||||
<ToastProvider components={{ Toast }}>
|
||||
<Head>
|
||||
<title>Overseerr</title>
|
||||
</Head>
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
</InteractionProvider>
|
||||
</IntlProvider>
|
||||
</LanguageContext.Provider>
|
||||
</SWRConfig>
|
||||
@@ -109,7 +122,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
||||
if (ctx.res) {
|
||||
// Check if app is initialized and redirect if necessary
|
||||
const response = await axios.get<{ initialized: boolean }>(
|
||||
`http://localhost:${process.env.PORT || 3000}/api/v1/settings/public`
|
||||
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
|
||||
);
|
||||
|
||||
const initialized = response.data.initialized;
|
||||
@@ -125,7 +138,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
||||
try {
|
||||
// Attempt to get the user by running a request to the local api
|
||||
const response = await axios.get<User>(
|
||||
`http://localhost:${process.env.PORT || 3000}/api/v1/auth/me`,
|
||||
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
|
||||
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }
|
||||
);
|
||||
user = response.data;
|
||||
|
||||
9
src/pages/discover/holiday.tsx
Normal file
9
src/pages/discover/holiday.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { NextPage } from 'next';
|
||||
import Holiday from '../../components/Discover/Holiday';
|
||||
|
||||
const HolidayPage: NextPage = () => {
|
||||
return <Holiday />;
|
||||
};
|
||||
|
||||
export default HolidayPage;
|
||||
@@ -17,7 +17,7 @@ MoviePage.getInitialProps = async (ctx) => {
|
||||
if (ctx.req) {
|
||||
const cookies = parseCookies(ctx);
|
||||
const response = await axios.get<MovieDetailsType>(
|
||||
`http://localhost:${process.env.PORT || 3000}/api/v1/movie/${
|
||||
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
|
||||
ctx.query.movieId
|
||||
}${cookies.locale ? `?language=${cookies.locale}` : ''}`,
|
||||
{
|
||||
|
||||
17
src/pages/settings/about.tsx
Normal file
17
src/pages/settings/about.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
import SettingsAbout from '../../components/Settings/SettingsAbout';
|
||||
import SettingsLayout from '../../components/Settings/SettingsLayout';
|
||||
import useRouteGuard from '../../hooks/useRouteGuard';
|
||||
import { Permission } from '../../hooks/useUser';
|
||||
|
||||
const SettingsAboutPage: NextPage = () => {
|
||||
useRouteGuard(Permission.MANAGE_SETTINGS);
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<SettingsAbout />
|
||||
</SettingsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsAboutPage;
|
||||
14
src/pages/settings/logs.tsx
Normal file
14
src/pages/settings/logs.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
import SettingsLayout from '../../components/Settings/SettingsLayout';
|
||||
import SettingsLogs from '../../components/Settings/SettingsLogs';
|
||||
|
||||
const SettingsLogsPage: NextPage = () => {
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<SettingsLogs />
|
||||
</SettingsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsLogsPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user