Compare commits
36 Commits
refactor-w
...
preview-dn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce5925426 | ||
|
|
38ad875dd7 | ||
|
|
a9741fa36d | ||
|
|
b5a069901a | ||
|
|
9aeb3604e6 | ||
|
|
6eb88f8674 | ||
|
|
46ee8a4ca1 | ||
|
|
f52939e4cd | ||
|
|
d31a2c37e6 | ||
|
|
20863d4a8d | ||
|
|
4757f1c3e5 | ||
|
|
1f1ad72e9e | ||
|
|
c3ddc860b6 | ||
|
|
2bd125d9a5 | ||
|
|
7a5e8d69bf | ||
|
|
650c339d74 | ||
|
|
4ef5a3c7c5 | ||
|
|
a791b53953 | ||
|
|
68467ced9d | ||
|
|
296aee6338 | ||
|
|
0a4b38e50d | ||
|
|
bcc84d8551 | ||
|
|
783fda9621 | ||
|
|
d765055da8 | ||
|
|
fed66f0702 | ||
|
|
461202da75 | ||
|
|
0bbcfdc4f9 | ||
|
|
f486fb5e75 | ||
|
|
10082292e8 | ||
|
|
c0a0b9c8a8 | ||
|
|
d9d07c705a | ||
|
|
0eea1090df | ||
|
|
cd0fa3e223 | ||
|
|
9c68616343 | ||
|
|
0c2713213c | ||
|
|
3856061fe1 |
@@ -322,6 +322,60 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joshuaboniface",
|
||||
"name": "Joshua M. Boniface",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4031396?v=4",
|
||||
"profile": "https://www.boniface.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gauthier-th",
|
||||
"name": "Gauthier",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
||||
"profile": "https://gauthierth.fr/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Kara-Zor-El",
|
||||
"name": "Kara",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/69772087?v=4",
|
||||
"profile": "https://github.com/Kara-Zor-El",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JoaquinOlivero",
|
||||
"name": "Joaquin Olivero",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/66050823?v=4",
|
||||
"profile": "https://joaquinolivero.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Bretterteig",
|
||||
"name": "Julian Behr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/47298401?v=4",
|
||||
"profile": "https://github.com/Bretterteig",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ThowZzy",
|
||||
"name": "ThowZzy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/61882536?v=4",
|
||||
"profile": "https://github.com/ThowZzy",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
github: [Fallenbagel]
|
||||
buy_me_a_coffee: fallen.bagel
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
container: node:18.18-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
env:
|
||||
HUSKY: 0
|
||||
@@ -34,18 +34,18 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
|
||||
26
.github/workflows/conflict_labeler.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Merge Conflict Labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
branches:
|
||||
- develop
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'Fallenbagel/jellyseerr' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
4
.github/workflows/cypress.yml
vendored
@@ -13,9 +13,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v4
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: yarn cypress:build
|
||||
start: yarn start
|
||||
|
||||
10
.github/workflows/preview.yml
vendored
@@ -11,21 +11,21 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
120
.github/workflows/release.yml
vendored
@@ -10,19 +10,19 @@ jobs:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
@@ -35,60 +35,60 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: npx semantic-release
|
||||
|
||||
build-snap:
|
||||
name: Build Snap Package (${{ matrix.architecture }})
|
||||
needs: semantic-release
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture:
|
||||
- amd64
|
||||
- arm64
|
||||
- armhf
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Switch to main branch
|
||||
run: git checkout main
|
||||
- name: Pull latest changes
|
||||
run: git pull
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
git fetch --prune --tags
|
||||
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo "RELEASE=stable" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "RELEASE=edge" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||
- name: Build Snap Package
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
id: build
|
||||
with:
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
- name: Publish Snap Package
|
||||
uses: snapcore/action-publish@v1
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }}
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||
# build-snap:
|
||||
# name: Build Snap Package (${{ matrix.architecture }})
|
||||
# needs: semantic-release
|
||||
# runs-on: ubuntu-22.04
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# architecture:
|
||||
# - amd64
|
||||
# - arm64
|
||||
# - armhf
|
||||
# steps:
|
||||
# - name: Checkout Code
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: Switch to main branch
|
||||
# run: git checkout main
|
||||
# - name: Pull latest changes
|
||||
# run: git pull
|
||||
# - name: Prepare
|
||||
# id: prepare
|
||||
# run: |
|
||||
# git fetch --prune --tags
|
||||
# if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
# echo "RELEASE=stable" >> $GITHUB_OUTPUT
|
||||
# else
|
||||
# echo "RELEASE=edge" >> $GITHUB_OUTPUT
|
||||
# fi
|
||||
# - name: Set Up QEMU
|
||||
# uses: docker/setup-qemu-action@v3
|
||||
# with:
|
||||
# image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||
# - name: Build Snap Package
|
||||
# uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
# id: build
|
||||
# with:
|
||||
# architecture: ${{ matrix.architecture }}
|
||||
# - name: Upload Snap Package
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||
# path: ${{ steps.build.outputs.snap }}
|
||||
# - name: Review Snap Package
|
||||
# uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
# with:
|
||||
# snap: ${{ steps.build.outputs.snap }}
|
||||
# - name: Publish Snap Package
|
||||
# uses: snapcore/action-publish@v1
|
||||
# env:
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }}
|
||||
# with:
|
||||
# snap: ${{ steps.build.outputs.snap }}
|
||||
# release: ${{ steps.prepare.outputs.RELEASE }}
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
|
||||
20
.github/workflows/snap.yaml
vendored
@@ -1,9 +1,13 @@
|
||||
name: Publish Snap
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
# turn off edge snap builds temporarily and make it manual
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches:
|
||||
# - develop
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
jobs:
|
||||
@@ -12,7 +16,7 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.10.0
|
||||
uses: styfle/cancel-workflow-action@0.12.1
|
||||
with:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -29,7 +33,7 @@ jobs:
|
||||
- armhf
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
@@ -40,7 +44,7 @@ jobs:
|
||||
echo "RELEASE=edge" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Configure Git
|
||||
run: git config --add safe.directory /data/parts/jellyseerr/src
|
||||
- name: Build Snap Package
|
||||
@@ -49,7 +53,7 @@ jobs:
|
||||
with:
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
|
||||
4
.github/workflows/support.yml
vendored
@@ -6,9 +6,9 @@ on:
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
- uses: dessant/support-requests@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'support'
|
||||
|
||||
10
README.md
@@ -11,7 +11,7 @@
|
||||
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-34-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-40-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
||||
@@ -229,6 +229,14 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"region": "",
|
||||
"originalLanguage": "",
|
||||
"trustProxy": false,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"locale": "en"
|
||||
},
|
||||
@@ -37,6 +38,17 @@
|
||||
],
|
||||
"machineId": "test"
|
||||
},
|
||||
"jellyfin": {
|
||||
"name": "",
|
||||
"ip": "",
|
||||
"port": 8096,
|
||||
"useSsl": false,
|
||||
"urlBase": "",
|
||||
"externalHostname": "",
|
||||
"jellyfinForgotPasswordUrl": "",
|
||||
"libraries": [],
|
||||
"serverId": ""
|
||||
},
|
||||
"tautulli": {},
|
||||
"radarr": [],
|
||||
"sonarr": [],
|
||||
@@ -139,11 +151,26 @@
|
||||
"sonarr-scan": {
|
||||
"schedule": "0 30 4 * * *"
|
||||
},
|
||||
"plex-watchlist-sync": {
|
||||
"schedule": "0 */10 * * * *"
|
||||
},
|
||||
"availability-sync": {
|
||||
"schedule": "0 0 5 * * *"
|
||||
},
|
||||
"download-sync": {
|
||||
"schedule": "0 * * * * *"
|
||||
},
|
||||
"download-sync-reset": {
|
||||
"schedule": "0 0 1 * * *"
|
||||
},
|
||||
"jellyfin-recently-added-scan": {
|
||||
"schedule": "0 */5 * * * *"
|
||||
},
|
||||
"jellyfin-full-scan": {
|
||||
"schedule": "0 0 3 * * *"
|
||||
},
|
||||
"image-cache-cleanup": {
|
||||
"schedule": "0 0 5 * * *"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2092,6 +2092,13 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
userId:
|
||||
type: integer
|
||||
/settings/jellyfin/sync:
|
||||
get:
|
||||
summary: Get status of full Jellyfin library sync
|
||||
@@ -3395,6 +3402,12 @@ paths:
|
||||
Updates a single slider and return the newly updated slider. Requires the `ADMIN` permission.
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: path
|
||||
name: sliderId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -3724,7 +3737,7 @@ paths:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: '#/components/schemas/User'
|
||||
post:
|
||||
summary: Create new user
|
||||
description: |
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"axios-rate-limit": "1.3.0",
|
||||
"bcrypt": "5.1.0",
|
||||
"bowser": "2.11.0",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"connect-typeorm": "1.1.4",
|
||||
"cookie-parser": "1.4.6",
|
||||
"copy-to-clipboard": "3.3.3",
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 38 KiB |
BIN
public/apple-splash-1179-2556.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 43 KiB |
BIN
public/apple-splash-1290-2796.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
BIN
public/apple-splash-1488-2266.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 47 KiB |
BIN
public/apple-splash-1640-2360.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 47 KiB |
BIN
public/apple-splash-2266-1488.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/apple-splash-2360-1640.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 33 KiB |
BIN
public/apple-splash-2556-1179.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 37 KiB |
BIN
public/apple-splash-2796-1290.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 821 B |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 137 KiB |
@@ -1,14 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import logger from '@server/logger';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
|
||||
export interface JellyfinUserResponse {
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
ServerName: string;
|
||||
Id: string;
|
||||
Configuration: {
|
||||
GroupedFolders: string[];
|
||||
};
|
||||
Policy: {
|
||||
IsAdministrator: boolean;
|
||||
};
|
||||
@@ -24,6 +29,13 @@ export interface JellyfinUserListResponse {
|
||||
users: JellyfinUserResponse[];
|
||||
}
|
||||
|
||||
interface JellyfinMediaFolder {
|
||||
Name: string;
|
||||
Id: string;
|
||||
Type: string;
|
||||
CollectionType: string;
|
||||
}
|
||||
|
||||
export interface JellyfinLibrary {
|
||||
type: 'show' | 'movie';
|
||||
key: string;
|
||||
@@ -80,48 +92,90 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
class JellyfinAPI {
|
||||
class JellyfinAPI extends ExternalAPI {
|
||||
private authToken?: string;
|
||||
private userId?: string;
|
||||
private jellyfinHost: string;
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||
this.jellyfinHost = jellyfinHost;
|
||||
this.authToken = authToken;
|
||||
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
let authHeaderVal: string;
|
||||
if (authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
||||
} else {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
|
||||
}
|
||||
|
||||
this.axios = axios.create({
|
||||
baseURL: this.jellyfinHost,
|
||||
headers: {
|
||||
'X-Emby-Authorization': authHeaderVal,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
super(
|
||||
jellyfinHost,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'X-Emby-Authorization': authHeaderVal,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.jellyfinHost = jellyfinHost;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
public async login(
|
||||
Username?: string,
|
||||
Password?: string
|
||||
Password?: string,
|
||||
ClientIP?: string
|
||||
): Promise<JellyfinLoginResponse> {
|
||||
try {
|
||||
const account = await this.axios.post<JellyfinLoginResponse>(
|
||||
const authenticate = async (useHeaders: boolean) => {
|
||||
const headers =
|
||||
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
||||
|
||||
return this.post<JellyfinLoginResponse>(
|
||||
'/Users/AuthenticateByName',
|
||||
{
|
||||
Username: Username,
|
||||
Username,
|
||||
Pw: Password,
|
||||
}
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
return account.data;
|
||||
};
|
||||
|
||||
try {
|
||||
return await authenticate(true);
|
||||
} catch (e) {
|
||||
throw new Error('Unauthorized');
|
||||
logger.debug(`Failed to authenticate with headers: ${e.message}`, {
|
||||
label: 'Jellyfin API',
|
||||
ip: ClientIP,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await authenticate(false);
|
||||
} catch (e) {
|
||||
const status = e.response?.status;
|
||||
|
||||
const networkErrorCodes = new Set([
|
||||
'ECONNREFUSED',
|
||||
'EHOSTUNREACH',
|
||||
'ENOTFOUND',
|
||||
'ETIMEDOUT',
|
||||
'ECONNRESET',
|
||||
'EADDRINUSE',
|
||||
'ENETDOWN',
|
||||
'ENETUNREACH',
|
||||
'EPIPE',
|
||||
'ECONNABORTED',
|
||||
'EPROTO',
|
||||
'EHOSTDOWN',
|
||||
'EAI_AGAIN',
|
||||
'ERR_INVALID_URL',
|
||||
]);
|
||||
|
||||
if (networkErrorCodes.has(e.code) || status === 404) {
|
||||
throw new ApiError(status, ApiErrorCode.InvalidUrl);
|
||||
}
|
||||
|
||||
throw new ApiError(status, ApiErrorCode.InvalidCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,69 +184,106 @@ class JellyfinAPI {
|
||||
return;
|
||||
}
|
||||
|
||||
public async getSystemInfo(): Promise<any> {
|
||||
try {
|
||||
const systemInfoResponse = await this.get<any>('/System/Info');
|
||||
|
||||
return systemInfoResponse;
|
||||
} catch (e) {
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getServerName(): Promise<string> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
"/System/Info/Public'}"
|
||||
const serverResponse = await this.get<JellyfinUserResponse>(
|
||||
'/System/Info/Public'
|
||||
);
|
||||
return account.data.ServerName;
|
||||
|
||||
return serverResponse.ServerName;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the server name from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('girl idk');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers(): Promise<JellyfinUserListResponse> {
|
||||
try {
|
||||
const account = await this.axios.get(`/Users`);
|
||||
return { users: account.data };
|
||||
const userReponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
||||
|
||||
return { users: userReponse };
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUser(): Promise<JellyfinUserResponse> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
const userReponse = await this.get<JellyfinUserResponse>(
|
||||
`/Users/${this.userId ?? 'Me'}`
|
||||
);
|
||||
return account.data;
|
||||
return userReponse;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
||||
try {
|
||||
// TODO: Try to fix automatic grouping without fucking up LDAP users
|
||||
// const libraries = await this.axios.get<any>('/Library/VirtualFolders');
|
||||
const mediaFolderResponse = await this.get<any>(`/Library/MediaFolders`);
|
||||
|
||||
const account = await this.axios.get<any>(
|
||||
`/Users/${this.userId ?? 'Me'}/Views`
|
||||
);
|
||||
return this.mapLibraries(mediaFolderResponse.Items);
|
||||
} catch (mediaFoldersResponseError) {
|
||||
// fallback to user views to get libraries
|
||||
// this only and maybe/depending on factors affects LDAP users
|
||||
try {
|
||||
const mediaFolderResponse = await this.get<any>(
|
||||
`/Users/${this.userId ?? 'Me'}/Views`
|
||||
);
|
||||
|
||||
const response: JellyfinLibrary[] = account.data.Items.filter(
|
||||
(Item: any) => {
|
||||
return (
|
||||
Item.Type === 'CollectionFolder' &&
|
||||
Item.CollectionType !== 'music' &&
|
||||
Item.CollectionType !== 'books' &&
|
||||
Item.CollectionType !== 'musicvideos' &&
|
||||
Item.CollectionType !== 'homevideos'
|
||||
);
|
||||
}
|
||||
).map((Item: any) => {
|
||||
return this.mapLibraries(mediaFolderResponse.Items);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private mapLibraries(mediaFolders: JellyfinMediaFolder[]): JellyfinLibrary[] {
|
||||
const excludedTypes = [
|
||||
'music',
|
||||
'books',
|
||||
'musicvideos',
|
||||
'homevideos',
|
||||
'boxsets',
|
||||
];
|
||||
|
||||
return mediaFolders
|
||||
.filter((Item: JellyfinMediaFolder) => {
|
||||
return (
|
||||
Item.Type === 'CollectionFolder' &&
|
||||
!excludedTypes.includes(Item.CollectionType)
|
||||
);
|
||||
})
|
||||
.map((Item: JellyfinMediaFolder) => {
|
||||
return <JellyfinLibrary>{
|
||||
key: Item.Id,
|
||||
title: Item.Name,
|
||||
@@ -200,24 +291,15 @@ class JellyfinAPI {
|
||||
agent: 'jellyfin',
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||
const libraryItemsResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false`
|
||||
);
|
||||
|
||||
return contents.data.Items.filter(
|
||||
return libraryItemsResponse.Items.filter(
|
||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -225,23 +307,25 @@ class JellyfinAPI {
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
return itemResponse;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,36 +333,38 @@ class JellyfinAPI {
|
||||
id: string
|
||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
return itemResponse;
|
||||
} catch (e) {
|
||||
if (availabilitySync.running) {
|
||||
if (e.response && e.response.status === 500) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
|
||||
const seasonResponse = await this.get<any>(`/Shows/${seriesID}/Seasons`);
|
||||
|
||||
return contents.data.Items;
|
||||
return seasonResponse.Items;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,11 +373,11 @@ class JellyfinAPI {
|
||||
seasonID: string
|
||||
): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
const episodeResponse = await this.get<any>(
|
||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||
);
|
||||
|
||||
return contents.data.Items.filter(
|
||||
return episodeResponse.Items.filter(
|
||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -299,7 +385,8 @@ class JellyfinAPI {
|
||||
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
server/constants/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum ApiErrorCode {
|
||||
InvalidUrl = 'INVALID_URL',
|
||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||
NotAdmin = 'NOT_ADMIN',
|
||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||
Unknown = 'UNKNOWN',
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import {
|
||||
AfterLoad,
|
||||
Column,
|
||||
@@ -211,15 +212,12 @@ class Media {
|
||||
} else {
|
||||
const pageName =
|
||||
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
||||
const { serverId, hostname, externalHostname } = getSettings().jellyfin;
|
||||
let jellyfinHost =
|
||||
const { serverId, externalHostname } = getSettings().jellyfin;
|
||||
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
: getHostname();
|
||||
|
||||
if (this.jellyfinMediaId) {
|
||||
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
||||
|
||||
@@ -23,19 +23,25 @@ import imageproxy from '@server/routes/imageproxy';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import type CacheableLookupType from 'cacheable-lookup';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import csurf from 'csurf';
|
||||
import { lookup } from 'dns';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
import type { Store } from 'express-session';
|
||||
import session from 'express-session';
|
||||
import next from 'next';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import YAML from 'yamljs';
|
||||
|
||||
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
@@ -46,6 +52,25 @@ const handle = app.getRequestHandler();
|
||||
app
|
||||
.prepare()
|
||||
.then(async () => {
|
||||
const CacheableLookup = (await _importDynamic('cacheable-lookup'))
|
||||
.default as typeof CacheableLookupType;
|
||||
const cacheable = new CacheableLookup();
|
||||
|
||||
const originalLookup = cacheable.lookup;
|
||||
|
||||
// if hostname is localhost use dns.lookup instead of cacheable-lookup
|
||||
cacheable.lookup = (...args: any) => {
|
||||
const [hostname] = args;
|
||||
if (hostname === 'localhost') {
|
||||
lookup(...(args as Parameters<typeof lookup>));
|
||||
} else {
|
||||
originalLookup(...(args as Parameters<typeof originalLookup>));
|
||||
}
|
||||
};
|
||||
|
||||
cacheable.install(http.globalAgent);
|
||||
cacheable.install(https.globalAgent);
|
||||
|
||||
const dbConnection = await dataSource.initialize();
|
||||
|
||||
// Run migrations in production
|
||||
@@ -59,6 +84,16 @@ app
|
||||
const settings = getSettings().load();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Overwrite DNS servers
|
||||
if (settings.main.overwriteDnsServers) {
|
||||
cacheable.servers = settings.main.overwriteDnsServers
|
||||
.split(',')
|
||||
.map((server) => server.trim());
|
||||
logger.info('Using custom DNS servers', {
|
||||
label: 'Settings',
|
||||
});
|
||||
}
|
||||
|
||||
// Migrate library types
|
||||
if (
|
||||
settings.plex.libraries.length > 1 &&
|
||||
|
||||
@@ -16,6 +16,7 @@ import { User } from '@server/entity/User';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
|
||||
class AvailabilitySync {
|
||||
public running = false;
|
||||
@@ -84,7 +85,7 @@ class AvailabilitySync {
|
||||
) {
|
||||
if (admin) {
|
||||
this.jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
@@ -141,7 +141,7 @@ class WebhookAgent
|
||||
const payloadString = Buffer.from(
|
||||
this.getSettings().options.jsonPayload,
|
||||
'base64'
|
||||
).toString('ascii');
|
||||
).toString('utf8');
|
||||
|
||||
const parsedJSON = JSON.parse(JSON.parse(payloadString));
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Library } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import AsyncLock from '@server/utils/asyncLock';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { randomUUID as uuid } from 'crypto';
|
||||
import { uniqWith } from 'lodash';
|
||||
|
||||
@@ -83,13 +84,17 @@ class JellyfinScanner {
|
||||
}
|
||||
|
||||
const has4k = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
return MediaSource.MediaStreams.filter(
|
||||
(MediaStream) => MediaStream.Type === 'Video'
|
||||
).some((MediaStream) => {
|
||||
return (MediaStream.Width ?? 0) > 2000;
|
||||
});
|
||||
});
|
||||
|
||||
const hasOtherResolution = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
return MediaSource.MediaStreams.filter(
|
||||
(MediaStream) => MediaStream.Type === 'Video'
|
||||
).some((MediaStream) => {
|
||||
return (MediaStream.Width ?? 0) <= 2000;
|
||||
});
|
||||
});
|
||||
@@ -590,8 +595,10 @@ class JellyfinScanner {
|
||||
return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
|
||||
}
|
||||
|
||||
const hostname = getHostname();
|
||||
|
||||
this.jfClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
hostname,
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { runMigrations } from '@server/lib/settings/migrator';
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import webpush from 'web-push';
|
||||
import { Permission } from './permissions';
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
@@ -38,7 +39,10 @@ export interface PlexSettings {
|
||||
|
||||
export interface JellyfinSettings {
|
||||
name: string;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
useSsl?: boolean;
|
||||
urlBase?: string;
|
||||
externalHostname?: string;
|
||||
jellyfinForgotPasswordUrl?: string;
|
||||
libraries: Library[];
|
||||
@@ -114,6 +118,7 @@ export interface MainSettings {
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
locale: string;
|
||||
overwriteDnsServers?: string;
|
||||
}
|
||||
|
||||
interface PublicSettings {
|
||||
@@ -130,7 +135,6 @@ interface FullPublicSettings extends PublicSettings {
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
jellyfinHost?: string;
|
||||
jellyfinExternalHost?: string;
|
||||
jellyfinForgotPasswordUrl?: string;
|
||||
jellyfinServerName?: string;
|
||||
@@ -274,7 +278,7 @@ export type JobId =
|
||||
| 'image-cache-cleanup'
|
||||
| 'availability-sync';
|
||||
|
||||
interface AllSettings {
|
||||
export interface AllSettings {
|
||||
clientId: string;
|
||||
vapidPublic: string;
|
||||
vapidPrivate: string;
|
||||
@@ -291,7 +295,7 @@ interface AllSettings {
|
||||
|
||||
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/settings.json`
|
||||
: path.join(__dirname, '../../config/settings.json');
|
||||
: path.join(__dirname, '../../../config/settings.json');
|
||||
|
||||
class Settings {
|
||||
private data: AllSettings;
|
||||
@@ -331,7 +335,10 @@ class Settings {
|
||||
},
|
||||
jellyfin: {
|
||||
name: '',
|
||||
hostname: '',
|
||||
ip: '',
|
||||
port: 8096,
|
||||
useSsl: false,
|
||||
urlBase: '',
|
||||
externalHostname: '',
|
||||
jellyfinForgotPasswordUrl: '',
|
||||
libraries: [],
|
||||
@@ -547,8 +554,6 @@ class Settings {
|
||||
region: this.data.main.region,
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
mediaServerType: this.main.mediaServerType,
|
||||
jellyfinHost: this.jellyfin.hostname,
|
||||
jellyfinExternalHost: this.jellyfin.externalHostname,
|
||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||
cacheImages: this.data.main.cacheImages,
|
||||
vapidPublic: this.vapidPublic,
|
||||
@@ -637,7 +642,11 @@ class Settings {
|
||||
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
|
||||
if (data) {
|
||||
this.data = merge(this.data, JSON.parse(data));
|
||||
const parsedJson = JSON.parse(data);
|
||||
this.data = runMigrations(parsedJson);
|
||||
|
||||
this.data = merge(this.data, parsedJson);
|
||||
|
||||
this.save();
|
||||
}
|
||||
return this;
|
||||
30
server/lib/settings/migrations/0001_migrate_hostname.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateHostname = (settings: any): AllSettings => {
|
||||
const oldJellyfinSettings = settings.jellyfin;
|
||||
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
||||
const { hostname } = oldJellyfinSettings;
|
||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||
|
||||
delete oldJellyfinSettings.hostname;
|
||||
if (urlMatch) {
|
||||
const [, ip, , port, urlBase] = urlMatch;
|
||||
settings.jellyfin = {
|
||||
...settings.jellyfin,
|
||||
ip,
|
||||
port: port || (useSsl ? 443 : 80),
|
||||
useSsl,
|
||||
urlBase: urlBase ? urlBase.replace(/\/$/, '') : '',
|
||||
};
|
||||
}
|
||||
}
|
||||
if (settings.jellyfin && settings.jellyfin.hostname) {
|
||||
delete settings.jellyfin.hostname;
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
export default migrateHostname;
|
||||
21
server/lib/settings/migrator.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const migrationsDir = path.join(__dirname, 'migrations');
|
||||
|
||||
export const runMigrations = (settings: AllSettings): AllSettings => {
|
||||
const migrations = fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
.map((file) => require(path.join(migrationsDir, file)).default);
|
||||
|
||||
let migrated = settings;
|
||||
|
||||
for (const migration of migrations) {
|
||||
migrated = migration(migrated);
|
||||
}
|
||||
|
||||
return migrated;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
@@ -9,9 +10,12 @@ import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import net from 'net';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -219,30 +223,39 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
urlBase?: string;
|
||||
useSsl?: boolean;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
|
||||
if (
|
||||
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
|
||||
settings.jellyfin.hostname !== ''
|
||||
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
return res.status(500).json({ error: 'Jellyfin login is disabled' });
|
||||
} else if (!body.username) {
|
||||
return res.status(500).json({ error: 'You must provide an username' });
|
||||
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
|
||||
} else if (settings.jellyfin.ip !== '' && body.hostname) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Jellyfin hostname already configured' });
|
||||
} else if (settings.jellyfin.hostname === '' && !body.hostname) {
|
||||
} else if (settings.jellyfin.ip === '' && !body.hostname) {
|
||||
return res.status(500).json({ error: 'No hostname provided.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hostname =
|
||||
settings.jellyfin.hostname !== ''
|
||||
? settings.jellyfin.hostname
|
||||
: body.hostname ?? '';
|
||||
settings.jellyfin.ip !== ''
|
||||
? getHostname()
|
||||
: getHostname({
|
||||
useSsl: body.useSsl,
|
||||
ip: body.hostname,
|
||||
port: body.port,
|
||||
urlBase: body.urlBase,
|
||||
});
|
||||
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
|
||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||
@@ -258,18 +271,31 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
'base64'
|
||||
);
|
||||
}
|
||||
|
||||
// First we need to attempt to log the user in to jellyfin
|
||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||
let jellyfinHost =
|
||||
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
const ip = req.ip;
|
||||
let clientIp;
|
||||
|
||||
if (ip) {
|
||||
if (net.isIPv4(ip)) {
|
||||
clientIp = ip;
|
||||
} else if (net.isIPv6(ip)) {
|
||||
clientIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
}
|
||||
}
|
||||
|
||||
const account = await jellyfinserver.login(
|
||||
body.username,
|
||||
body.password,
|
||||
clientIp
|
||||
);
|
||||
|
||||
const account = await jellyfinserver.login(body.username, body.password);
|
||||
// Next let's see if the user already exists
|
||||
user = await userRepository.findOne({
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
@@ -278,7 +304,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
if (!user && !(await userRepository.count())) {
|
||||
// Check if user is admin on jellyfin
|
||||
if (account.User.Policy.IsAdministrator === false) {
|
||||
throw new Error('not_admin');
|
||||
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -306,15 +332,21 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
settings.jellyfin.hostname = body.hostname ?? '';
|
||||
const serverName = await jellyfinserver.getServerName();
|
||||
|
||||
settings.jellyfin.name = serverName;
|
||||
settings.jellyfin.serverId = account.User.ServerId;
|
||||
settings.jellyfin.ip = body.hostname ?? '';
|
||||
settings.jellyfin.port = body.port ?? 8096;
|
||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||
settings.save();
|
||||
startJobs();
|
||||
|
||||
await userRepository.save(user);
|
||||
}
|
||||
// User already exists, let's update their information
|
||||
else if (body.username === user?.jellyfinUsername) {
|
||||
else if (account.User.Id === user?.jellyfinUserId) {
|
||||
logger.info(
|
||||
`Found matching ${
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
@@ -412,43 +444,68 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
|
||||
return res.status(200).json(user?.filter() ?? {});
|
||||
} catch (e) {
|
||||
if (e.message === 'Unauthorized') {
|
||||
logger.warn(
|
||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
password: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
} else if (e.message === 'not_admin') {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'CREDENTIAL_ERROR_NOT_ADMIN',
|
||||
});
|
||||
} else if (e.message === 'add_email') {
|
||||
return next({
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
|
||||
});
|
||||
} else if (e.message === 'select_server_type') {
|
||||
return next({
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
|
||||
});
|
||||
} else {
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
switch (e.errorCode) {
|
||||
case ApiErrorCode.InvalidUrl:
|
||||
logger.error(
|
||||
`The provided ${
|
||||
process.env.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin'
|
||||
} is invalid or the server is not reachable.`,
|
||||
{
|
||||
label: 'Auth',
|
||||
error: e.errorCode,
|
||||
status: e.statusCode,
|
||||
hostname: getHostname({
|
||||
useSsl: body.useSsl,
|
||||
ip: body.hostname,
|
||||
port: body.port,
|
||||
urlBase: body.urlBase,
|
||||
}),
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
case ApiErrorCode.InvalidCredentials:
|
||||
logger.warn(
|
||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
password: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
case ApiErrorCode.NotAdmin:
|
||||
logger.warn(
|
||||
'Failed login attempt from user without admin permissions',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
default:
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||
@@ -24,8 +25,10 @@ import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import discoverSettingRoutes from '@server/routes/settings/discover';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { appDataPath } from '@server/utils/appDataVolume';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
@@ -252,16 +255,64 @@ settingsRoutes.get('/jellyfin', (_req, res) => {
|
||||
res.status(200).json(settings.jellyfin);
|
||||
});
|
||||
|
||||
settingsRoutes.post('/jellyfin', (req, res) => {
|
||||
settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const settings = getSettings();
|
||||
|
||||
settings.jellyfin = merge(settings.jellyfin, req.body);
|
||||
settings.save();
|
||||
try {
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
where: { id: 1 },
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
const tempJellyfinSettings = { ...settings.jellyfin, ...req.body };
|
||||
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
getHostname(tempJellyfinSettings),
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
const result = await jellyfinClient.getSystemInfo();
|
||||
|
||||
if (!result?.Id) {
|
||||
throw new ApiError(result?.status, ApiErrorCode.InvalidUrl);
|
||||
}
|
||||
|
||||
Object.assign(settings.jellyfin, req.body);
|
||||
settings.jellyfin.serverId = result.Id;
|
||||
settings.jellyfin.name = result.ServerName;
|
||||
settings.save();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
logger.error('Something went wrong testing Jellyfin connection', {
|
||||
label: 'API',
|
||||
status: e.statusCode,
|
||||
errorMessage: ApiErrorCode.InvalidUrl,
|
||||
});
|
||||
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: ApiErrorCode.InvalidUrl,
|
||||
});
|
||||
} else {
|
||||
logger.error('Something went wrong', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
|
||||
return next({
|
||||
status: e.statusCode ?? 500,
|
||||
message: ApiErrorCode.Unknown,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json(settings.jellyfin);
|
||||
});
|
||||
|
||||
settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
if (req.query.sync) {
|
||||
@@ -272,7 +323,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
@@ -281,6 +332,22 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
|
||||
const libraries = await jellyfinClient.getLibraries();
|
||||
|
||||
if (libraries.length === 0) {
|
||||
// Check if no libraries are found due to the fallback to user views
|
||||
// This only affects LDAP users
|
||||
const account = await jellyfinClient.getUser();
|
||||
|
||||
// Automatic Library grouping is not supported when user views are used to get library
|
||||
if (account.Configuration.GroupedFolders.length > 0) {
|
||||
return next({
|
||||
status: 501,
|
||||
message: ApiErrorCode.SyncErrorGroupedFolders,
|
||||
});
|
||||
}
|
||||
|
||||
return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries });
|
||||
}
|
||||
|
||||
const newLibraries: Library[] = libraries.map((library) => {
|
||||
const existing = settings.jellyfin.libraries.find(
|
||||
(l) => l.id === library.key && l.name === library.title
|
||||
@@ -309,16 +376,12 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
});
|
||||
|
||||
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||
let jellyfinHost =
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
: getHostname();
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
||||
@@ -326,7 +389,6 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
@@ -275,7 +275,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
||||
...webhookSettings.options,
|
||||
jsonPayload: JSON.parse(
|
||||
Buffer.from(webhookSettings.options.jsonPayload, 'base64').toString(
|
||||
'ascii'
|
||||
'utf8'
|
||||
)
|
||||
),
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { findIndex, sortBy } from 'lodash';
|
||||
@@ -496,7 +497,6 @@ router.post(
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
@@ -504,15 +504,14 @@ router.post(
|
||||
|
||||
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
||||
const createdUsers: User[] = [];
|
||||
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||
let jellyfinHost =
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
const hostname = getHostname();
|
||||
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
const jellyfinUsers = await jellyfinClient.getUsers();
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ userSettingsRoutes.post<
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
user.email = req.body.email ?? user.email;
|
||||
|
||||
// Update quota values only if the user has the correct permissions
|
||||
if (
|
||||
@@ -127,20 +128,19 @@ userSettingsRoutes.post<
|
||||
user.settings.originalLanguage = req.body.originalLanguage;
|
||||
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
|
||||
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
|
||||
user.email = req.body.email ?? user.email;
|
||||
}
|
||||
|
||||
await userRepository.save(user);
|
||||
const savedUser = await userRepository.save(user);
|
||||
|
||||
return res.status(200).json({
|
||||
username: user.username,
|
||||
discordId: user.settings.discordId,
|
||||
locale: user.settings.locale,
|
||||
region: user.settings.region,
|
||||
originalLanguage: user.settings.originalLanguage,
|
||||
watchlistSyncMovies: user.settings.watchlistSyncMovies,
|
||||
watchlistSyncTv: user.settings.watchlistSyncTv,
|
||||
email: user.email,
|
||||
username: savedUser.username,
|
||||
discordId: savedUser.settings?.discordId,
|
||||
locale: savedUser.settings?.locale,
|
||||
region: savedUser.settings?.region,
|
||||
originalLanguage: savedUser.settings?.originalLanguage,
|
||||
watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies,
|
||||
watchlistSyncTv: savedUser.settings?.watchlistSyncTv,
|
||||
email: savedUser.email,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
|
||||
9
server/types/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { ApiErrorCode } from '@server/constants/error';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public statusCode: number, public errorCode: ApiErrorCode) {
|
||||
super();
|
||||
|
||||
this.name = 'apiError';
|
||||
}
|
||||
}
|
||||
18
server/utils/getHostname.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
|
||||
interface HostnameParams {
|
||||
useSsl?: boolean;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
export const getHostname = (params?: HostnameParams): string => {
|
||||
const settings = params ? params : getSettings().jellyfin;
|
||||
|
||||
const { useSsl, ip, port, urlBase } = settings;
|
||||
|
||||
const hostname = `${useSsl ? 'https' : 'http'}://${ip}:${port}${urlBase}`;
|
||||
|
||||
return hostname;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import getConfig from 'next/config';
|
||||
@@ -13,7 +14,10 @@ import * as Yup from 'yup';
|
||||
const messages = defineMessages({
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
host: '{mediaServerName} URL',
|
||||
hostname: '{mediaServerName} URL',
|
||||
port: 'Port',
|
||||
enablessl: 'Use SSL',
|
||||
urlBase: 'URL Base',
|
||||
email: 'Email',
|
||||
emailtooltip:
|
||||
'Address does not need to be associated with your {mediaServerName} instance.',
|
||||
@@ -23,9 +27,15 @@ const messages = defineMessages({
|
||||
validationemailformat: 'Valid email required',
|
||||
validationusernamerequired: 'Username required',
|
||||
validationpasswordrequired: 'Password required',
|
||||
validationHostnameRequired: 'You must provide a valid hostname or IP address',
|
||||
validationPortRequired: 'You must provide a valid port number',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
validationUrlBaseLeadingSlash: 'URL base must have a leading slash',
|
||||
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
||||
loginerror: 'Something went wrong while trying to sign in.',
|
||||
adminerror: 'You must use an admin account to sign in.',
|
||||
credentialerror: 'The username or password is incorrect.',
|
||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||
signingin: 'Signing in…',
|
||||
signin: 'Sign In',
|
||||
initialsigningin: 'Connecting…',
|
||||
@@ -49,16 +59,23 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
|
||||
if (initial) {
|
||||
const LoginSchema = Yup.object().shape({
|
||||
host: Yup.string()
|
||||
hostname: Yup.string().required(
|
||||
intl.formatMessage(messages.validationhostrequired, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
})
|
||||
),
|
||||
port: Yup.number().required(
|
||||
intl.formatMessage(messages.validationPortRequired)
|
||||
),
|
||||
urlBase: Yup.string()
|
||||
.matches(
|
||||
/^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/,
|
||||
intl.formatMessage(messages.validationhostformat)
|
||||
/^(\/[^/].*[^/]$)/,
|
||||
intl.formatMessage(messages.validationUrlBaseLeadingSlash)
|
||||
)
|
||||
.required(
|
||||
intl.formatMessage(messages.validationhostrequired, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
})
|
||||
.matches(
|
||||
/^(.*[^/])$/,
|
||||
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
|
||||
),
|
||||
email: Yup.string()
|
||||
.email(intl.formatMessage(messages.validationemailformat))
|
||||
@@ -73,12 +90,16 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
username: '',
|
||||
password: '',
|
||||
host: '',
|
||||
hostname: '',
|
||||
port: 8096,
|
||||
useSsl: false,
|
||||
urlBase: '',
|
||||
email: '',
|
||||
}}
|
||||
validationSchema={LoginSchema}
|
||||
@@ -87,18 +108,31 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
await axios.post('/api/v1/auth/jellyfin', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
hostname: values.host,
|
||||
hostname: values.hostname,
|
||||
port: values.port,
|
||||
useSsl: values.useSsl,
|
||||
urlBase: values.urlBase,
|
||||
email: values.email,
|
||||
});
|
||||
} catch (e) {
|
||||
let errorMessage = null;
|
||||
switch (e.response?.data?.message) {
|
||||
case ApiErrorCode.InvalidUrl:
|
||||
errorMessage = messages.invalidurlerror;
|
||||
break;
|
||||
case ApiErrorCode.InvalidCredentials:
|
||||
errorMessage = messages.credentialerror;
|
||||
break;
|
||||
case ApiErrorCode.NotAdmin:
|
||||
errorMessage = messages.adminerror;
|
||||
break;
|
||||
default:
|
||||
errorMessage = messages.loginerror;
|
||||
break;
|
||||
}
|
||||
|
||||
toasts.addToast(
|
||||
intl.formatMessage(
|
||||
e.message == 'Request failed with status code 401'
|
||||
? messages.credentialerror
|
||||
: e.message == 'Request failed with status code 403'
|
||||
? messages.adminerror
|
||||
: messages.loginerror
|
||||
),
|
||||
intl.formatMessage(errorMessage, mediaServerFormatValues),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
@@ -109,32 +143,100 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, isValid }) => (
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => (
|
||||
<Form>
|
||||
<div className="sm:border-t sm:border-gray-800">
|
||||
<label htmlFor="host" className="text-label">
|
||||
{intl.formatMessage(messages.host, mediaServerFormatValues)}
|
||||
<div className="flex flex-col sm:flex-row sm:gap-4">
|
||||
<div className="w-full">
|
||||
<label htmlFor="hostname" className="text-label">
|
||||
{intl.formatMessage(
|
||||
messages.hostname,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mb-0 sm:mt-0">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
{values.useSsl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
type="text"
|
||||
className="rounded-r-only flex-1"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.hostname,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname && touched.hostname && (
|
||||
<div className="error">{errors.hostname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="port" className="text-label">
|
||||
{intl.formatMessage(messages.port)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0">
|
||||
<Field
|
||||
id="port"
|
||||
name="port"
|
||||
inputMode="numeric"
|
||||
type="text"
|
||||
className="short flex-1"
|
||||
placeholder={intl.formatMessage(messages.port)}
|
||||
/>
|
||||
{errors.port && touched.port && (
|
||||
<div className="error">{errors.port}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="useSsl" className="text-label mt-2">
|
||||
{intl.formatMessage(messages.enablessl)}
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Field
|
||||
id="useSsl"
|
||||
name="useSsl"
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
setFieldValue('useSsl', !values.useSsl);
|
||||
setFieldValue('port', values.useSsl ? 8096 : 443);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="urlBase" className="text-label mt-1">
|
||||
{intl.formatMessage(messages.urlBase)}
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Field
|
||||
id="host"
|
||||
name="host"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.host,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
inputMode="url"
|
||||
id="urlBase"
|
||||
name="urlBase"
|
||||
placeholder={intl.formatMessage(messages.urlBase)}
|
||||
/>
|
||||
</div>
|
||||
{errors.host && touched.host && (
|
||||
<div className="error">{errors.host}</div>
|
||||
{errors.urlBase && touched.urlBase && (
|
||||
<div className="error">{errors.urlBase}</div>
|
||||
)}
|
||||
</div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-label"
|
||||
style={{ display: 'inline-flex' }}
|
||||
className="text-label inline-flex gap-1 align-middle"
|
||||
>
|
||||
{intl.formatMessage(messages.email)}
|
||||
<span className="label-tip">
|
||||
@@ -150,7 +252,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
</Tooltip>
|
||||
</span>
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||
<div className="mt-1 sm:col-span-2 sm:mb-2 sm:mt-0">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Field
|
||||
id="email"
|
||||
|
||||
@@ -434,33 +434,38 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && (
|
||||
<Tooltip content={intl.formatMessage(messages.managemovie)}>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() => setShowManager(true)}
|
||||
className="relative ml-2 first:ml-0"
|
||||
>
|
||||
<CogIcon className="!mr-0" />
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
) &&
|
||||
(
|
||||
data.mediaInfo?.issues.filter(
|
||||
(issue) => issue.status === IssueStatus.OPEN
|
||||
) ?? []
|
||||
).length > 0 && (
|
||||
<>
|
||||
<div className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-red-600" />
|
||||
<div className="absolute -right-1 -top-1 h-3 w-3 animate-ping rounded-full bg-red-600" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
data.mediaInfo &&
|
||||
(data.mediaInfo.jellyfinMediaId ||
|
||||
data.mediaInfo.jellyfinMediaId4k ||
|
||||
data.mediaInfo.status !== MediaStatus.UNKNOWN ||
|
||||
data.mediaInfo.status4k !== MediaStatus.UNKNOWN) && (
|
||||
<Tooltip content={intl.formatMessage(messages.managemovie)}>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() => setShowManager(true)}
|
||||
className="relative ml-2 first:ml-0"
|
||||
>
|
||||
<CogIcon className="!mr-0" />
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
) &&
|
||||
(
|
||||
data.mediaInfo?.issues.filter(
|
||||
(issue) => issue.status === IssueStatus.OPEN
|
||||
) ?? []
|
||||
).length > 0 && (
|
||||
<>
|
||||
<div className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-red-600" />
|
||||
<div className="absolute -right-1 -top-1 h-3 w-3 animate-ping rounded-full bg-red-600" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="media-overview">
|
||||
|
||||
@@ -4,6 +4,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import LibraryItem from '@app/components/Settings/LibraryItem';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import type { JellyfinSettings } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import { Field, Formik } from 'formik';
|
||||
@@ -32,9 +33,17 @@ const messages = defineMessages({
|
||||
jellyfinSettingsDescription:
|
||||
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.',
|
||||
externalUrl: 'External URL',
|
||||
internalUrl: 'Internal URL',
|
||||
hostname: 'Hostname or IP Address',
|
||||
port: 'Port',
|
||||
enablessl: 'Use SSL',
|
||||
urlBase: 'URL Base',
|
||||
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
||||
validationUrl: 'You must provide a valid URL',
|
||||
jellyfinSyncFailedNoLibrariesFound: 'No libraries were found',
|
||||
jellyfinSyncFailedAutomaticGroupedFolders:
|
||||
'Custom authentication with Automatic Library Grouping not supported',
|
||||
jellyfinSyncFailedGenericError:
|
||||
'Something went wrong while syncing libraries',
|
||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||
syncing: 'Syncing',
|
||||
syncJellyfin: 'Sync Libraries',
|
||||
manualscanJellyfin: 'Manual Library Scan',
|
||||
@@ -45,6 +54,12 @@ const messages = defineMessages({
|
||||
librariesRemaining: 'Libraries Remaining: {count}',
|
||||
startscan: 'Start Scan',
|
||||
cancelscan: 'Cancel Scan',
|
||||
validationUrl: 'You must provide a valid URL',
|
||||
validationHostnameRequired: 'You must provide a valid hostname or IP address',
|
||||
validationPortRequired: 'You must provide a valid port number',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
validationUrlBaseLeadingSlash: 'URL base must have a leading slash',
|
||||
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
||||
});
|
||||
|
||||
interface Library {
|
||||
@@ -60,6 +75,7 @@ interface SyncStatus {
|
||||
currentLibrary?: Library;
|
||||
libraries: Library[];
|
||||
}
|
||||
|
||||
interface SettingsJellyfinProps {
|
||||
showAdvancedSettings?: boolean;
|
||||
onComplete?: () => void;
|
||||
@@ -70,6 +86,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
showAdvancedSettings,
|
||||
}) => {
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const toasts = useToasts();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -87,18 +104,50 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const JellyfinSettingsSchema = Yup.object().shape({
|
||||
jellyfinExternalUrl: Yup.string().matches(
|
||||
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
|
||||
intl.formatMessage(messages.validationUrl)
|
||||
),
|
||||
jellyfinInternalUrl: Yup.string().matches(
|
||||
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
|
||||
intl.formatMessage(messages.validationUrl)
|
||||
),
|
||||
jellyfinForgotPasswordUrl: Yup.string().matches(
|
||||
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
|
||||
intl.formatMessage(messages.validationUrl)
|
||||
),
|
||||
hostname: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationHostnameRequired))
|
||||
.matches(
|
||||
/^(((([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])):((([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))@)?(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
|
||||
intl.formatMessage(messages.validationHostnameRequired)
|
||||
),
|
||||
port: Yup.number().when(['hostname'], {
|
||||
is: (value: unknown) => !!value,
|
||||
then: Yup.number()
|
||||
.typeError(intl.formatMessage(messages.validationPortRequired))
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationPortRequired)),
|
||||
otherwise: Yup.number()
|
||||
.typeError(intl.formatMessage(messages.validationPortRequired))
|
||||
.nullable(),
|
||||
}),
|
||||
urlBase: Yup.string()
|
||||
.test(
|
||||
'leading-slash',
|
||||
intl.formatMessage(messages.validationUrlBaseLeadingSlash),
|
||||
(value) => !value || value.startsWith('/')
|
||||
)
|
||||
.test(
|
||||
'trailing-slash',
|
||||
intl.formatMessage(messages.validationUrlBaseTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
jellyfinExternalUrl: Yup.string()
|
||||
.nullable()
|
||||
.url(intl.formatMessage(messages.validationUrl))
|
||||
.test(
|
||||
'no-trailing-slash',
|
||||
intl.formatMessage(messages.validationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
jellyfinForgotPasswordUrl: Yup.string()
|
||||
.nullable()
|
||||
.url(intl.formatMessage(messages.validationUrl))
|
||||
.test(
|
||||
'no-trailing-slash',
|
||||
intl.formatMessage(messages.validationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
});
|
||||
|
||||
const activeLibraries =
|
||||
@@ -117,11 +166,43 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
params.enable = activeLibraries.join(',');
|
||||
}
|
||||
|
||||
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||
params,
|
||||
});
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
try {
|
||||
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||
params,
|
||||
});
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
} catch (e) {
|
||||
if (e.response.data.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
|
||||
toasts.addToast(
|
||||
intl.formatMessage(
|
||||
messages.jellyfinSyncFailedAutomaticGroupedFolders
|
||||
),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'warning',
|
||||
}
|
||||
);
|
||||
} else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') {
|
||||
toasts.addToast(
|
||||
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
toasts.addToast(
|
||||
intl.formatMessage(messages.jellyfinSyncFailedGenericError),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
}
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
|
||||
const startScan = async () => {
|
||||
@@ -356,7 +437,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={{
|
||||
jellyfinInternalUrl: data?.hostname || '',
|
||||
hostname: data?.ip,
|
||||
port: data?.port ?? 8096,
|
||||
useSsl: data?.useSsl,
|
||||
urlBase: data?.urlBase || '',
|
||||
jellyfinExternalUrl: data?.externalHostname || '',
|
||||
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
|
||||
}}
|
||||
@@ -364,7 +448,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/jellyfin', {
|
||||
hostname: values.jellyfinInternalUrl,
|
||||
ip: values.hostname,
|
||||
port: Number(values.port),
|
||||
useSsl: values.useSsl,
|
||||
urlBase: values.urlBase,
|
||||
externalHostname: values.jellyfinExternalUrl,
|
||||
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
|
||||
} as JellyfinSettings);
|
||||
@@ -382,44 +469,127 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
addToast(
|
||||
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
if (e.response?.data?.message === ApiErrorCode.InvalidUrl) {
|
||||
addToast(
|
||||
intl.formatMessage(messages.invalidurlerror, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
addToast(
|
||||
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, handleSubmit, isSubmitting, isValid }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
setFieldValue,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => {
|
||||
return (
|
||||
<form className="section" onSubmit={handleSubmit}>
|
||||
<div className="form-row">
|
||||
<label htmlFor="jellyfinInternalUrl" className="text-label">
|
||||
{intl.formatMessage(messages.internalUrl)}
|
||||
<label htmlFor="hostname" className="text-label">
|
||||
{intl.formatMessage(messages.hostname)}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
{values.useSsl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
type="text"
|
||||
inputMode="url"
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
className="rounded-r-only"
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname &&
|
||||
touched.hostname &&
|
||||
typeof errors.hostname === 'string' && (
|
||||
<div className="error">{errors.hostname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="port" className="text-label">
|
||||
{intl.formatMessage(messages.port)}
|
||||
<span className="label-required">*</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
id="port"
|
||||
name="port"
|
||||
className="short"
|
||||
/>
|
||||
{errors.port &&
|
||||
touched.port &&
|
||||
typeof errors.port === 'string' && (
|
||||
<div className="error">{errors.port}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="useSsl" className="checkbox-label">
|
||||
{intl.formatMessage(messages.enablessl)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="useSsl"
|
||||
name="useSsl"
|
||||
onChange={() => {
|
||||
setFieldValue('useSsl', !values.useSsl);
|
||||
setFieldValue('port', values.useSsl ? 8096 : 443);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="urlBase" className="text-label">
|
||||
{intl.formatMessage(messages.urlBase)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
type="text"
|
||||
inputMode="url"
|
||||
id="jellyfinInternalUrl"
|
||||
name="jellyfinInternalUrl"
|
||||
id="urlBase"
|
||||
name="urlBase"
|
||||
/>
|
||||
</div>
|
||||
{errors.jellyfinInternalUrl &&
|
||||
touched.jellyfinInternalUrl && (
|
||||
<div className="error">
|
||||
{errors.jellyfinInternalUrl}
|
||||
</div>
|
||||
{errors.urlBase &&
|
||||
touched.urlBase &&
|
||||
typeof errors.urlBase === 'string' && (
|
||||
<div className="error">{errors.urlBase}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,9 @@ const messages = defineMessages({
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||
locale: 'Display Language',
|
||||
overwriteDnsServers: 'Overwrite Default DNS Servers',
|
||||
overwriteDnsServersTip:
|
||||
'A comma-separated list of DNS server to use instead of the default ones',
|
||||
});
|
||||
|
||||
const SettingsMain = () => {
|
||||
@@ -134,6 +137,7 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
trustProxy: data?.trustProxy,
|
||||
cacheImages: data?.cacheImages,
|
||||
overwriteDnsServers: data?.overwriteDnsServers,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={MainSettingsSchema}
|
||||
@@ -150,6 +154,7 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
trustProxy: values.trustProxy,
|
||||
cacheImages: values.cacheImages,
|
||||
overwriteDnsServers: values.overwriteDnsServers,
|
||||
});
|
||||
mutate('/api/v1/settings/public');
|
||||
mutate('/api/v1/status');
|
||||
@@ -427,6 +432,37 @@ const SettingsMain = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="overwriteDnsServers"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.overwriteDnsServers)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.overwriteDnsServersTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="overwriteDnsServers"
|
||||
name="overwriteDnsServers"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.overwriteDnsServers &&
|
||||
touched.overwriteDnsServers &&
|
||||
typeof errors.overwriteDnsServers === 'string' && (
|
||||
<div className="error">
|
||||
{errors.overwriteDnsServers}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { type SonarrSettings } from '@server/lib/settings';
|
||||
import type { SonarrSettings } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import { Field, Formik } from 'formik';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
@@ -111,7 +109,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
const { addToast } = useToasts();
|
||||
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const settings = useSettings();
|
||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||
profiles: [],
|
||||
rootFolders: [],
|
||||
@@ -258,9 +255,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
animeTags: sonarr?.animeTags ?? [],
|
||||
isDefault: sonarr?.isDefault ?? false,
|
||||
is4k: sonarr?.is4k ?? false,
|
||||
enableSeasonFolders:
|
||||
sonarr?.enableSeasonFolders ??
|
||||
settings.currentSettings.mediaServerType !== MediaServerType.PLEX,
|
||||
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
||||
externalUrl: sonarr?.externalUrl,
|
||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||
enableSearch: !sonarr?.preventSearch,
|
||||
@@ -966,24 +961,11 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
>
|
||||
{intl.formatMessage(messages.seasonfolders)}
|
||||
</label>
|
||||
<div
|
||||
className={`form-input-area ${
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.JELLYFIN ||
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? 'opacity-50'
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enableSeasonFolders"
|
||||
name="enableSeasonFolders"
|
||||
disabled={
|
||||
settings.currentSettings.mediaServerType !==
|
||||
MediaServerType.PLEX
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,8 @@ const messages = defineMessages({
|
||||
discordId: 'Discord User ID',
|
||||
discordIdTip:
|
||||
'The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account',
|
||||
validationemailrequired: 'Email required',
|
||||
validationemailformat: 'Valid email required',
|
||||
validationDiscordId: 'You must provide a valid Discord user ID',
|
||||
plexwatchlistsyncmovies: 'Auto-Request Movies',
|
||||
plexwatchlistsyncmoviestip:
|
||||
@@ -88,6 +90,9 @@ const UserGeneralSettings = () => {
|
||||
);
|
||||
|
||||
const UserGeneralSettingsSchema = Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email(intl.formatMessage(messages.validationemailformat))
|
||||
.required(intl.formatMessage(messages.validationemailrequired)),
|
||||
discordId: Yup.string()
|
||||
.nullable()
|
||||
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||
|
||||
@@ -9,6 +9,7 @@ export type AvailableLocale =
|
||||
| 'en'
|
||||
| 'el'
|
||||
| 'es'
|
||||
| 'es-MX'
|
||||
| 'fr'
|
||||
| 'hr'
|
||||
| 'hu'
|
||||
@@ -59,6 +60,10 @@ export const availableLanguages: AvailableLanguageObject = {
|
||||
code: 'es',
|
||||
display: 'Español',
|
||||
},
|
||||
'es-MX': {
|
||||
code: 'es-MX',
|
||||
display: 'Español (Latinoamérica)',
|
||||
},
|
||||
fr: {
|
||||
code: 'fr',
|
||||
display: 'Français',
|
||||
|
||||
@@ -219,17 +219,20 @@
|
||||
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
||||
"components.Login.credentialerror": "The username or password is incorrect.",
|
||||
"components.Login.adminerror": "You must use an admin account to sign in.",
|
||||
"components.Login.credentialerror": "The username or password is incorrect.",
|
||||
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
|
||||
"components.Login.email": "Email Address",
|
||||
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
|
||||
"components.Login.enablessl": "Use SSL",
|
||||
"components.Login.forgotpassword": "Forgot Password?",
|
||||
"components.Login.host": "{mediaServerName} URL",
|
||||
"components.Login.hostname": "{mediaServerName} URL",
|
||||
"components.Login.initialsignin": "Connect",
|
||||
"components.Login.initialsigningin": "Connecting…",
|
||||
"components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||
"components.Login.loginerror": "Something went wrong while trying to sign in.",
|
||||
"components.Login.password": "Password",
|
||||
"components.Login.port": "Port",
|
||||
"components.Login.save": "Add",
|
||||
"components.Login.saving": "Adding…",
|
||||
"components.Login.signin": "Sign In",
|
||||
@@ -239,9 +242,15 @@
|
||||
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
|
||||
"components.Login.signinwithplex": "Use your Plex account",
|
||||
"components.Login.title": "Add Email",
|
||||
"components.Login.urlBase": "URL Base",
|
||||
"components.Login.username": "Username",
|
||||
"components.Login.validationEmailFormat": "Invalid email",
|
||||
"components.Login.validationEmailRequired": "You must provide an email",
|
||||
"components.Login.validationHostnameRequired": "You must provide a valid hostname or IP address",
|
||||
"components.Login.validationPortRequired": "You must provide a valid port number",
|
||||
"components.Login.validationUrlBaseLeadingSlash": "URL base must have a leading slash",
|
||||
"components.Login.validationUrlBaseTrailingSlash": "URL base must not end in a trailing slash",
|
||||
"components.Login.validationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
"components.Login.validationemailformat": "Valid email required",
|
||||
"components.Login.validationemailrequired": "You must provide a valid email address",
|
||||
"components.Login.validationhostformat": "Valid URL required",
|
||||
@@ -752,8 +761,8 @@
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
|
||||
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "You are running the <code>develop</code> branch of Jellyseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||
"components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||
@@ -936,12 +945,16 @@
|
||||
"components.Settings.experimentalTooltip": "Enabling this setting may result in unexpected application behavior",
|
||||
"components.Settings.externalUrl": "External URL",
|
||||
"components.Settings.hostname": "Hostname or IP Address",
|
||||
"components.Settings.internalUrl": "Internal URL",
|
||||
"components.Settings.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||
"components.Settings.is4k": "4K",
|
||||
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
||||
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
|
||||
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.",
|
||||
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
||||
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
||||
"components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries",
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found",
|
||||
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",
|
||||
"components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.",
|
||||
"components.Settings.jellyfinsettings": "{mediaServerName} Settings",
|
||||
@@ -1172,6 +1185,8 @@
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Valid email required",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Email required",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
"components.Settings.SonarrModal.animerootfolder": "Carpeta raíz de anime",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Perfil de calidad de anime",
|
||||
"components.Settings.SettingsAbout.timezone": "Zona horaria",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Apoya a Overseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Apoya a Jellyseerr",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Ayúdame invitándome a un café",
|
||||
"components.Settings.SettingsAbout.Releases.viewongithub": "Ver en GitHub",
|
||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "Ver registro de cambios",
|
||||
@@ -299,14 +299,14 @@
|
||||
"components.RequestButton.viewrequest": "Ver Solicitud",
|
||||
"components.RequestButton.requestmore4k": "Solicitar más en 4K",
|
||||
"components.RequestButton.requestmore": "Solicitar más",
|
||||
"components.RequestButton.declinerequests": "Rechazar {requestCount, plural, one {Request} other {{requestCount} Requests}}",
|
||||
"components.RequestButton.declinerequests": "Rechazar {requestCount, plural, one {solicitud} other {{requestCount} solicitudes}}",
|
||||
"components.RequestButton.declinerequest4k": "Rechazar Solicitud 4K",
|
||||
"components.RequestButton.declinerequest": "Rechazar Solicitud",
|
||||
"components.RequestButton.decline4krequests": "Rechazar {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
|
||||
"components.RequestButton.approverequests": "Aprobar {requestCount, plural, one {Request} other {{requestCount} Requests}}",
|
||||
"components.RequestButton.decline4krequests": "Rechazar {requestCount, plural, one {solicitud en 4K} other {{requestCount} solicitudes en 4K}}",
|
||||
"components.RequestButton.approverequests": "Aprobar {requestCount, plural, one {solicitud} other {{requestCount} solicitudes}}",
|
||||
"components.RequestButton.approverequest4k": "Aprobar Solicitud 4K",
|
||||
"components.RequestButton.approverequest": "Aprobar Solicitud",
|
||||
"components.RequestButton.approve4krequests": "Aprobar {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
|
||||
"components.RequestButton.approve4krequests": "Aprobar {requestCount, plural, one {petición en 4K} other {requestCount} peticiones en 4K}}",
|
||||
"components.RequestBlock.server": "Servidor de Destino",
|
||||
"components.RequestBlock.rootfolder": "Carpeta Raíz",
|
||||
"components.RequestBlock.profilechanged": "Perfil de Calidad",
|
||||
@@ -656,7 +656,7 @@
|
||||
"components.QuotaSelector.unlimited": "Ilimitadas",
|
||||
"components.MovieDetails.originaltitle": "Título Original",
|
||||
"components.LanguageSelector.originalLanguageDefault": "Todos los Idiomas",
|
||||
"components.LanguageSelector.languageServerDefault": "({language}) por defecto",
|
||||
"components.LanguageSelector.languageServerDefault": "({Language}) por defecto",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {cambio} other {cambios}} por detrás",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Tu cuenta no tiene configurada una contraseña actualmente. Configure una contraseña a continuación para habilitar el acceso como \"usuario local\" utilizando tu dirección de email.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Esta cuenta de usuario no tiene configurada una contraseña actualmente. Configure una contraseña a continuación para habilitar el acceso como \"usuario local\"",
|
||||
@@ -784,7 +784,7 @@
|
||||
"components.DownloadBlock.estimatedtime": "Estimación de {time}",
|
||||
"components.Settings.Notifications.encryptionOpportunisticTls": "Usa siempre STARTTLS",
|
||||
"components.TvDetails.streamingproviders": "Emisión Actual en",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "({language}) por defecto",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "{{Language}} por defecto",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Para recibir notificaciones web push, Jellyseerr debe servirse mediante HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationTypes": "Debes seleccionar, al menos, un tipo de notificación",
|
||||
"components.Settings.Notifications.validationTypes": "Debes seleccionar, al menos, un tipo de notificación",
|
||||
@@ -816,10 +816,10 @@
|
||||
"components.Settings.Notifications.encryptionTip": "Normalmente, TLS Implícito usa el puerto 465 y STARTTLS usa el puerto 587",
|
||||
"components.UserList.localLoginDisabled": "El ajuste para <strong>Habilitar el Inicio de Sesión Local</strong> está actualmente deshabilitado.",
|
||||
"components.Settings.SettingsUsers.defaultPermissionsTip": "Permisos iniciales asignados a nuevos usuarios",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "Estás utilizando la rama de <code>desarrollo</code> de Jellyseerr, la cual solo se recomienda para aquellos que contribuyen al desarrollo o al soporte de las pruebas de nuevos desarrollos.",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "Estás utilizando la rama de <code>develop</code> de Jellyseerr, la cual solo se recomienda para aquellos que contribuyen al desarrollo o al soporte de las pruebas de nuevos desarrollos.",
|
||||
"components.StatusBadge.status": "{status}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Cada {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Cada {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Cada {jobScheduleMinutes, plural, one {minuto} other {{jobScheduleMinutes} minutos}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Cada {jobScheduleHours, plural, one {hora} other {{jobScheduleHours} horas}}",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Algo fue mal al guardar la tarea programada.",
|
||||
"components.Settings.SettingsJobsCache.editJobSchedule": "Modificar tarea programada",
|
||||
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Nueva frecuencia",
|
||||
@@ -848,7 +848,7 @@
|
||||
"components.IssueDetails.nocomments": "Sin comentarios.",
|
||||
"components.IssueDetails.openedby": "#{issueId} abierta {relativeTime} por {username}",
|
||||
"components.IssueDetails.openin4karr": "Abrir en {arr} 4K",
|
||||
"components.IssueDetails.openinarr": "Abrir en {arr}",
|
||||
"components.IssueDetails.openinarr": "Abierta en {arr}",
|
||||
"components.IssueDetails.play4konplex": "Ver en 4K en {mediaServerName}",
|
||||
"components.IssueDetails.playonplex": "Ver en {mediaServerName}",
|
||||
"components.IssueDetails.problemepisode": "Episodio Afectado",
|
||||
@@ -1193,7 +1193,7 @@
|
||||
"components.RequestList.RequestItem.tvdbid": "Identificador de TheTVDB",
|
||||
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Limpieza de la caché de imágenes",
|
||||
"components.Settings.SettingsJobsCache.imagecache": "Caché de imágenes",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Cada {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Cada {jobScheduleSeconds, plural, one {segundo} other {{jobScheduleSeconds} segundos}}",
|
||||
"components.Settings.SettingsJobsCache.availability-sync": "Sincronización de la disponibilidad de medios",
|
||||
"components.Discover.tmdbmoviestreamingservices": "Servicios de streaming de películas TMDB",
|
||||
"components.Discover.tmdbtvstreamingservices": "Servicios de TV en streaming TMDB",
|
||||
@@ -1204,111 +1204,5 @@
|
||||
"i18n.collection": "Colección",
|
||||
"components.Settings.RadarrModal.tagRequestsInfo": "Añadir automáticamente una etiqueta adicional con el nombre de usuario y el nombre para mostrar del solicitante",
|
||||
"components.Settings.SonarrModal.tagRequestsInfo": "Añadir automáticamente una etiqueta adicional con el nombre de usuario y el nombre para mostrar del solicitante",
|
||||
"components.MovieDetails.imdbuserscore": "Puntuación de los usuarios de IMDB",
|
||||
"components.Layout.UserWarnings.passwordRequired": "Se requiere una contraseña.",
|
||||
"components.Login.credentialerror": "El usuario o contraseña es incorrecto.",
|
||||
"components.Login.host": "{mediaServerName} URL",
|
||||
"components.Login.initialsignin": "Conectar",
|
||||
"components.Login.initialsigningin": "Conectando…",
|
||||
"components.Login.emailtooltip": "No es necesario asociar la dirección con su instancia de {mediaServerName}.",
|
||||
"components.Login.saving": "Añadiendo…",
|
||||
"components.Login.title": "Añadir Email",
|
||||
"components.Login.username": "Nombre de usuario",
|
||||
"components.Login.validationEmailFormat": "El email es inválido",
|
||||
"components.Login.validationEmailRequired": "Debes proporcional un email",
|
||||
"components.Login.validationemailformat": "Se requiere de un email válido",
|
||||
"components.Login.validationhostformat": "Se requiere de una URL válida",
|
||||
"components.Login.validationusernamerequired": "Se requiere un nombre de usuario",
|
||||
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Esto eliminará de manera irreversible esta {mediaType} de {arr}, incluyendo todos los archivos.",
|
||||
"i18n.open": "Abierto",
|
||||
"components.MovieDetails.downloadstatus": "Estado de la descarga",
|
||||
"components.MovieDetails.openradarr": "Abrir Película en Radarr",
|
||||
"components.MovieDetails.openradarr4k": "Abrir Película 4K en Radarr",
|
||||
"components.MovieDetails.play": "Reproducir en {mediaServerName}",
|
||||
"components.MovieDetails.play4k": "Reproducir 4K en {mediaServerName}",
|
||||
"components.NotificationTypeSelector.issueresolved": "Incidencia Resuelta",
|
||||
"components.NotificationTypeSelector.userissuecommentDescription": "Notificame cuando haya nuevos comentarios en incidencias que haya abierto.",
|
||||
"components.NotificationTypeSelector.userissuecreatedDescription": "Notificame cuando otros usuarios reporten incidencias.",
|
||||
"components.PermissionEdit.viewissues": "Ver incidencias",
|
||||
"components.PermissionEdit.manageissuesDescription": "Dar permiso para administrar incidencias.",
|
||||
"components.PermissionEdit.viewissuesDescription": "Dar permiso para ver incidencias reportadas por otros usuarios.",
|
||||
"components.Settings.Notifications.NotificationsPushover.sound": "Sonido de Notificacion",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Escanear Añadidos Recientemente de Jellyfin",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Escaneo Completo de la libreria de Jellyfin",
|
||||
"components.Settings.jellyfinSettings": "Ajustes de {mediaServerName}",
|
||||
"components.Settings.jellyfinsettings": "Ajustes de {mediaServerName}",
|
||||
"components.Settings.jellyfinSettingsFailure": "Algo salió mal al guardar la configuración de {mediaServerName}.",
|
||||
"components.Settings.jellyfinSettingsSuccess": "¡La configuración de {mediaServerName} se guardó correctamente!",
|
||||
"components.Settings.jellyfinlibraries": "Bibliotecas {mediaServerName}",
|
||||
"components.Settings.jellyfinlibrariesDescription": "La biblioteca {mediaServerName} busca títulos. Haga clic en el botón a continuación si no aparece ninguna biblioteca.",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Normalmente, esto sólo se ejecutará una vez cada 24 horas. Jellyseerr comprobará de forma más agresiva los añadidos recientemente de su servidor {mediaServerName}. ¡Si es la primera vez que configura Jellyseerr, se recomienda un escaneo manual completo de la biblioteca!",
|
||||
"components.Settings.save": "Guardar Cambios",
|
||||
"components.Settings.saving": "Guardando…",
|
||||
"components.Settings.syncing": "Sincronizando",
|
||||
"components.Settings.timeout": "Tiempo agotado",
|
||||
"components.Setup.signinWithPlex": "Usa tu cuenta de Plex",
|
||||
"components.Setup.configuremediaserver": "Configurar servidor multimedia",
|
||||
"components.TitleCard.addToWatchList": "Añadir a lista de seguimiento",
|
||||
"components.TitleCard.watchlistCancel": "Lista de seguimiento para <strong>{title}</strong> cancelada.",
|
||||
"components.TitleCard.watchlistError": "Algo salió mal, intenta de nuevo.",
|
||||
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> añadido correctamente a la lista de seguimiento!",
|
||||
"components.TvDetails.play": "Reproducir en {mediaServerName}",
|
||||
"components.UserList.importfromJellyfin": "Importar Usuarios de {mediaServerName}",
|
||||
"components.UserList.mediaServerUser": "Usuario de {mediaServerName}",
|
||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} importado correctamente!",
|
||||
"components.UserList.importfromJellyfinerror": "Se produjo un error al importar usuarios de {mediaServerName}.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Guardar Cambios",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Email",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Dispositivo Predeterminado",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Sonido de Notificacion",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Fallo al guardar los ajustes de la notificación Pushbullet.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "¡Los ajustes de notificación Pushbullet se han guardado con éxito!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "Token de aplicación API",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Clave de usuario o grupo",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "Tu <UsersGroupsLink>identificador de usuario o grupo</UsersGroupsLink> de 30 caracteres",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "¡Se han guardado los ajustes de notificación de Pushover!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "<ApplicationRegistrationLink>Registrar una aplicación</ApplicationRegistrationLink> para usar con {applicationTitle}",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Debes proporcionar una clave de usuario o grupo válida",
|
||||
"components.Login.validationhostrequired": "{mediaServerName} URL requerida",
|
||||
"i18n.resolved": "Resuelto",
|
||||
"components.UserList.importfrommediaserver": "Importar Usuarios de {mediaServerName}",
|
||||
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Dispositivo Predeterminado",
|
||||
"components.ManageSlideOver.removearr": "Eliminar de {arr}",
|
||||
"components.NotificationTypeSelector.issuereopenedDescription": "Enviar notificación cuando se reabran incidencias.",
|
||||
"components.Layout.UserWarnings.emailRequired": "Se requiere una dirección de email.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Token de Acceso",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Debes proporcionar un token de aplicación válido",
|
||||
"components.Settings.syncJellyfin": "Sincronizar Bibliotecas",
|
||||
"components.Layout.UserWarnings.emailInvalid": "La dirección de correo es inválida.",
|
||||
"components.Login.description": "Como es la primera vez que inicias sesión en {applicationName}, es necesario añadir una dirección de email válida.",
|
||||
"components.Login.save": "Añadir",
|
||||
"components.NotificationTypeSelector.issueresolvedDescription": "Enviar notificación cuando se resuelvan incidencias.",
|
||||
"components.PermissionEdit.manageissues": "Administrar incidencias",
|
||||
"components.PermissionEdit.createissues": "Notificar incidencia",
|
||||
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Guardando…",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "Debes indicar un token de acceso",
|
||||
"components.Login.signinwithjellyfin": "Utiliza tu cuenta de {mediaServerName}",
|
||||
"components.ManageSlideOver.removearr4k": "Eliminar de {arr} 4K",
|
||||
"components.Settings.internalUrl": "URL Interna",
|
||||
"components.TvDetails.play4k": "Reproducir 4K en {mediaServerName}",
|
||||
"components.Setup.signin": "Iniciar Sesión",
|
||||
"components.Setup.signinWithJellyfin": "Utiliza tu cuenta de {mediaServerName}",
|
||||
"components.UserList.noJellyfinuserstoimport": "No hay usuarios de {mediaServerName} que importar.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Usuario de {mediaServerName}",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Crea un token desde tu <PushbulletSettingsLink>Opciones de Cuenta</PushbulletSettingsLink>",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "No se pudo guardar la configuración de notificaciones de Pushover.",
|
||||
"components.NotificationTypeSelector.userissuereopenedDescription": "Recibir notificaciones cuando se vuelvan a abrir incidencias que haya reportado.",
|
||||
"components.NotificationTypeSelector.userissueresolvedDescription": "Reciba notificaciones cuando se resuelvan las incidencias que haya reportado.",
|
||||
"components.PermissionEdit.createissuesDescription": "Dar permiso para informar incidencias.",
|
||||
"components.Settings.SettingsAbout.supportjellyseerr": "Apoya a Jellyseerr",
|
||||
"components.Settings.Notifications.userEmailRequired": "Requerir email de usuario",
|
||||
"components.Settings.SonarrModal.animeSeriesType": "Tipo de Serie Anime",
|
||||
"components.Settings.SonarrModal.seriesType": "Tipo Serie",
|
||||
"components.Settings.jellyfinSettingsDescription": "Opcionalmente, configure los puntos finales internos y externos para su servidor {mediaServerName}. En la mayoría de los casos, la URL externa es diferente a la URL interna. También se puede configurar una URL de restablecimiento de contraseña personalizada para el inicio de sesión de {mediaServerName}, en caso de que desee redirigir a una página de restablecimiento de contraseña diferente.",
|
||||
"components.Settings.jellyfinsettingsDescription": "Configure los ajustes para su servidor {mediaServerName}. {mediaServerName} escanea sus bibliotecas de {mediaServerName} para ver qué contenido está disponible.",
|
||||
"components.Settings.manualscanJellyfin": "Escanear Libreria Manualmente",
|
||||
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> Eliminado correctamente de la lista de seguimiento!",
|
||||
"components.UserList.newJellyfinsigninenabled": "La configuración <strong>Habilitar nuevo inicio de sesión de {mediaServerName}</strong> está actualmente habilitada. Los usuarios de {mediaServerName} con acceso a la biblioteca no necesitan ser importados para poder iniciar sesión.",
|
||||
"components.UserProfile.localWatchlist": "Lista de seguimiento de {username}"
|
||||
"components.MovieDetails.imdbuserscore": "Puntuación de los usuarios de IMDB"
|
||||
}
|
||||
|
||||
1205
src/i18n/locale/es_MX.json
Normal file
@@ -59,7 +59,7 @@
|
||||
"components.Discover.DiscoverTvGenre.genreSeries": "Séries {genre}",
|
||||
"components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Séries",
|
||||
"components.Discover.DiscoverTvLanguage.languageSeries": "Séries en {language}",
|
||||
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Votre watchlist",
|
||||
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Votre watchlist Plex",
|
||||
"components.Discover.DiscoverWatchlist.watchlist": "Watchlist Plex",
|
||||
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Filtre actif} other {# Filtres actifs}}",
|
||||
"components.Discover.FilterSlideover.clearfilters": "Effacer les filtres actifs",
|
||||
@@ -1251,71 +1251,5 @@
|
||||
"components.Settings.SonarrModal.tagRequests": "Tager les demandes",
|
||||
"components.Settings.SonarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur",
|
||||
"i18n.collection": "Collection",
|
||||
"components.Settings.RadarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur",
|
||||
"components.IssueModal.issueVideo": "Vidéo",
|
||||
"components.Settings.Notifications.NotificationsPushover.sound": "Son de notification",
|
||||
"components.Settings.jellyfinSettings": "Paramètres pour {mediaServerName}",
|
||||
"components.Settings.jellyfinSettingsFailure": "Une erreur est survenue lors de l'enregistrement des paramètres pour {mediaServerName}.",
|
||||
"components.Settings.jellyfinSettingsSuccess": "Les paramètres pour {mediaServerName} ont été enregistrés avec succès !",
|
||||
"components.Settings.jellyfinlibraries": "Bibliothèques {mediaServerName}",
|
||||
"components.Settings.jellyfinlibrariesDescription": "Les bibliothèques de {mediaServerName} sont en cours d'analyze. Cliquez sur le bouton ci-dessous si aucune bibliothèque n'est répertoriée.",
|
||||
"components.Settings.jellyfinsettings": "Paramètres pour {mediaServerName}",
|
||||
"components.Settings.manualscanJellyfin": "Analyse manuelle de la bibliothèque",
|
||||
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
||||
"components.Settings.save": "Enregistrer les modifications",
|
||||
"components.Settings.saving": "Sauvegarde en cours…",
|
||||
"components.Settings.syncing": "Synchronisation en cours",
|
||||
"components.Setup.signin": "Se connecter",
|
||||
"components.Setup.signinWithPlex": "Utilisez votre compte Plex",
|
||||
"components.StatusBadge.managemedia": "Gérer {mediaType}",
|
||||
"components.StatusBadge.openinarr": "Ouvrir dans {arr}",
|
||||
"components.StatusBadge.playonplex": "Lire sur {mediaServerName}",
|
||||
"components.TitleCard.addToWatchList": "Ajouter à votre watchlist",
|
||||
"components.TitleCard.watchlistCancel": "Watchlist pour <strong>{title}</strong> annulée.",
|
||||
"components.TitleCard.watchlistError": "Une erreur est survenue. Veuillez réessayer.",
|
||||
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> a été ajouté à votre watchlist avec succès !",
|
||||
"components.TvDetails.Season.somethingwentwrong": "Une erreur est survenue lors de la récupération des données de la saison.",
|
||||
"components.TvDetails.manageseries": "Gérer les séries",
|
||||
"components.TvDetails.play": "Jouer sur {mediaServerName}",
|
||||
"components.TvDetails.play4k": "Jouer en 4K sur {mediaServerName}",
|
||||
"components.TvDetails.rtcriticsscore": "Tomatometer sur Rotten Tomatoes",
|
||||
"components.TvDetails.seasonnumber": "Saison {seasonNumber}",
|
||||
"components.TvDetails.seasonstitle": "Saisons",
|
||||
"components.TvDetails.status4k": "{status} 4K",
|
||||
"components.TvDetails.tmdbuserscore": "Score utilisateur sur TMDB",
|
||||
"components.UserList.importfromJellyfin": "Importer les utilisateurs de {mediaServerName}",
|
||||
"components.UserList.importfromJellyfinerror": "Une erreur est survenue lors de l'importation des utilisateurs de {mediaServerName}.",
|
||||
"components.UserList.importfrommediaserver": "Importer les utilisateurs de {mediaServerName}",
|
||||
"components.UserList.noJellyfinuserstoimport": "Il n'y a aucun utilisateur à importer pour {mediaServerName}.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Sauvegarde en cours…",
|
||||
"components.UserProfile.plexwatchlist": "Watchlist Plex",
|
||||
"components.Settings.syncJellyfin": "Synchroniser les bibliothèques",
|
||||
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> a été retiré de votre watchlist avec succès !",
|
||||
"components.IssueModal.issueSubtitles": "Sous-titre",
|
||||
"components.Login.emailtooltip": "L'adresse ne nécessite pas d'être associée avec votre instance {mediaServerName}.",
|
||||
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Appareil par défaut",
|
||||
"components.Settings.Notifications.userEmailRequired": "E-mail utilisateur requis",
|
||||
"components.Settings.SettingsAbout.supportjellyseerr": "Soutenez Jellyseerr",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Scan complet des bibliothèques Jellyfin",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Scan des ajouts récents aux bibliothèques Jellyfin",
|
||||
"components.Settings.SonarrModal.animeSeriesType": "Type de série anime",
|
||||
"components.Settings.SonarrModal.seriesType": "Type de série",
|
||||
"components.Settings.internalUrl": "URL interne",
|
||||
"components.Settings.jellyfinsettingsDescription": "Configurez les paramètres de votre serveur {mediaServerName}. {mediaServerName} analyse vos bibliothèques {mediaServerName} pour voir quel contenu est disponible.",
|
||||
"components.Settings.jellyfinSettingsDescription": "Configurez facultativement les URL internes et externes pour votre serveur {mediaServerName}. Dans la plupart des cas, l'URL externe est différente de l'URL interne. Vous pouvez également définir une URL de réinitialisation de mot de passe personnalisée pour la connexion à {mediaServerName}, au cas où vous souhaiteriez rediriger vers une page de réinitialisation de mot de passe différente.",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Normalement, cette tâche est executée qu'une fois toutes les 24 heures. Jellyseerr vérifiera plus agressivement les éléments récemment ajoutés à votre serveur {mediaServerName}. Si c'est la première fois que vous configurez Jellyseerr, une analyse complète manuelle de la bibliothèque est recommandée !",
|
||||
"components.Settings.timeout": "Temps écoulé",
|
||||
"components.Setup.configuremediaserver": "Configurer le serveur multimédia",
|
||||
"components.TvDetails.rtaudiencescore": "Score de l'audience sur Rotten Tomatoes",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Utilisateur pour {mediaServerName}",
|
||||
"components.Setup.signinWithJellyfin": "Utilisez votre compte {mediaServerName}",
|
||||
"components.UserList.mediaServerUser": "Utilisateur de {mediaServerName}",
|
||||
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Épisode} other {# Épisodes}}",
|
||||
"components.UserList.newJellyfinsigninenabled": "Le paramètre <strong>Activer la nouvelle connexion à {mediaServerName}</strong> est actuellement activé. Les utilisateurs de {mediaServerName} avec accès à la bibliothèque n'ont pas besoin d'être importés pour se connecter.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Enregistrer les modifications",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Appareil par défaut",
|
||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} importé(s) avec succès !",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Son de notification",
|
||||
"components.UserProfile.localWatchlist": "Watchlist de {username}"
|
||||
"components.Settings.RadarrModal.tagRequestsInfo": "Ajouter automatiquement un tag supplémentaire avec l'ID utilisateur et le nom d'affichage du demandeur"
|
||||
}
|
||||
|
||||
@@ -135,67 +135,5 @@
|
||||
"components.Login.loginerror": "משהו השתבש בזמן ההתחברות.",
|
||||
"components.Login.password": "סיסמה",
|
||||
"components.Login.signin": "התחברות",
|
||||
"components.Login.validationpasswordrequired": "חובה לכתוב סיסמה",
|
||||
"components.Discover.CreateSlider.editSlider": "ערוך סליידר",
|
||||
"components.Discover.CreateSlider.editfail": "נכשלה עריכת סליידר.",
|
||||
"components.Discover.CreateSlider.needresults": "אתה צריך לפחות תוצאה אחת.",
|
||||
"components.Discover.CreateSlider.nooptions": "אין תוצאות.",
|
||||
"components.Discover.CreateSlider.providetmdbgenreid": "ספק מזהה ז'אנר TMDB",
|
||||
"components.Discover.CreateSlider.providetmdbkeywordid": "ספק מזהה מילת מפתח TMDB",
|
||||
"components.Discover.CreateSlider.providetmdbsearch": "ספק שאילתת חיפוש",
|
||||
"components.Discover.CreateSlider.providetmdbstudio": "ספק מזהה סטודיו TMDB",
|
||||
"components.Discover.CreateSlider.searchGenres": "חפש ז'אנרים…",
|
||||
"components.Discover.CreateSlider.searchKeywords": "חפש מילות מפתח…",
|
||||
"components.Discover.CreateSlider.searchStudios": "חפש אולפנים…",
|
||||
"components.Discover.CreateSlider.slidernameplaceholder": "שם הסליידר",
|
||||
"components.Discover.CreateSlider.validationDatarequired": "עליך לספק ערך.",
|
||||
"components.Discover.CreateSlider.validationTitlerequired": "עליך לספק כותר.",
|
||||
"components.Discover.DiscoverMovieKeyword.keywordMovies": "סרטים {keywordTitle}",
|
||||
"components.Discover.DiscoverMovies.discovermovies": "סרטים",
|
||||
"components.Discover.DiscoverMovies.sortPopularityAsc": "פופולריות בסדר עולה",
|
||||
"components.Discover.DiscoverMovies.sortPopularityDesc": "פופולריות בסדר יורד",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "תאריך שחרור בסדר עולה",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "תאריך שחרור בסדר יורד",
|
||||
"components.Discover.DiscoverMovies.sortTitleAsc": "כותר (א-ת, A-Z) עולה",
|
||||
"components.Discover.DiscoverMovies.sortTitleDesc": "כותר (ת-א, A-Z) יורד",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "דירוג TMDB בסדר עולה",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "דירוג TMDB בסדר יורד",
|
||||
"components.Discover.DiscoverSliderEdit.deletesuccess": "סליידר נמחק בהצלחה.",
|
||||
"components.Discover.DiscoverSliderEdit.enable": "שנה נראות",
|
||||
"components.Discover.DiscoverSliderEdit.remove": "הסר",
|
||||
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {פילטר אחד פעיל} other {# פילטרים פעילים}}",
|
||||
"components.Discover.DiscoverTv.discovertv": "סדרה",
|
||||
"components.Discover.CreateSlider.addfail": "נכשלה יצירת סליידר חדש.",
|
||||
"components.Discover.DiscoverTv.sortPopularityAsc": "פופולריות בסדר עולה",
|
||||
"components.Discover.DiscoverTv.sortPopularityDesc": "פופולריות בסדר יורד",
|
||||
"components.Discover.DiscoverTv.sortTitleAsc": "כותר (א-ת, A-Z) עולה",
|
||||
"components.Discover.DiscoverTv.sortTitleDesc": "כותר (א-ת, A-Z) יורד",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "דירוג TMDB בסדר עולה",
|
||||
"components.Discover.DiscoverTvKeyword.keywordSeries": "סדרה {keywordTitle}",
|
||||
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {פילטר אחד פעיל} other {# פילטרים פעילים}}",
|
||||
"components.Discover.FilterSlideover.clearfilters": "נקה פילטרים פעילים",
|
||||
"components.Discover.FilterSlideover.filters": "פילטרים",
|
||||
"components.Discover.FilterSlideover.firstAirDate": "תאריך שידור ראשון",
|
||||
"components.Discover.FilterSlideover.from": "מאת",
|
||||
"components.Discover.FilterSlideover.keywords": "מילות מפתח",
|
||||
"components.Discover.FilterSlideover.originalLanguage": "שפה מקורית",
|
||||
"components.Discover.FilterSlideover.ratingText": "דירוג בין {minValue} ל {maxValue}",
|
||||
"components.Discover.FilterSlideover.releaseDate": "תאריך שחרור",
|
||||
"components.Discover.FilterSlideover.runtime": "זמן ריצה",
|
||||
"components.Discover.FilterSlideover.streamingservices": "שירותי הזרמה",
|
||||
"components.Discover.FilterSlideover.studio": "אולפן",
|
||||
"components.Discover.FilterSlideover.tmdbuserscore": "דירוג משתמש TMDB",
|
||||
"components.Discover.CreateSlider.addSlider": "הוסף סליידר",
|
||||
"components.Discover.CreateSlider.addcustomslider": "צור סליידר מותאם",
|
||||
"components.Discover.CreateSlider.addsuccess": "סליידר חדש נוצר בהצלחה ונשמרו הגדרות התאמה.",
|
||||
"components.Discover.CreateSlider.providetmdbnetwork": "ספק מזהה רשת TMDB",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "תאריך שידור ראשון בסדר יורד",
|
||||
"components.Discover.CreateSlider.starttyping": "התחל להזין כדי לחפש.",
|
||||
"components.Discover.FilterSlideover.runtimeText": "זמן ריצה בין {minValue} ל {maxValue}",
|
||||
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {פילטר פעיל} other {# פילטרים פעילים}}",
|
||||
"components.Discover.DiscoverSliderEdit.deletefail": "כשל במחיקת סליידר.",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "תאריך שידור ראשון בסדר עולה",
|
||||
"components.Discover.CreateSlider.editsuccess": "סליידר נערך ונשמר בהצלחה.",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "דירוג TMDB בסדר יורד",
|
||||
"components.Discover.FilterSlideover.genres": "ז'אנרים"
|
||||
"components.Login.validationpasswordrequired": "חובה לכתוב סיסמה"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"components.Discover.discovertv": "Populaire series",
|
||||
"components.Discover.popularmovies": "Populaire films",
|
||||
"components.Discover.populartv": "Populaire series",
|
||||
"components.Discover.recentlyAdded": "Onlangs toegevoegd",
|
||||
"components.Discover.recentlyAdded": "Recent toegevoegd",
|
||||
"components.Discover.recentrequests": "Recente verzoeken",
|
||||
"components.Discover.trending": "Trending",
|
||||
"components.Discover.upcoming": "Verwachte films",
|
||||
"components.Discover.upcomingmovies": "Verwachte films",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Films en series zoeken",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Zoek films en series",
|
||||
"components.Layout.Sidebar.dashboard": "Ontdekken",
|
||||
"components.Layout.Sidebar.requests": "Verzoeken",
|
||||
"components.Layout.Sidebar.settings": "Instellingen",
|
||||
@@ -16,7 +16,7 @@
|
||||
"components.Layout.UserDropdown.signout": "Uitloggen",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.cast": "Cast",
|
||||
"components.MovieDetails.originallanguage": "Oorspronkelijke taal",
|
||||
"components.MovieDetails.originallanguage": "Originele taal",
|
||||
"components.MovieDetails.overview": "Overzicht",
|
||||
"components.MovieDetails.overviewunavailable": "Overzicht niet beschikbaar.",
|
||||
"components.MovieDetails.recommendations": "Aanbevelingen",
|
||||
@@ -114,7 +114,7 @@
|
||||
"components.Settings.hostname": "Hostnaam of IP-adres",
|
||||
"components.Settings.librariesRemaining": "Resterende bibliotheken: {count}",
|
||||
"components.Settings.manualscan": "Handmatige bibliotheekscan",
|
||||
"components.Settings.manualscanDescription": "Normaliter wordt dit eenmaal per 24 uur uitgevoerd. Jellyseerr zal de lijst met onlangs toegevoegde media op je Plex-server vaker controleren. Als dit de eerste keer is dat je Jellyseerr instelt, wordt aanbevolen eenmalig een handmatige, volledige bibliotheekscan uit te voeren!",
|
||||
"components.Settings.manualscanDescription": "Normaal wordt dit eens elke 24 uur uitgevoerd. Jellyseerr controleert de recent toegevoegde items van je Plex-server agressiever. Als je Plex voor de eerste keer configureert, is een eenmalige handmatige volledige bibliotheekscan aanbevolen!",
|
||||
"components.Settings.menuAbout": "Over",
|
||||
"components.Settings.menuGeneralSettings": "Algemeen",
|
||||
"components.Settings.menuJobs": "Taken en cache",
|
||||
@@ -127,7 +127,7 @@
|
||||
"components.Settings.plexlibraries": "Plex-bibliotheken",
|
||||
"components.Settings.plexlibrariesDescription": "De bibliotheken die Jellyseerr scant voor titels. Stel je Plex-verbinding in en sla ze op. Klik daarna op de onderstaande knop als er geen bibliotheken staan.",
|
||||
"components.Settings.plexsettings": "Plex-instellingen",
|
||||
"components.Settings.plexsettingsDescription": "Configureer de instellingen voor je Plex-server. Jellyseerr scant je Plex-bibliotheken om te zien welke inhoud beschikbaar is.",
|
||||
"components.Settings.plexsettingsDescription": "Configureer de instellingen voor je Plex-server. Jellyseerr scant je Plex-bibliotheken om te zien welke content beschikbaar is.",
|
||||
"components.Settings.port": "Poort",
|
||||
"components.Settings.radarrsettings": "Radarr-instellingen",
|
||||
"components.Settings.sonarrsettings": "Sonarr-instellingen",
|
||||
@@ -137,12 +137,12 @@
|
||||
"components.Setup.configureservices": "Diensten configureren",
|
||||
"components.Setup.continue": "Doorgaan",
|
||||
"components.Setup.finish": "Installatie voltooien",
|
||||
"components.Setup.finishing": "Voltooien…",
|
||||
"components.Setup.finishing": "Bezig met voltooien…",
|
||||
"components.Setup.loginwithplex": "Inloggen met Plex",
|
||||
"components.Setup.signinMessage": "Ga aan de slag door je aan te melden",
|
||||
"components.Setup.signinMessage": "Ga aan de slag door in te loggen met je Plex-account",
|
||||
"components.Setup.welcome": "Welkom bij Jellyseerr",
|
||||
"components.TvDetails.cast": "Cast",
|
||||
"components.TvDetails.originallanguage": "Oorspronkelijke taal",
|
||||
"components.TvDetails.originallanguage": "Originele taal",
|
||||
"components.TvDetails.overview": "Overzicht",
|
||||
"components.TvDetails.overviewunavailable": "Overzicht niet beschikbaar.",
|
||||
"components.TvDetails.recommendations": "Aanbevelingen",
|
||||
@@ -164,7 +164,7 @@
|
||||
"i18n.movies": "Films",
|
||||
"i18n.partiallyavailable": "Deels beschikbaar",
|
||||
"i18n.pending": "In behandeling",
|
||||
"i18n.processing": "Verwerken",
|
||||
"i18n.processing": "Bezig met verwerken",
|
||||
"i18n.tvshows": "Series",
|
||||
"i18n.unavailable": "Niet beschikbaar",
|
||||
"pages.oops": "Oeps",
|
||||
@@ -187,14 +187,14 @@
|
||||
"components.Setup.tip": "Tip",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Test verbinding om hoofdmappen te laden",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test verbinding om kwaliteitsprofielen te laden",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Hoofdmappen laden…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Kwaliteitsprofielen laden…",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Bezig met laden van hoofdmappen…",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Ondersteuning krijgen",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Je moet een minimale beschikbaarheid selecteren",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Test verbinding om hoofdmappen te laden",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test verbinding om kwaliteitsprofielen te laden",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Hoofdmappen laden…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Kwaliteitsprofielen laden…",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Bezig met laden van hoofdmappen…",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Bezig met laden van kwaliteitsprofielen…",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Versiegegevens zijn momenteel niet beschikbaar.",
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Nieuwste",
|
||||
"components.Settings.SettingsAbout.Releases.currentversion": "Huidig",
|
||||
@@ -208,7 +208,7 @@
|
||||
"i18n.retry": "Opnieuw proberen",
|
||||
"i18n.requested": "Aangevraagd",
|
||||
"i18n.failed": "Mislukt",
|
||||
"i18n.deleting": "Verwijderen…",
|
||||
"i18n.deleting": "Bezig met verwijderen…",
|
||||
"i18n.close": "Sluiten",
|
||||
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.",
|
||||
"components.UserList.userdeleted": "Gebruiker succesvol verwijderd!",
|
||||
@@ -223,7 +223,7 @@
|
||||
"components.TvDetails.network": "{networkCount, plural, one {Netwerk} other {Netwerken}}",
|
||||
"components.TvDetails.firstAirDate": "Datum eerste uitzending",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.StatusChacker.reloadJellyseerr": "Herladen",
|
||||
"components.StatusChacker.reloadOverseerr": "Herladen",
|
||||
"components.StatusChacker.newversionavailable": "Toepassingsupdate",
|
||||
"components.StatusChacker.newversionDescription": "Jellyseerr is geüpdatet! Klik op de onderstaande knop om de pagina opnieuw te laden.",
|
||||
"components.Settings.toastSettingsSuccess": "Instellingen succesvol opgeslagen!",
|
||||
@@ -303,10 +303,10 @@
|
||||
"components.UserList.create": "Aanmaken",
|
||||
"components.UserList.createlocaluser": "Lokale gebruiker aanmaken",
|
||||
"components.UserList.usercreatedfailed": "Er ging iets mis bij het aanmaken van de gebruiker.",
|
||||
"components.UserList.creating": "Aanmaken…",
|
||||
"components.UserList.creating": "Bezig met aanmaken…",
|
||||
"components.UserList.validationpasswordminchars": "Wachtwoord is te kort; moet minimaal 8 tekens bevatten",
|
||||
"components.UserList.usercreatedsuccess": "Gebruiker succesvol aangemaakt!",
|
||||
"components.UserList.passwordinfodescription": "Stel een applicatie-URL in en schakel e-mailmeldingen in om automatische wachtwoordgeneratie mogelijk te maken.",
|
||||
"components.UserList.passwordinfodescription": "Configureer een applicatie-URL en schakel e-mailmeldingen in om automatische wachtwoordgeneratie mogelijk te maken.",
|
||||
"components.UserList.password": "Wachtwoord",
|
||||
"components.UserList.localuser": "Lokale gebruiker",
|
||||
"components.UserList.email": "E-mailadres",
|
||||
@@ -340,23 +340,23 @@
|
||||
"components.RequestModal.SearchByNameModal.notvdbiddescription": "We kunnen deze serie niet automatisch matchen. Selecteer hieronder de juiste match.",
|
||||
"components.Login.signinwithplex": "Plex-account gebruiken",
|
||||
"components.Login.signinheader": "Log in om verder te gaan",
|
||||
"components.Login.signingin": "Aanmelden…",
|
||||
"components.Login.signingin": "Bezig met inloggen…",
|
||||
"components.Login.signin": "Inloggen",
|
||||
"components.Settings.notificationAgentSettingsDescription": "Meldingsagenten configureren en inschakelen.",
|
||||
"components.PlexLoginButton.signinwithplex": "Inloggen",
|
||||
"components.PlexLoginButton.signingin": "Aanmelden…",
|
||||
"components.PlexLoginButton.signingin": "Bezig met inloggen…",
|
||||
"components.PermissionEdit.advancedrequest": "Geavanceerde aanvragen",
|
||||
"components.PermissionEdit.admin": "Beheerder",
|
||||
"components.UserList.userssaved": "Gebruikersrechten succesvol opgeslagen!",
|
||||
"components.Settings.toastPlexRefreshSuccess": "Serverlijst van Plex succesvol opgehaald!",
|
||||
"components.Settings.toastPlexRefresh": "Bezig met serverlijst ophalen van Plex…",
|
||||
"components.Settings.toastPlexConnecting": "Verbinden met Plex…",
|
||||
"components.Settings.toastPlexConnecting": "Bezig met verbinden met Plex-server…",
|
||||
"components.UserList.bulkedit": "Meerdere bewerken",
|
||||
"components.Settings.toastPlexRefreshFailure": "Kan serverlijst van Plex niet ophalen.",
|
||||
"components.Settings.toastPlexConnectingSuccess": "Succesvol verbonden met Plex-server!",
|
||||
"components.Settings.toastPlexConnectingFailure": "Kan geen verbinding maken met Plex.",
|
||||
"components.Settings.settingUpPlexDescription": "Om Plex in te stellen, kan je de gegevens handmatig invoeren of een server selecteren die is opgehaald van <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Druk op de knop rechts van de vervolgkeuzelijst om de lijst van beschikbare servers op te halen.",
|
||||
"components.Settings.serverpresetRefreshing": "Servers ophalen…",
|
||||
"components.Settings.serverpresetRefreshing": "Bezig met servers ophalen…",
|
||||
"components.Settings.serverpresetManualMessage": "Handmatige configuratie",
|
||||
"components.Settings.serverpresetLoad": "Klik op de knop om de beschikbare servers te laden",
|
||||
"components.Settings.serverpreset": "Server",
|
||||
@@ -431,7 +431,7 @@
|
||||
"components.RequestModal.AdvancedRequester.requestas": "Aanvragen als",
|
||||
"components.Discover.discover": "Ontdekken",
|
||||
"components.Settings.validationApplicationTitle": "Je moet een toepassingstitel opgeven",
|
||||
"components.AppDataWarning.dockerVolumeMissingDescription": "De volumekoppeling <code>{appDataPath}</code> is niet correct geconfigureerd. Alle gegevens zullen worden gewist wanneer de container wordt gestopt of opnieuw wordt gestart.",
|
||||
"components.AppDataWarning.dockerVolumeMissingDescription": "De volumekoppeling <code>{appDataPath}</code> was niet correct geconfigureerd. Alle gegevens zullen worden gewist wanneer de container wordt gestopt of opnieuw wordt gestart.",
|
||||
"components.Settings.validationApplicationUrlTrailingSlash": "URL mag niet eindigen op een schuine streep",
|
||||
"components.Settings.validationApplicationUrl": "Je moet een geldige URL opgeven",
|
||||
"components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "URL mag niet eindigen op een schuine streep",
|
||||
@@ -520,7 +520,7 @@
|
||||
"components.Layout.UserDropdown.myprofile": "Profiel",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Je moet een geldige gebruikers-ID opgeven",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Het <FindDiscordIdLink>meercijferige ID-nummer</FindDiscordIdLink> van je gebruikersaccount",
|
||||
"components.CollectionDetails.requestcollection4k": "Collectie aanvragen in 4K",
|
||||
"components.CollectionDetails.requestcollection4k": "Collectie in 4K aanvragen",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Inhoud filteren op regionale beschikbaarheid",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Regio van Ontdekken",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Inhoud filteren op oorspronkelijke taal",
|
||||
@@ -546,7 +546,7 @@
|
||||
"components.Settings.SettingsJobsCache.download-sync-reset": "Reset download sync",
|
||||
"components.Settings.SettingsJobsCache.download-sync": "Synchronisatie downloads",
|
||||
"components.TvDetails.seasons": "{seasonCount, plural, one {# seizoen} other {# seizoenen}}",
|
||||
"i18n.loading": "Laden…",
|
||||
"i18n.loading": "Bezig met laden…",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Je moet een geldige chat-ID opgeven",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Een chat starten</TelegramBotLink>, <GetIdBotLink>@get_id_bot</GetIdBotLink> toevoegen en de opdracht <code>/my_id</code> geven",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chat-ID",
|
||||
@@ -558,14 +558,14 @@
|
||||
"components.Discover.DiscoverNetwork.networkSeries": "Series van {network}",
|
||||
"components.Discover.DiscoverMovieGenre.genreMovies": "{genre} films",
|
||||
"components.Setup.scanbackground": "Het scannen wordt op de achtergrond uitgevoerd. Je kunt in de tussentijd doorgaan met het installatieproces.",
|
||||
"components.Settings.scanning": "Synchroniseren…",
|
||||
"components.Settings.scanning": "Bezig met synchroniseren…",
|
||||
"components.Settings.scan": "Bibliotheken synchroniseren",
|
||||
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr-scan",
|
||||
"components.Settings.SettingsJobsCache.radarr-scan": "Radarr-scan",
|
||||
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex recent toegevoegde scan",
|
||||
"components.Settings.SettingsJobsCache.plex-full-scan": "Plex volledige bibliotheekscan",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "volledige bibliotheekscan Jellyfin",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Scan van 'onlangs toegevoegd' in Jellyfin",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Jellyfin recent toegevoegde scan",
|
||||
"components.Settings.Notifications.validationUrl": "Je moet een geldige URL opgeven",
|
||||
"components.Settings.Notifications.botAvatarUrl": "URL bot-avatar",
|
||||
"components.RequestList.RequestItem.requested": "Aangevraagd",
|
||||
@@ -670,27 +670,27 @@
|
||||
"components.QuotaSelector.unlimited": "Onbeperkt",
|
||||
"i18n.view": "Bekijken",
|
||||
"i18n.tvshow": "Serie",
|
||||
"i18n.testing": "Testen…",
|
||||
"i18n.testing": "Bezig met testen…",
|
||||
"i18n.test": "Test",
|
||||
"i18n.status": "Status",
|
||||
"i18n.showingresults": "<strong>{from}</strong> tot <strong>{to}</strong> van de <strong>{total}</strong> resultaten worden weergegeven",
|
||||
"i18n.saving": "Opslaan…",
|
||||
"i18n.saving": "Bezig met opslaan…",
|
||||
"i18n.save": "Wijzigingen opslaan",
|
||||
"i18n.resultsperpage": "{pageSize} resultaten per pagina weergeven",
|
||||
"i18n.requesting": "Aanvragen…",
|
||||
"i18n.requesting": "Bezig met aanvragen…",
|
||||
"i18n.request4k": "Aanvragen in 4K",
|
||||
"i18n.previous": "Vorige",
|
||||
"i18n.notrequested": "Niet aangevraagd",
|
||||
"i18n.noresults": "Geen resultaten.",
|
||||
"i18n.next": "Volgende",
|
||||
"i18n.movie": "Film",
|
||||
"i18n.canceling": "Annuleren…",
|
||||
"i18n.canceling": "Bezig met annuleren…",
|
||||
"i18n.back": "Terug",
|
||||
"i18n.areyousure": "Weet je het zeker?",
|
||||
"i18n.all": "Alle",
|
||||
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Deze gebruiker heeft nog minstens <strong>{seasons}</strong> {seasons, plural, one {seizoensverzoek} other {seizoensverzoeken}} nodig om deze serie aan te vragen.",
|
||||
"components.TvDetails.originaltitle": "Oorspronkelijke titel",
|
||||
"components.MovieDetails.originaltitle": "Oorspronkelijke titel",
|
||||
"components.TvDetails.originaltitle": "Originele titel",
|
||||
"components.MovieDetails.originaltitle": "Originele titel",
|
||||
"components.LanguageSelector.originalLanguageDefault": "Alle talen",
|
||||
"components.LanguageSelector.languageServerDefault": "Standaard ({language})",
|
||||
"components.Settings.SonarrModal.testFirstTags": "Test de verbinding om labels te laden",
|
||||
@@ -733,9 +733,9 @@
|
||||
"components.RequestModal.pendingapproval": "Je verzoek is in afwachting van goedkeuring.",
|
||||
"components.RequestList.RequestItem.cancelRequest": "Verzoek annuleren",
|
||||
"components.NotificationTypeSelector.notificationTypes": "Meldingtypes",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Er is voor jouw account momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord om in te kunnen loggen als een \"lokale gebruiker\" met je e-mailadres.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Er is voor jouw account momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord om in te kunnen loggen als een \"lokale gebruiker\" met uw e-mailadres.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Deze gebruikersaccount heeft momenteel geen wachtwoord ingesteld. Configureer hieronder een wachtwoord zodat deze account in staat is om zich aan te melden als een \"lokale gebruiker\".",
|
||||
"components.Settings.serviceSettingsDescription": "Stel je {serverType}-server(s) hieronder in. Je kunt meerdere {serverType}-servers verbinden, maar slechts twee ervan kunnen als standaard worden gemarkeerd (één niet-4K en één 4K). Beheerders kunnen vóór goedkeuring de server aanpassen die voor nieuwe aanvragen gebruikt wordt.",
|
||||
"components.Settings.serviceSettingsDescription": "Configureer je {serverType} server(s) hieronder. Je kunt meerdere {serverType} servers verbinden, maar slechts twee ervan kunnen als standaard worden gemarkeerd (één niet-4K en één 4K). Beheerders kunnen vóór de goedkeuring de server die gebruikt wordt om nieuwe aanvragen te verwerken aanpassen.",
|
||||
"components.Settings.noDefaultServer": "Ten minste één {serverType} server moet als standaard worden gemarkeerd om {mediaType}verzoeken te kunnen verwerken.",
|
||||
"components.Settings.noDefaultNon4kServer": "Als je slechts één enkele {serverType} server hebt voor zowel niet-4K als 4K-inhoud (of als je alleen 4K-inhoud downloadt), dan moet je {serverType} server <strong>NIET</strong> aangeduid worden als een 4K-server.",
|
||||
"components.Settings.mediaTypeSeries": "serie",
|
||||
@@ -747,7 +747,7 @@
|
||||
"components.Layout.VersionStatus.outofdate": "Verouderd",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} achter",
|
||||
"components.UserList.autogeneratepasswordTip": "Een door de server gegenereerd wachtwoord naar de gebruiker e-mailen",
|
||||
"i18n.retrying": "Opnieuw proberen…",
|
||||
"i18n.retrying": "Bezig met opnieuw proberen…",
|
||||
"components.Settings.serverSecure": "veilig",
|
||||
"components.UserList.usercreatedfailedexisting": "Het opgegeven e-mailadres wordt al gebruikt door een andere gebruiker.",
|
||||
"components.RequestModal.edit": "Verzoek bewerken",
|
||||
@@ -847,7 +847,7 @@
|
||||
"components.NotificationTypeSelector.usermediadeclinedDescription": "Een melding ontvangen wanneer je mediaverzoeken worden geweigerd.",
|
||||
"components.NotificationTypeSelector.usermediaavailableDescription": "Een melding ontvangen wanneer je mediaverzoeken beschikbaar zijn.",
|
||||
"components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Een melding ontvangen wanneer andere gebruikers nieuwe mediaverzoeken indienen die automatisch worden goedgekeurd.",
|
||||
"components.Settings.SettingsAbout.betawarning": "Dit is BETA-software. Functies kunnen kapot en/of instabiel zijn. Meld eventuele problemen op GitHub!",
|
||||
"components.Settings.SettingsAbout.betawarning": "Dit is BETA software. Functies kunnen kapot en/of instabiel zijn. Meld eventuele problemen op GitHub!",
|
||||
"components.Layout.LanguagePicker.displaylanguage": "Weergavetaal",
|
||||
"components.MovieDetails.showmore": "Meer tonen",
|
||||
"components.MovieDetails.showless": "Minder tonen",
|
||||
@@ -971,7 +971,7 @@
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Je moet een geldige gebruikers- of groepssleutel opgeven",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Instellingen voor Pushbullet-meldingen succesvol opgeslagen!",
|
||||
"components.IssueDetails.playonplex": "Afspelen op {mediaServerName}",
|
||||
"components.IssueDetails.play4konplex": "Afspelen op {mediaServerName} in 4K",
|
||||
"components.IssueDetails.play4konplex": "Afspelen in 4K op {mediaServerName}",
|
||||
"components.IssueDetails.openin4karr": "Openen in 4K {arr}",
|
||||
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {aflevering} other {afleveringen}}",
|
||||
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {seizoen} other {seizoenen}}",
|
||||
@@ -982,7 +982,7 @@
|
||||
"components.NotificationTypeSelector.adminissuereopenedDescription": "Ontvang een melding wanneer problemen door andere gebruikers opnieuw worden ingediend.",
|
||||
"components.NotificationTypeSelector.issuereopenedDescription": "Stuur meldingen wanneer problemen opnieuw worden ingediend.",
|
||||
"components.NotificationTypeSelector.userissuereopenedDescription": "Ontvang een bericht wanneer problemen die jij hebt gemeld, opnieuw worden ingediend.",
|
||||
"components.RequestModal.requestseasons4k": "{seasonCount} {seasonCount, plural, one {seizoen} other {seizoenen}} aanvragen in 4K",
|
||||
"components.RequestModal.requestseasons4k": "{seasonCount} {seasonCount, plural, one {seizoen} other {seizoenen}} in 4K aanvragen",
|
||||
"components.RequestModal.requestmovies": "{count} {count, plural, one {film} other {films}} aanvragen",
|
||||
"components.RequestModal.selectmovies": "Film(s) selecteren",
|
||||
"components.MovieDetails.productioncountries": "Productie{countryCount, plural, one {land} other {landen}}",
|
||||
@@ -998,7 +998,7 @@
|
||||
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Agent inschakelen",
|
||||
"components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Instellingen voor meldingen Gotify succesvol opgeslagen!",
|
||||
"components.Settings.Notifications.NotificationsGotify.token": "Toepassingstoken",
|
||||
"i18n.importing": "Importeren…",
|
||||
"i18n.importing": "Bezig met importeren…",
|
||||
"components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Instellingen voor meldingen Gotify niet opgeslagen.",
|
||||
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Testmelding Gotify niet verzonden.",
|
||||
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "Testmelding Gotify verzonden!",
|
||||
@@ -1012,7 +1012,7 @@
|
||||
"components.UserList.newplexsigninenabled": "De instelling <strong>Nieuwe Plex-aanmelding inschakelen</strong> is momenteel ingeschakeld. Plex-gebruikers met bibliotheektoegang hoeven niet te worden geïmporteerd om in te loggen.",
|
||||
"components.ManageSlideOver.manageModalAdvanced": "Geavanceerd",
|
||||
"components.ManageSlideOver.alltime": "Altijd",
|
||||
"components.ManageSlideOver.markallseasons4kavailable": "Alle seizoenen markeren als beschikbaar in 4K",
|
||||
"components.ManageSlideOver.markallseasons4kavailable": "Alle seizoenen als beschikbaar in 4K markeren",
|
||||
"components.ManageSlideOver.opentautulli": "In Tautulli openen",
|
||||
"components.ManageSlideOver.pastdays": "Afgelopen {days, number} dagen",
|
||||
"components.ManageSlideOver.playedby": "Afgespeeld door",
|
||||
@@ -1046,8 +1046,8 @@
|
||||
"components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.MovieDetails.digitalrelease": "Digitale release",
|
||||
"i18n.restartRequired": "Opnieuw opstarten vereist",
|
||||
"components.PermissionEdit.viewrecentDescription": "Toestemming geven om de lijst met onlangs toegevoegde media weer te geven.",
|
||||
"components.PermissionEdit.viewrecent": "Onlangs toegevoegd weergeven",
|
||||
"components.PermissionEdit.viewrecentDescription": "Toestemming geven om de lijst met recent toegevoegde media te bekijken.",
|
||||
"components.PermissionEdit.viewrecent": "Recent toegevoegd bekijken",
|
||||
"components.Settings.deleteServer": "{serverType}-server verwijderen",
|
||||
"components.StatusChecker.appUpdated": "{applicationTitle} bijgewerkt",
|
||||
"components.RequestList.RequestItem.tmdbid": "TMDB ID",
|
||||
@@ -1072,8 +1072,8 @@
|
||||
"components.TvDetails.seasonnumber": "Seizoen {seasonNumber}",
|
||||
"components.TvDetails.Season.somethingwentwrong": "Er ging iets mis bij het ophalen van de seizoensgegevens.",
|
||||
"components.TvDetails.seasonstitle": "Seizoenen",
|
||||
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Jouw kijklijst",
|
||||
"components.Discover.plexwatchlist": "Jouw kijklijst",
|
||||
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Je Plex-kijklijst",
|
||||
"components.Discover.plexwatchlist": "Je Plex Kijklijst",
|
||||
"components.MovieDetails.physicalrelease": "Fysieke release",
|
||||
"components.PermissionEdit.autorequest": "Automatisch aanvragen",
|
||||
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Kijklijst synchroniseren",
|
||||
@@ -1099,7 +1099,7 @@
|
||||
"components.TvDetails.manageseries": "Serie beheren",
|
||||
"components.MovieDetails.managemovie": "Film beheren",
|
||||
"components.MovieDetails.reportissue": "Probleem melden",
|
||||
"components.PermissionEdit.autorequestMoviesDescription": "Toestemming geven om niet-4K films in je Plex-kijklijst automatisch aan te vragen.",
|
||||
"components.PermissionEdit.autorequestMoviesDescription": "Toestemming geven om niet-4K films in je Plex Kijklijst automatisch aan te vragen.",
|
||||
"components.PermissionEdit.autorequestSeries": "Series automatisch aanvragen",
|
||||
"components.PermissionEdit.autorequestMovies": "Films automatisch aanvragen",
|
||||
"components.Settings.experimentalTooltip": "Deze instelling inschakelen, kan leiden tot onverwacht gedrag van de toepassing",
|
||||
@@ -1152,8 +1152,8 @@
|
||||
"components.Discover.DiscoverSliderEdit.remove": "Verwijderen",
|
||||
"components.Discover.resetfailed": "Er is iets fout gegaan bij het resetten van de instellingen van Ontdekken.",
|
||||
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Jouw kijklijst",
|
||||
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Onlangs toegevoegd",
|
||||
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Je Plex Kijklijst",
|
||||
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Recent toegevoegd",
|
||||
"components.Discover.networks": "Netwerken",
|
||||
"components.Discover.CreateSlider.searchStudios": "Studio's zoeken…",
|
||||
"components.Discover.CreateSlider.starttyping": "Begin met typen om te zoeken.",
|
||||
@@ -1184,8 +1184,8 @@
|
||||
"components.Discover.DiscoverMovies.sortPopularityAsc": "Populariteit oplopend",
|
||||
"components.Discover.DiscoverMovies.sortPopularityDesc": "Populariteit aflopend",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Releasedatum oplopend",
|
||||
"components.Discover.DiscoverMovies.sortTitleAsc": "Titel oplopend (A-Z)",
|
||||
"components.Discover.DiscoverMovies.sortTitleDesc": "Titel aflopend (Z-A)",
|
||||
"components.Discover.DiscoverMovies.sortTitleAsc": "Titel (A-Z) oplopend",
|
||||
"components.Discover.DiscoverMovies.sortTitleDesc": "Titel (Z-A) aflopend",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMDB-beoordeling oplopend",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "TMDB-beoordeling aflopend",
|
||||
"components.Discover.DiscoverSliderEdit.deletefail": "Slider verwijderen mislukt.",
|
||||
@@ -1202,7 +1202,7 @@
|
||||
"components.Discover.FilterSlideover.from": "Van",
|
||||
"components.Discover.FilterSlideover.genres": "Genres",
|
||||
"components.Discover.FilterSlideover.keywords": "Trefwoorden",
|
||||
"components.Discover.FilterSlideover.originalLanguage": "Oorspronkelijke taal",
|
||||
"components.Discover.FilterSlideover.originalLanguage": "Originele taal",
|
||||
"components.Discover.FilterSlideover.ratingText": "Beoordelingen tussen {minValue} en {maxValue}",
|
||||
"components.Discover.FilterSlideover.releaseDate": "Releasedatum",
|
||||
"components.Discover.FilterSlideover.runtime": "Duur",
|
||||
@@ -1261,88 +1261,5 @@
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Elke {jobScheduleSeconds, plural, one {seconde} other {{jobScheduleSeconds} seconden}}",
|
||||
"components.Settings.SettingsJobsCache.availability-sync": "Synchronisatie van mediabeschikbaarheid",
|
||||
"components.Discover.tmdbmoviestreamingservices": "Streamingdiensten voor films TMDB",
|
||||
"components.Discover.tmdbtvstreamingservices": "Streamingdiensten voor series TMDB",
|
||||
"components.Login.validationhostrequired": "{mediaServerName}-URL vereist",
|
||||
"components.Layout.UserWarnings.emailInvalid": "E-mailadres is ongeldig.",
|
||||
"components.Login.description": "Aangezien dit de eerste keer is dat je je aanmeldt bij {applicationName}, dien je een geldig e-mailadres op te geven.",
|
||||
"components.Login.saving": "Toevoegen…",
|
||||
"components.ManageSlideOver.removearr": "Verwijderen van {arr}",
|
||||
"components.Settings.RadarrModal.tagRequests": "Aanvragen taggen",
|
||||
"components.MovieDetails.openradarr4k": "Film openen in 4K-Radarr",
|
||||
"components.Settings.RadarrModal.tagRequestsInfo": "Automatisch een extra label toevoegen met de gebruikers-id en weergavenaam van de aanvrager",
|
||||
"components.Settings.SonarrModal.animeSeriesType": "Serietype anime",
|
||||
"components.Settings.SonarrModal.tagRequestsInfo": "Automatisch een extra label toevoegen met de gebruikers-id en weergavenaam van de aanvrager",
|
||||
"components.Settings.internalUrl": "Interne URL",
|
||||
"components.Settings.jellyfinsettings": "{mediaServerName}-instellingen",
|
||||
"components.Settings.jellyfinlibrariesDescription": "De {mediaServerName}-bibliotheken die op titels worden gescand. Klik op onderstaande knop als er geen bibliotheken in de lijst staan.",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Normaliter wordt dit eenmaal per 24 uur uitgevoerd. Jellyseerr zal de lijst met onlangs toegevoegde media op je {mediaServerName}-server vaker controleren. Als dit de eerste keer is dat je Jellyseerr instelt, wordt aanbevolen eenmalig een handmatige, volledige bibliotheekscan uit te voeren!",
|
||||
"components.Settings.save": "Wijzigingen opslaan",
|
||||
"components.Settings.syncJellyfin": "Bibliotheken synchoniseren",
|
||||
"components.TvDetails.play": "Afspelen op {mediaServerName}",
|
||||
"components.Discover.FilterSlideover.tmdbuservotecount": "Aantal gebruikersstemmen TMDB",
|
||||
"components.Login.save": "Toevoegen",
|
||||
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Hiermee wordt deze {mediaType} onomkeerbaar verwijderd van {arr}, inclusief alle bestanden.",
|
||||
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Apparaatstandaard",
|
||||
"components.Settings.Notifications.userEmailRequired": "Gebruikerse-mail vereisen",
|
||||
"components.Settings.SettingsAbout.supportjellyseerr": "Jellyseerr ondersteunen",
|
||||
"components.Settings.SonarrModal.seriesType": "Serietype",
|
||||
"components.Settings.jellyfinSettings": "{mediaServerName}-instellingen",
|
||||
"components.Setup.configuremediaserver": "Mediaserver instellen",
|
||||
"components.TvDetails.play4k": "Afspelen op {mediaServerName} in 4K",
|
||||
"components.UserList.mediaServerUser": "{mediaServerName}-gebruiker",
|
||||
"components.UserList.noJellyfinuserstoimport": "Er zijn geen {mediaServerName}-gebruikers om te importeren.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Apparaatstandaard",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Meldingsgeluid",
|
||||
"components.Login.signinwithjellyfin": "{mediaServerName}-account gebruiken",
|
||||
"components.Discover.FilterSlideover.voteCount": "Aantal stemmen tussen {minValue} en {maxValue}",
|
||||
"components.Layout.UserWarnings.emailRequired": "Een e-mailadres is vereist.",
|
||||
"components.Layout.UserWarnings.passwordRequired": "Een wachtwoord is vereist.",
|
||||
"components.Login.credentialerror": "Gebruikersnaam of wachtwoord is onjuist.",
|
||||
"components.Login.emailtooltip": "Het adres hoeft niet gelieerd te zijn aan je {mediaServerName}-instantie.",
|
||||
"components.Login.host": "{mediaServerName}-URL",
|
||||
"components.Login.initialsignin": "Verbinden",
|
||||
"components.Login.initialsigningin": "Verbinden…",
|
||||
"components.Login.title": "E-mail toevoegen",
|
||||
"components.Login.username": "Gebruikersnaam",
|
||||
"components.Login.validationEmailRequired": "Je moet een e-mailadres opgeven",
|
||||
"components.Login.validationEmailFormat": "Ongeldig e-mailadres",
|
||||
"components.Login.validationemailformat": "Geldig e-mailadres vereist",
|
||||
"components.Login.validationhostformat": "Geldige URL vereist",
|
||||
"components.Login.validationusernamerequired": "Gebruikersnaam vereist",
|
||||
"components.ManageSlideOver.removearr4k": "Verwijderen van 4K-{arr}",
|
||||
"components.MovieDetails.downloadstatus": "Downloadstatus",
|
||||
"components.MovieDetails.imdbuserscore": "Gebruikersbeoordeling IMDB",
|
||||
"components.MovieDetails.openradarr": "Film openen in Radarr",
|
||||
"components.MovieDetails.play": "Afspelen op {mediaServerName}",
|
||||
"components.MovieDetails.play4k": "Afspelen op {mediaServerName} in 4K",
|
||||
"components.Settings.Notifications.NotificationsPushover.sound": "Meldingsgeluid",
|
||||
"components.Settings.SonarrModal.tagRequests": "Aanvragen taggen",
|
||||
"components.Settings.jellyfinSettingsFailure": "Er is iets misgegaan bij het opslaan van de {mediaServerName}-instellingen.",
|
||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName}-instellingen opgeslagen!",
|
||||
"components.Settings.jellyfinlibraries": "{mediaServerName}-bibliotheken",
|
||||
"components.Settings.manualscanJellyfin": "Handmatige bibliotheekscan",
|
||||
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
||||
"components.Settings.saving": "Opslaan…",
|
||||
"components.Settings.syncing": "Synchroniseren",
|
||||
"components.Settings.timeout": "Time-out",
|
||||
"components.Setup.signin": "Aanmelden",
|
||||
"components.Setup.signinWithJellyfin": "{mediaServerName}-account gebruiken",
|
||||
"components.TitleCard.addToWatchList": "Toevoegen aan kijklijst",
|
||||
"components.TitleCard.watchlistError": "Er is iets misgegaan. Probeer het opnieuw.",
|
||||
"components.UserList.importfromJellyfin": "{mediaServerName}-gebruikers importeren",
|
||||
"components.UserList.importfromJellyfinerror": "Er is iets misgegaan bij het importeren van {mediaServerName}-gebruikers.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "{mediaServerName}-gebruiker",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Wijzigingen opslaan",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Opslaan…",
|
||||
"i18n.collection": "Collectie",
|
||||
"components.UserProfile.localWatchlist": "Kijklijst van {username}",
|
||||
"components.Setup.signinWithPlex": "Plex-account gebruiken",
|
||||
"components.Settings.jellyfinSettingsDescription": "Optioneel, configureer de interne en externe eindpunten voor uw {mediaServerName} server. In de meeste gevallen verschilt de externe URL met de interne URL. Een aangepaste wachtwoord reset URL kan ook gebruikt worden voor de {mediaServerName} login, voor het geval dat u doorverwezen wilt worden naar een andere wachtwoord reset pagina.",
|
||||
"components.Settings.jellyfinsettingsDescription": "Configureer de instellingen voor uw {mediaServerName} server. {mediaServerName} scanned uw {mediaServerName} bibliotheken om te zien welke content beschikbaar is.",
|
||||
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> Is succesvol verwijderd van de kijklijst!",
|
||||
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> succesvol toegevoegd aan de kijklijst!",
|
||||
"components.TitleCard.watchlistCancel": "kijklijst voor <strong>{title}</strong> is geannuleerd.",
|
||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} succesvol geimporteerd!",
|
||||
"components.UserList.newJellyfinsigninenabled": "De <strong>Gebruik Nieuwe {mediaServerName} Login</strong> instelling staat momenteel aan. {mediaServerName} gebruikers met toegang tot de bibliotheek, hoeven niet geïmporteerd te worden om in te kunnen loggen."
|
||||
"components.Discover.tmdbtvstreamingservices": "Streamingdiensten voor series TMDB"
|
||||
}
|
||||
|
||||
@@ -1060,75 +1060,5 @@
|
||||
"components.Layout.UserDropdown.requests": "Prośby",
|
||||
"components.MovieDetails.rtaudiencescore": "Ocena Rotten Tomatoes",
|
||||
"components.MovieDetails.rtcriticsscore": "Tomatometer Rotten Tomatoes",
|
||||
"components.MovieDetails.tmdbuserscore": "Ocena użytkowników TMDB",
|
||||
"components.Discover.CreateSlider.addSlider": "Dodaj suwak",
|
||||
"components.Discover.CreateSlider.addcustomslider": "Utwórz niestandardowy suwak",
|
||||
"components.Discover.CreateSlider.addfail": "Nie udało się utworzyć nowego suwaka.",
|
||||
"components.Discover.CreateSlider.addsuccess": "Stworzony nowy suwak i zapisano dostosowywania odkrywania.",
|
||||
"components.Discover.CreateSlider.editSlider": "Edytuj suwak",
|
||||
"components.Discover.CreateSlider.editfail": "Nie udało się edytować suwaka.",
|
||||
"components.Discover.CreateSlider.needresults": "Musisz mieć przynajmniej 1 wynik.",
|
||||
"components.Discover.CreateSlider.nooptions": "Brak wyników.",
|
||||
"components.Discover.CreateSlider.providetmdbgenreid": "Podaj ID gatunku TMDB",
|
||||
"components.Discover.CreateSlider.providetmdbnetwork": "Podaj ID stacji TMDB",
|
||||
"components.Discover.CreateSlider.providetmdbsearch": "Podaj zapytanie wyszukiwania",
|
||||
"components.Discover.CreateSlider.providetmdbstudio": "Podaj ID studia TMDB",
|
||||
"components.Discover.CreateSlider.searchGenres": "Szukaj gatunków…",
|
||||
"components.Discover.CreateSlider.searchStudios": "Wyszukaj studia…",
|
||||
"components.Discover.CreateSlider.slidernameplaceholder": "Nazwa suwaka",
|
||||
"components.Discover.CreateSlider.starttyping": "Zacznij pisać aby wyszukać.",
|
||||
"components.Discover.CreateSlider.validationDatarequired": "Należy wprowadzić wartość danych.",
|
||||
"components.Discover.CreateSlider.validationTitlerequired": "Należy podać tytuł.",
|
||||
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Filmy: {keywordTitle}",
|
||||
"components.Discover.DiscoverMovies.discovermovies": "Filmy",
|
||||
"components.Discover.DiscoverMovies.sortPopularityAsc": "Popularność rosnąco",
|
||||
"components.Discover.DiscoverMovies.sortPopularityDesc": "Popularność malejąco",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Data wydania malejąco",
|
||||
"components.Discover.DiscoverMovies.sortTitleAsc": "Tytuł (A-Z) rosnąco",
|
||||
"components.Discover.DiscoverMovies.sortTitleDesc": "Tytuł (Z-A) malejąco",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Ocena TMDB rosnąco",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Ocena TMDB malejąco",
|
||||
"components.Discover.DiscoverSliderEdit.deletefail": "Nie udało się usunąć suwaka.",
|
||||
"components.Discover.DiscoverSliderEdit.deletesuccess": "Pomyślnie usunięto suwak.",
|
||||
"components.Discover.DiscoverSliderEdit.enable": "Przełącz widoczność",
|
||||
"components.Discover.DiscoverSliderEdit.remove": "Usuń",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Data premiery rosnąco",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Data premiery malejąco",
|
||||
"components.Discover.DiscoverTv.sortPopularityAsc": "Popularność rosnąco",
|
||||
"components.Discover.DiscoverTv.sortPopularityDesc": "Popularność malejąco",
|
||||
"components.Discover.DiscoverTv.sortTitleAsc": "Tytuł (A-Z) rosnąco",
|
||||
"components.Discover.DiscoverTv.sortTitleDesc": "Tytuł (Z-A) malejąco",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "Ocena TMDB malejąco",
|
||||
"components.Discover.FilterSlideover.clearfilters": "Wyczyść aktywne filtry",
|
||||
"components.Discover.FilterSlideover.filters": "Filtry",
|
||||
"components.Discover.FilterSlideover.firstAirDate": "Data pierwszego wyemitowania",
|
||||
"components.Discover.FilterSlideover.from": "Od",
|
||||
"components.Discover.FilterSlideover.genres": "Gatunki",
|
||||
"components.Discover.FilterSlideover.keywords": "Słowa kluczowe",
|
||||
"components.Discover.FilterSlideover.ratingText": "Oceny między {minValue} a {maxValue}",
|
||||
"components.Discover.CreateSlider.providetmdbkeywordid": "Podaj ID słowa kluczowego TMDB",
|
||||
"components.Discover.CreateSlider.searchKeywords": "Wyszukaj słowa kluczowe…",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Data wydania rosnąco",
|
||||
"components.Discover.DiscoverTv.discovertv": "Seriale",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB rosnąco",
|
||||
"components.Discover.DiscoverTvKeyword.keywordSeries": "Seriale: {keywordTitle}",
|
||||
"components.Discover.FilterSlideover.originalLanguage": "Oryginalny język",
|
||||
"components.Discover.FilterSlideover.releaseDate": "Data wydania",
|
||||
"components.Discover.FilterSlideover.runtime": "Długość",
|
||||
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minut trwania",
|
||||
"components.Discover.FilterSlideover.streamingservices": "Serwisy streamingowe",
|
||||
"components.Discover.FilterSlideover.studio": "Studio",
|
||||
"components.Discover.FilterSlideover.tmdbuservotecount": "Liczba głosów użytkowników TMDB",
|
||||
"components.Discover.FilterSlideover.to": "Do",
|
||||
"components.Discover.FilterSlideover.voteCount": "Liczba głosów między {minValue} a {maxValue}",
|
||||
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Twoja lista obserwowanych Plex",
|
||||
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Niedawno dodane",
|
||||
"components.Discover.createnewslider": "Dodaj nowy suwak",
|
||||
"components.Discover.customizediscover": "Dostosuj Odkryj",
|
||||
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Media dodane do twojej <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink> zostaną wyświetlone tutaj.",
|
||||
"components.Discover.emptywatchlist": "Media dodane do twojej <PlexWatchlistSupportLink>listy obserwowanych Plex</PlexWatchlistSupportLink> zostaną wyświetlone tutaj.",
|
||||
"components.Discover.tmdbstudio": "Studio TMDB",
|
||||
"components.Discover.tmdbtvkeyword": "Słowo kluczowe serialu TMDB",
|
||||
"components.Discover.tmdbtvgenre": "Gatunek serialu TMDB",
|
||||
"components.Discover.tmdbsearch": "Wyszukiwanie TMDB"
|
||||
"components.MovieDetails.tmdbuserscore": "Ocena użytkowników TMDB"
|
||||
}
|
||||
|
||||
@@ -1256,36 +1256,5 @@
|
||||
"components.Discover.tmdbmoviestreamingservices": "Serviços de Streaming de Filmes do TMDB",
|
||||
"components.Discover.FilterSlideover.tmdbuservotecount": "Qtd de Votos de Usuários TMDB",
|
||||
"components.Discover.FilterSlideover.voteCount": "Qtd the votos entre {minValue} e {maxValue}",
|
||||
"components.Settings.RadarrModal.tagRequestsInfo": "Adicione automaticamente uma tag extra com o ID de usuário e o nome de exibição do solicitante",
|
||||
"components.Layout.UserWarnings.emailRequired": "Um endereço de e-mail é necessário.",
|
||||
"components.Login.credentialerror": "O nome de usuário ou senha está incorreto.",
|
||||
"components.Login.description": "Já que é sua primeira vez entrando em {applicationName}, você precisa adicionar um e-mail válido.",
|
||||
"components.Login.host": "URL de {mediaServerName}",
|
||||
"components.Login.initialsignin": "Conectar",
|
||||
"components.Login.initialsigningin": "Conectando…",
|
||||
"components.Login.save": "Adicionar",
|
||||
"components.Login.saving": "Adicionando…",
|
||||
"components.Login.signinwithjellyfin": "Use sua conta de {mediaServerName}",
|
||||
"components.Login.title": "Adicionar E-Mail",
|
||||
"components.Login.username": "Nome de usuário",
|
||||
"components.Login.validationEmailFormat": "E-mail inválido",
|
||||
"components.Login.validationEmailRequired": "Você precisa providenciar um e-mail",
|
||||
"components.Login.validationemailformat": "E-mail válido necessário",
|
||||
"components.Login.validationhostformat": "URL válido necessário",
|
||||
"components.Login.validationusernamerequired": "Nome de usuário necessário",
|
||||
"components.ManageSlideOver.removearr": "Remover de {arr}",
|
||||
"components.ManageSlideOver.removearr4k": "Remover de {arr} 4K",
|
||||
"components.MovieDetails.downloadstatus": "Status de download",
|
||||
"components.MovieDetails.imdbuserscore": "Avaliação de usuário no IMDB",
|
||||
"components.MovieDetails.openradarr": "Abrir filme no Radarr",
|
||||
"components.Settings.Notifications.NotificationsPushover.sound": "Som de notificação",
|
||||
"components.Login.emailtooltip": "Endereço não precisa ser associado à sua instância de {mediaServerName}.",
|
||||
"components.Layout.UserWarnings.emailInvalid": "Endereço de e-mail inválido.",
|
||||
"components.Layout.UserWarnings.passwordRequired": "Uma senha é necessária.",
|
||||
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Padrão do dispositivo",
|
||||
"components.Login.validationhostrequired": "URL de {mediaServerName} necessário",
|
||||
"components.MovieDetails.openradarr4k": "Abrir filme em Radarr 4K",
|
||||
"components.MovieDetails.play": "Reproduzir em {mediaServerName}",
|
||||
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Isto irá remover este {mediaType} de {arr}, incluindo todos os arquivos.",
|
||||
"components.MovieDetails.play4k": "Reproduzir em 4K em {mediaServerName}"
|
||||
"components.Settings.RadarrModal.tagRequestsInfo": "Adicione automaticamente uma tag extra com o ID de usuário e o nome de exibição do solicitante"
|
||||
}
|
||||
|
||||