Compare commits
28 Commits
preview-bl
...
preview-tm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74b6d793a3 | ||
|
|
f773e0fb2a | ||
|
|
767a24164d | ||
|
|
8394eb5ad4 | ||
|
|
b8425d6388 | ||
|
|
ebb7f00305 | ||
|
|
418d51590d | ||
|
|
a6dd4a8fed | ||
|
|
4d1163c343 | ||
|
|
b085e12ff9 | ||
|
|
33e7a153aa | ||
|
|
9891a7577c | ||
|
|
077e355c77 | ||
|
|
21ab20bba9 | ||
|
|
cdfb30ea16 | ||
|
|
771ecdf781 | ||
|
|
863b675c77 | ||
|
|
5b998bef82 | ||
|
|
0113612ced | ||
|
|
f8c9689745 | ||
|
|
af8d6b475c | ||
|
|
dcc13080bc | ||
|
|
e97a13e1e4 | ||
|
|
1de518d915 | ||
|
|
4e44282387 | ||
|
|
67bd639a43 | ||
|
|
ada467ecf4 | ||
|
|
9cc6930fed |
@@ -702,6 +702,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RankWeis",
|
||||
"name": "RankWeis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/733691?v=4",
|
||||
"profile": "https://github.com/RankWeis",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
8
.github/workflows/cypress.yml
vendored
8
.github/workflows/cypress.yml
vendored
@@ -36,3 +36,11 @@ jobs:
|
||||
# Fix test titles in cypress dashboard
|
||||
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
||||
- name: Upload video files
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cypress-videos
|
||||
path: |
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
16
README.md
16
README.md
@@ -11,7 +11,7 @@
|
||||
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
<!-- 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-67-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-77-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
@@ -96,7 +96,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CyferShepard"><img src="https://avatars.githubusercontent.com/u/24864904?v=4?s=100" width="100px;" alt="Thegan Govender"/><br /><sub><b>Thegan Govender</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=CyferShepard" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a> <a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nvds.be"><img src="https://avatars.githubusercontent.com/u/5257222?v=4?s=100" width="100px;" alt="Nicolai Van der Storm"/><br /><sub><b>Nicolai Van der Storm</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=NicolaiVdS" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Smexhy"><img src="https://avatars.githubusercontent.com/u/4880625?v=4?s=100" width="100px;" alt="Smexhy"/><br /><sub><b>Smexhy</b></sub></a><br /><a href="#translation-Smexhy" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dd06-dev.fr"><img src="https://avatars.githubusercontent.com/u/58089504?v=4?s=100" width="100px;" alt="dd060606"/><br /><sub><b>dd060606</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=dd060606" title="Code">💻</a></td>
|
||||
@@ -171,6 +171,18 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrewkolda"><img src="https://avatars.githubusercontent.com/u/158614532?v=4?s=100" width="100px;" alt="andrewkolda"/><br /><sub><b>andrewkolda</b></sub></a><br /><a href="#design-andrewkolda" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ishanjain.me"><img src="https://avatars.githubusercontent.com/u/7921368?v=4?s=100" width="100px;" alt="Ishan Jain"/><br /><sub><b>Ishan Jain</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ishanjain28" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://michaelt.xyz"><img src="https://avatars.githubusercontent.com/u/18223295?v=4?s=100" width="100px;" alt="Michael Thomas"/><br /><sub><b>Michael Thomas</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=michaelhthomas" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JackW6809" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RankWeis"><img src="https://avatars.githubusercontent.com/u/733691?v=4?s=100" width="100px;" alt="RankWeis"/><br /><sub><b>RankWeis</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=RankWeis" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
|
||||
name: jellyseerr-chart
|
||||
description: Jellyseerr helm chart for Kubernetes
|
||||
type: application
|
||||
version: 2.1.1
|
||||
appVersion: "2.3.0"
|
||||
version: 2.3.0
|
||||
appVersion: "2.5.0"
|
||||
maintainers:
|
||||
- name: Jellyseerr
|
||||
url: https://github.com/Fallenbagel/jellyseerr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# jellyseerr-chart
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Jellyseerr helm chart for Kubernetes
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export default defineConfig({
|
||||
projectId: 'xkm1b4',
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:5055',
|
||||
video: true,
|
||||
experimentalSessionAndOrigin: true,
|
||||
},
|
||||
env: {
|
||||
|
||||
@@ -19,14 +19,11 @@
|
||||
"discoverRegion": "",
|
||||
"streamingRegion": "",
|
||||
"originalLanguage": "",
|
||||
"blacklistedTags": "",
|
||||
"blacklistedTagsLimit": 50,
|
||||
"trustProxy": false,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"enableSpecialEpisodes": false,
|
||||
"forceIpv4First": false,
|
||||
"dnsServers": "",
|
||||
"locale": "en"
|
||||
},
|
||||
"plex": {
|
||||
|
||||
@@ -255,7 +255,8 @@ To run jellyseerr as a service:
|
||||
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
||||
2. Install NSSM:
|
||||
```powershell
|
||||
nssm install Jellyseerr "C:\Program Files\nodejs\node.exe" ["C:\jellyseerr\dist\index.js"]
|
||||
nssm install Jellyseerr "C:\Program Files\nodejs\node.exe" "C:\jellyseerr\dist\index.js"
|
||||
nssm set Jellyseerr AppDirectory "C:\jellyseerr"
|
||||
nssm set Jellyseerr AppEnvironmentExtra NODE_ENV=production
|
||||
```
|
||||
3. Start the service:
|
||||
|
||||
@@ -24,6 +24,12 @@ or for Cloudflare's DNS:
|
||||
```bash
|
||||
--dns=1.1.1.1
|
||||
```
|
||||
or for Quad9 DNS:
|
||||
```bash
|
||||
--dns=9.9.9.9
|
||||
```
|
||||
|
||||
You can try them all and see which one works for your network.
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -45,6 +51,16 @@ services:
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
```
|
||||
or for Quad9's DNS:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
dns:
|
||||
- 9.9.9.9
|
||||
```
|
||||
|
||||
You can try them all and see which one works for your network.
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -56,7 +72,7 @@ services:
|
||||
4. Click on Change adapter settings.
|
||||
5. Right-click the network interface connected to the internet and select Properties.
|
||||
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
|
||||
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS.
|
||||
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS or `9.9.9.9` for Quad9's DNS.
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -73,6 +89,10 @@ services:
|
||||
```bash
|
||||
nameserver 1.1.1.1
|
||||
```
|
||||
or for Quad9's DNS:
|
||||
```bash
|
||||
nameserver 9.9.9.9
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -81,7 +101,7 @@ services:
|
||||
|
||||
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
||||
|
||||
You can try to force the resolution to use IPV4 first by setting the `FORCE_IPV4_FIRST` environment variable to `true`:
|
||||
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting. You can also add the environment variable, `FORCE_IPV4_FIRST=true`:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
@@ -62,14 +62,6 @@ Set the default display language for Jellyseerr. Users can override this setting
|
||||
|
||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings.
|
||||
|
||||
## Blacklist Content with Tags and Limit Content Blacklisted per Tag
|
||||
|
||||
These settings blacklist any TV shows or movies that have one of the entered tags. The "Process Blacklisted Tags" job adds entries to the blacklist based on the configured blacklisted tags. If a blacklisted tag is removed, any media blacklisted under that tag will be removed from the blacklist when the "Process Blacklisted Tags" job runs.
|
||||
|
||||
The limit setting determines how many pages per tag the job will process, with each page containing 20 entries. The job cycles through all 16 available discovery sort options, querying the defined number of pages to blacklist media that is most likely to appear at the top of each sort. Higher limits will create a more accurate blacklist, but will require more storage.
|
||||
|
||||
Blacklisted tags are disabled until at least one tag is entered. These settings cannot be overridden in user settings.
|
||||
|
||||
## Hide Available Media
|
||||
|
||||
When enabled, media which is already available will not appear on the "Discover" home page, or in the "Recommended" or "Similar" categories or other links on media detail pages.
|
||||
|
||||
@@ -194,9 +194,6 @@ components:
|
||||
forceIpv4First:
|
||||
type: boolean
|
||||
example: false
|
||||
dnsServers:
|
||||
type: string
|
||||
example: '1.1.1.1'
|
||||
trustProxy:
|
||||
type: boolean
|
||||
example: true
|
||||
@@ -3815,6 +3812,11 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: includeIds
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of all users
|
||||
@@ -4156,12 +4158,6 @@ paths:
|
||||
type: string
|
||||
nullable: true
|
||||
example: dune
|
||||
- in: query
|
||||
name: filter
|
||||
schema:
|
||||
type: string
|
||||
enum: [all, manual, blacklistedTags]
|
||||
default: manual
|
||||
responses:
|
||||
'200':
|
||||
description: Blacklisted items returned
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
|
||||
34
package.json
34
package.json
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dr.pogodin/csurf": "^1.14.1",
|
||||
"@formatjs/intl-displaynames": "6.2.6",
|
||||
"@formatjs/intl-locale": "3.1.1",
|
||||
"@formatjs/intl-pluralrules": "5.1.10",
|
||||
@@ -47,16 +48,15 @@
|
||||
"bcrypt": "5.1.0",
|
||||
"bowser": "2.11.0",
|
||||
"connect-typeorm": "1.1.4",
|
||||
"cookie-parser": "1.4.6",
|
||||
"cookie-parser": "1.4.7",
|
||||
"copy-to-clipboard": "3.3.3",
|
||||
"country-flag-icons": "1.5.5",
|
||||
"cronstrue": "2.23.0",
|
||||
"csurf": "1.11.0",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "1.11.7",
|
||||
"email-templates": "9.0.0",
|
||||
"email-templates": "12.0.1",
|
||||
"email-validator": "2.0.4",
|
||||
"express": "4.18.2",
|
||||
"express": "4.21.2",
|
||||
"express-openapi-validator": "4.13.8",
|
||||
"express-rate-limit": "6.7.0",
|
||||
"express-session": "1.17.3",
|
||||
@@ -64,15 +64,15 @@
|
||||
"gravatar-url": "3.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"mime": "3",
|
||||
"next": "^14.2.4",
|
||||
"next": "^14.2.24",
|
||||
"node-cache": "5.1.2",
|
||||
"node-gyp": "9.3.1",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.1",
|
||||
"openpgp": "5.7.0",
|
||||
"nodemailer": "6.10.0",
|
||||
"openpgp": "5.11.2",
|
||||
"pg": "8.11.0",
|
||||
"plex-api": "5.3.2",
|
||||
"pug": "3.0.2",
|
||||
"pug": "3.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "10.1.0",
|
||||
"react-animate-height": "2.1.2",
|
||||
@@ -91,14 +91,14 @@
|
||||
"react-use-clipboard": "1.0.9",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"secure-random-password": "0.2.3",
|
||||
"semver": "7.3.8",
|
||||
"semver": "7.7.1",
|
||||
"sharp": "^0.33.4",
|
||||
"sqlite3": "5.1.4",
|
||||
"sqlite3": "5.1.7",
|
||||
"swagger-ui-express": "4.6.2",
|
||||
"swr": "2.2.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"typeorm": "0.3.11",
|
||||
"undici": "^6.20.1",
|
||||
"undici": "^7.3.0",
|
||||
"web-push": "3.5.0",
|
||||
"wink-jaro-distance": "^2.0.0",
|
||||
"winston": "3.8.2",
|
||||
@@ -106,7 +106,7 @@
|
||||
"xml2js": "0.4.23",
|
||||
"yamljs": "0.3.0",
|
||||
"yup": "0.32.11",
|
||||
"zod": "3.20.6"
|
||||
"zod": "3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "17.4.4",
|
||||
@@ -116,8 +116,8 @@
|
||||
"@semantic-release/exec": "6.0.3",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@tailwindcss/aspect-ratio": "0.4.2",
|
||||
"@tailwindcss/forms": "0.5.3",
|
||||
"@tailwindcss/typography": "0.5.9",
|
||||
"@tailwindcss/forms": "0.5.10",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@types/bcrypt": "5.0.0",
|
||||
"@types/cookie-parser": "1.4.3",
|
||||
"@types/country-flag-icons": "1.2.0",
|
||||
@@ -146,7 +146,7 @@
|
||||
"commitizen": "4.3.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"cy-mobile-commands": "0.3.0",
|
||||
"cypress": "12.7.0",
|
||||
"cypress": "14.1.0",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-config-next": "^14.2.4",
|
||||
@@ -159,8 +159,8 @@
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"husky": "8.0.3",
|
||||
"lint-staged": "13.1.2",
|
||||
"nodemon": "2.0.20",
|
||||
"postcss": "8.4.21",
|
||||
"nodemon": "3.1.9",
|
||||
"postcss": "8.4.31",
|
||||
"prettier": "2.8.4",
|
||||
"prettier-plugin-organize-imports": "3.2.2",
|
||||
"prettier-plugin-tailwindcss": "0.2.3",
|
||||
|
||||
2762
pnpm-lock.yaml
generated
2762
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
@@ -92,14 +94,22 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
export interface JellyfinItemsReponse {
|
||||
Items: JellyfinLibraryItemExtended[];
|
||||
TotalRecordCount: number;
|
||||
StartIndex: number;
|
||||
}
|
||||
|
||||
class JellyfinAPI extends ExternalAPI {
|
||||
private userId?: string;
|
||||
private mediaServerType: MediaServerType;
|
||||
|
||||
constructor(
|
||||
jellyfinHost: string,
|
||||
authToken?: string | null,
|
||||
deviceId?: string | null
|
||||
) {
|
||||
const settings = getSettings();
|
||||
let authHeaderVal: string;
|
||||
if (authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
||||
@@ -116,6 +126,8 @@ class JellyfinAPI extends ExternalAPI {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.mediaServerType = settings.main.mediaServerType;
|
||||
}
|
||||
|
||||
public async login(
|
||||
@@ -296,18 +308,15 @@ class JellyfinAPI extends ExternalAPI {
|
||||
|
||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
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',
|
||||
}
|
||||
);
|
||||
const libraryItemsResponse = await this.get<any>(`/Items`, {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Series,Movie,Others',
|
||||
Recursive: 'true',
|
||||
StartIndex: '0',
|
||||
ParentId: id,
|
||||
collapseBoxSetItems: 'false',
|
||||
});
|
||||
|
||||
return libraryItemsResponse.Items.filter(
|
||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||
@@ -324,13 +333,22 @@ class JellyfinAPI extends ExternalAPI {
|
||||
|
||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/Latest`,
|
||||
{
|
||||
Limit: '12',
|
||||
ParentId: id,
|
||||
}
|
||||
);
|
||||
const endpoint =
|
||||
this.mediaServerType === MediaServerType.JELLYFIN
|
||||
? `/Items/Latest`
|
||||
: `/Users/${this.userId}/Items/Latest`;
|
||||
|
||||
const baseParams = {
|
||||
Limit: '12',
|
||||
ParentId: id,
|
||||
};
|
||||
|
||||
const params =
|
||||
this.mediaServerType === MediaServerType.JELLYFIN
|
||||
? { ...baseParams, userId: this.userId ?? `Me` }
|
||||
: baseParams;
|
||||
|
||||
const itemResponse = await this.get<any>(endpoint, params);
|
||||
|
||||
return itemResponse;
|
||||
} catch (e) {
|
||||
@@ -347,11 +365,12 @@ class JellyfinAPI extends ExternalAPI {
|
||||
id: string
|
||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||
try {
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/${id}`
|
||||
);
|
||||
const itemResponse = await this.get<JellyfinItemsReponse>(`/Items`, {
|
||||
ids: id,
|
||||
fields: 'ProviderIds,MediaSources,Width,Height,IsHD,DateCreated',
|
||||
});
|
||||
|
||||
return itemResponse;
|
||||
return itemResponse.Items?.[0];
|
||||
} catch (e) {
|
||||
if (availabilitySync.running) {
|
||||
if (e.cause?.status === 500) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import { sortBy } from 'lodash';
|
||||
import type {
|
||||
TmdbCollection,
|
||||
@@ -37,26 +38,23 @@ interface SingleSearchOptions extends SearchOptions {
|
||||
year?: number;
|
||||
}
|
||||
|
||||
export const SortOptionsIterable = [
|
||||
'popularity.desc',
|
||||
'popularity.asc',
|
||||
'release_date.desc',
|
||||
'release_date.asc',
|
||||
'revenue.desc',
|
||||
'revenue.asc',
|
||||
'primary_release_date.desc',
|
||||
'primary_release_date.asc',
|
||||
'original_title.asc',
|
||||
'original_title.desc',
|
||||
'vote_average.desc',
|
||||
'vote_average.asc',
|
||||
'vote_count.desc',
|
||||
'vote_count.asc',
|
||||
'first_air_date.desc',
|
||||
'first_air_date.asc',
|
||||
] as const;
|
||||
|
||||
export type SortOptions = (typeof SortOptionsIterable)[number];
|
||||
export type SortOptions =
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
| 'release_date.asc'
|
||||
| 'release_date.desc'
|
||||
| 'revenue.asc'
|
||||
| 'revenue.desc'
|
||||
| 'primary_release_date.asc'
|
||||
| 'primary_release_date.desc'
|
||||
| 'original_title.asc'
|
||||
| 'original_title.desc'
|
||||
| 'vote_average.asc'
|
||||
| 'vote_average.desc'
|
||||
| 'vote_count.asc'
|
||||
| 'vote_count.desc'
|
||||
| 'first_air_date.asc'
|
||||
| 'first_air_date.desc';
|
||||
|
||||
interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
@@ -102,6 +100,7 @@ interface DiscoverTvOptions {
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
private locale: string;
|
||||
private discoverRegion?: string;
|
||||
private originalLanguage?: string;
|
||||
constructor({
|
||||
@@ -121,6 +120,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
},
|
||||
}
|
||||
);
|
||||
this.locale = getSettings().main?.locale || 'en';
|
||||
this.discoverRegion = discoverRegion;
|
||||
this.originalLanguage = originalLanguage;
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||
@@ -154,7 +154,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
year,
|
||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
@@ -181,7 +181,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
year,
|
||||
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
@@ -206,7 +206,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public getPerson = async ({
|
||||
personId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
@@ -224,7 +224,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public getPersonCombinedCredits = async ({
|
||||
personId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
@@ -247,7 +247,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public getMovie = async ({
|
||||
movieId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
movieId: number;
|
||||
language?: string;
|
||||
@@ -272,7 +272,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public getTvShow = async ({
|
||||
tvId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
tvId: number;
|
||||
language?: string;
|
||||
@@ -322,7 +322,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public async getMovieRecommendations({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
@@ -346,7 +346,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public async getMovieSimilar({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
@@ -370,7 +370,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public async getMoviesByKeyword({
|
||||
keywordId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
keywordId: number;
|
||||
page?: number;
|
||||
@@ -394,7 +394,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public async getTvRecommendations({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
@@ -420,7 +420,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public async getTvSimilar({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
@@ -442,7 +442,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
primaryReleaseDateGte,
|
||||
primaryReleaseDateLte,
|
||||
originalLanguage,
|
||||
@@ -513,7 +513,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public getDiscoverTv = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
firstAirDateGte,
|
||||
firstAirDateLte,
|
||||
includeEmptyReleaseDate = false,
|
||||
@@ -588,7 +588,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public getUpcomingMovies = async ({
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
page: number;
|
||||
language: string;
|
||||
@@ -613,7 +613,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public getAllTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
@@ -680,7 +680,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
public async getByExternalId({
|
||||
externalId,
|
||||
type,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}:
|
||||
| {
|
||||
externalId: string;
|
||||
@@ -709,7 +709,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public async getMediaByImdbId({
|
||||
imdbId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
imdbId: string;
|
||||
language?: string;
|
||||
@@ -748,7 +748,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public async getShowByTvdbId({
|
||||
tvdbId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
tvdbId: number;
|
||||
language?: string;
|
||||
@@ -778,7 +778,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
public async getCollection({
|
||||
collectionId,
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
collectionId: number;
|
||||
language?: string;
|
||||
@@ -852,7 +852,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
}
|
||||
|
||||
public async getMovieGenres({
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbGenre[]> {
|
||||
@@ -899,7 +899,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
}
|
||||
|
||||
public async getTvGenres({
|
||||
language = 'en',
|
||||
language = this.locale,
|
||||
}: {
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbGenre[]> {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { MediaStatus, type MediaType } from '@server/constants/media';
|
||||
import dataSource from '@server/datasource';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
|
||||
import type { EntityManager } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -36,7 +35,7 @@ export class Blacklist implements BlacklistItem {
|
||||
@ManyToOne(() => User, (user) => user.id, {
|
||||
eager: true,
|
||||
})
|
||||
user?: User;
|
||||
user: User;
|
||||
|
||||
@OneToOne(() => Media, (media) => media.blacklist, {
|
||||
onDelete: 'CASCADE',
|
||||
@@ -44,9 +43,6 @@ export class Blacklist implements BlacklistItem {
|
||||
@JoinColumn()
|
||||
public media: Media;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
public blacklistedTags?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
@@ -54,32 +50,27 @@ export class Blacklist implements BlacklistItem {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
public static async addToBlacklist(
|
||||
{
|
||||
blacklistRequest,
|
||||
}: {
|
||||
blacklistRequest: {
|
||||
mediaType: MediaType;
|
||||
title?: ZodOptional<ZodString>['_output'];
|
||||
tmdbId: ZodNumber['_output'];
|
||||
blacklistedTags?: string;
|
||||
};
|
||||
},
|
||||
entityManager?: EntityManager
|
||||
): Promise<void> {
|
||||
const em = entityManager ?? dataSource;
|
||||
public static async addToBlacklist({
|
||||
blacklistRequest,
|
||||
}: {
|
||||
blacklistRequest: {
|
||||
mediaType: MediaType;
|
||||
title?: ZodOptional<ZodString>['_output'];
|
||||
tmdbId: ZodNumber['_output'];
|
||||
};
|
||||
}): Promise<void> {
|
||||
const blacklist = new this({
|
||||
...blacklistRequest,
|
||||
});
|
||||
|
||||
const mediaRepository = em.getRepository(Media);
|
||||
const mediaRepository = getRepository(Media);
|
||||
let media = await mediaRepository.findOne({
|
||||
where: {
|
||||
tmdbId: blacklistRequest.tmdbId,
|
||||
},
|
||||
});
|
||||
|
||||
const blacklistRepository = em.getRepository(this);
|
||||
const blacklistRepository = getRepository(this);
|
||||
|
||||
await blacklistRepository.save(blacklist);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import csurf from '@dr.pogodin/csurf';
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import dataSource, { getRepository, isPgsql } from '@server/datasource';
|
||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
@@ -28,7 +29,6 @@ import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import csurf from 'csurf';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
@@ -83,12 +83,6 @@ app
|
||||
net.setDefaultAutoSelectFamily(false);
|
||||
}
|
||||
|
||||
if (settings.network.dnsServers.trim() !== '') {
|
||||
dns.setServers(
|
||||
settings.network.dnsServers.split(',').map((server) => server.trim())
|
||||
);
|
||||
}
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.network.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.network.proxy);
|
||||
|
||||
@@ -6,8 +6,7 @@ export interface BlacklistItem {
|
||||
mediaType: 'movie' | 'tv';
|
||||
title?: string;
|
||||
createdAt?: Date;
|
||||
user?: User;
|
||||
blacklistedTags?: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface BlacklistResultsResponse extends PaginatedResponse {
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import type { SortOptions } from '@server/api/themoviedb';
|
||||
import { SortOptionsIterable } from '@server/api/themoviedb';
|
||||
import type {
|
||||
TmdbSearchMovieResponse,
|
||||
TmdbSearchTvResponse,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import dataSource from '@server/datasource';
|
||||
import { Blacklist } from '@server/entity/Blacklist';
|
||||
import Media from '@server/entity/Media';
|
||||
import type {
|
||||
RunnableScanner,
|
||||
StatusBase,
|
||||
} from '@server/lib/scanners/baseScanner';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { createTmdbWithRegionLanguage } from '@server/routes/discover';
|
||||
import type { EntityManager } from 'typeorm';
|
||||
|
||||
const TMDB_API_DELAY_MS = 250;
|
||||
class AbortTransaction extends Error {}
|
||||
|
||||
class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
private running = false;
|
||||
private progress = 0;
|
||||
private total = 0;
|
||||
|
||||
public async run() {
|
||||
this.running = true;
|
||||
|
||||
try {
|
||||
await dataSource.transaction(async (em) => {
|
||||
await this.cleanBlacklist(em);
|
||||
await this.createBlacklistEntries(em);
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof AbortTransaction) {
|
||||
logger.info('Aborting job: Process Blacklisted Tags', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public status(): StatusBase {
|
||||
return {
|
||||
running: this.running,
|
||||
progress: this.progress,
|
||||
total: this.total,
|
||||
};
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
this.running = false;
|
||||
this.progress = 0;
|
||||
this.total = 0;
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
private async createBlacklistEntries(em: EntityManager) {
|
||||
const tmdb = createTmdbWithRegionLanguage();
|
||||
|
||||
const settings = getSettings();
|
||||
const blacklistedTags = settings.main.blacklistedTags;
|
||||
const blacklistedTagsArr = blacklistedTags.split(',');
|
||||
|
||||
const pageLimit = settings.main.blacklistedTagsLimit;
|
||||
|
||||
if (blacklistedTags.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The maximum number of queries we're expected to execute
|
||||
this.total =
|
||||
2 * blacklistedTagsArr.length * pageLimit * SortOptionsIterable.length;
|
||||
|
||||
for (const type of [MediaType.MOVIE, MediaType.TV]) {
|
||||
const getDiscover =
|
||||
type === MediaType.MOVIE ? tmdb.getDiscoverMovies : tmdb.getDiscoverTv;
|
||||
|
||||
// Iterate for each tag
|
||||
for (const tag of blacklistedTagsArr) {
|
||||
let queryMax = pageLimit * SortOptionsIterable.length;
|
||||
let fixedSortMode = false; // Set to true when the page limit allows for getting every page of tag
|
||||
|
||||
for (let query = 0; query < queryMax; query++) {
|
||||
const page: number = fixedSortMode
|
||||
? query + 1
|
||||
: (query % pageLimit) + 1;
|
||||
const sortBy: SortOptions | undefined = fixedSortMode
|
||||
? undefined
|
||||
: SortOptionsIterable[query % SortOptionsIterable.length];
|
||||
|
||||
if (!this.running) {
|
||||
throw new AbortTransaction();
|
||||
}
|
||||
|
||||
const response = await getDiscover({
|
||||
page,
|
||||
sortBy,
|
||||
keywords: tag,
|
||||
});
|
||||
await this.processResults(response, tag, type, em);
|
||||
await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS));
|
||||
|
||||
this.progress++;
|
||||
if (page === 1 && response.total_pages <= queryMax) {
|
||||
// We will finish the tag with less queries than expected, move progress accordingly
|
||||
this.progress += queryMax - response.total_pages;
|
||||
fixedSortMode = true;
|
||||
queryMax = response.total_pages;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async processResults(
|
||||
response: TmdbSearchMovieResponse | TmdbSearchTvResponse,
|
||||
keywordId: string,
|
||||
mediaType: MediaType,
|
||||
em: EntityManager
|
||||
) {
|
||||
const blacklistRepository = em.getRepository(Blacklist);
|
||||
|
||||
for (const entry of response.results) {
|
||||
const blacklistEntry = await blacklistRepository.findOne({
|
||||
where: { tmdbId: entry.id },
|
||||
});
|
||||
|
||||
if (blacklistEntry) {
|
||||
// Don't mark manual blacklists with tags
|
||||
// If media wasn't previously blacklisted for this tag, add the tag to the media's blacklist
|
||||
if (
|
||||
blacklistEntry.blacklistedTags &&
|
||||
!blacklistEntry.blacklistedTags.includes(`,${keywordId},`)
|
||||
) {
|
||||
await blacklistRepository.update(blacklistEntry.id, {
|
||||
blacklistedTags: `${blacklistEntry.blacklistedTags}${keywordId},`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Media wasn't previously blacklisted, add it to the blacklist
|
||||
await Blacklist.addToBlacklist(
|
||||
{
|
||||
blacklistRequest: {
|
||||
mediaType,
|
||||
title: 'title' in entry ? entry.title : entry.name,
|
||||
tmdbId: entry.id,
|
||||
blacklistedTags: `,${keywordId},`,
|
||||
},
|
||||
},
|
||||
em
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanBlacklist(em: EntityManager) {
|
||||
// Remove blacklist and media entries blacklisted by tags
|
||||
const mediaRepository = em.getRepository(Media);
|
||||
const mediaToRemove = await mediaRepository
|
||||
.createQueryBuilder('media')
|
||||
.innerJoinAndSelect(Blacklist, 'blist', 'blist.tmdbId = media.tmdbId')
|
||||
.where(`blist.blacklistedTags IS NOT NULL`)
|
||||
.getMany();
|
||||
|
||||
// Batch removes so the query doesn't get too large
|
||||
for (let i = 0; i < mediaToRemove.length; i += 500) {
|
||||
await mediaRepository.remove(mediaToRemove.slice(i, i + 500)); // This also deletes the blacklist entries via cascading
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blacklistedTagsProcessor = new BlacklistedTagProcessor();
|
||||
|
||||
export default blacklistedTagsProcessor;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import blacklistedTagsProcessor from '@server/job/blacklistedTagsProcessor';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
@@ -22,7 +21,7 @@ interface ScheduledJob {
|
||||
job: schedule.Job;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'seconds' | 'minutes' | 'hours' | 'days' | 'fixed';
|
||||
interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
|
||||
cronSchedule: string;
|
||||
running?: () => boolean;
|
||||
cancelFn?: () => void;
|
||||
@@ -238,21 +237,5 @@ export const startJobs = (): void => {
|
||||
}),
|
||||
});
|
||||
|
||||
scheduledJobs.push({
|
||||
id: 'process-blacklisted-tags',
|
||||
name: 'Process Blacklisted Tags',
|
||||
type: 'process',
|
||||
interval: 'days',
|
||||
cronSchedule: jobs['process-blacklisted-tags'].schedule,
|
||||
job: schedule.scheduleJob(jobs['process-blacklisted-tags'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Process Blacklisted Tags', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
blacklistedTagsProcessor.run();
|
||||
}),
|
||||
running: () => blacklistedTagsProcessor.status().running,
|
||||
cancelFn: () => blacklistedTagsProcessor.cancel(),
|
||||
});
|
||||
|
||||
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ class PreparedEmail extends Email {
|
||||
},
|
||||
send: true,
|
||||
transport: transport,
|
||||
preview: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@ export interface MainSettings {
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
originalLanguage: string;
|
||||
blacklistedTags: string;
|
||||
blacklistedTagsLimit: number;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
@@ -139,7 +137,6 @@ export interface MainSettings {
|
||||
export interface NetworkSettings {
|
||||
csrfProtection: boolean;
|
||||
forceIpv4First: boolean;
|
||||
dnsServers: string;
|
||||
trustProxy: boolean;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
@@ -305,8 +302,7 @@ export type JobId =
|
||||
| 'jellyfin-recently-added-scan'
|
||||
| 'jellyfin-full-scan'
|
||||
| 'image-cache-cleanup'
|
||||
| 'availability-sync'
|
||||
| 'process-blacklisted-tags';
|
||||
| 'availability-sync';
|
||||
|
||||
export interface AllSettings {
|
||||
clientId: string;
|
||||
@@ -353,8 +349,6 @@ class Settings {
|
||||
discoverRegion: '',
|
||||
streamingRegion: '',
|
||||
originalLanguage: '',
|
||||
blacklistedTags: '',
|
||||
blacklistedTagsLimit: 50,
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
@@ -510,15 +504,11 @@ class Settings {
|
||||
'image-cache-cleanup': {
|
||||
schedule: '0 0 5 * * *',
|
||||
},
|
||||
'process-blacklisted-tags': {
|
||||
schedule: '0 30 1 */7 * *',
|
||||
},
|
||||
},
|
||||
network: {
|
||||
csrfProtection: false,
|
||||
trustProxy: false,
|
||||
forceIpv4First: false,
|
||||
dnsServers: '',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
|
||||
@@ -10,7 +10,6 @@ const migrateNetworkSettings = (settings: any): AllSettings => {
|
||||
csrfProtection: settings.main.csrfProtection ?? false,
|
||||
trustProxy: settings.main.trustProxy ?? false,
|
||||
forceIpv4First: settings.main.forceIpv4First ?? false,
|
||||
dnsServers: settings.main.dnsServers ?? '',
|
||||
proxy: settings.main.proxy ?? {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
@@ -25,7 +24,6 @@ const migrateNetworkSettings = (settings: any): AllSettings => {
|
||||
delete settings.main.csrfProtection;
|
||||
delete settings.main.trustProxy;
|
||||
delete settings.main.forceIpv4First;
|
||||
delete settings.main.dnsServers;
|
||||
delete settings.main.proxy;
|
||||
return newSettings;
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBlacklistTagsColumn1737320080282 implements MigrationInterface {
|
||||
name = 'AddBlacklistTagsColumn1737320080282';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" ADD blacklistedTags character varying`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" DROP COLUMN blacklistedTags`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBlacklistTagsColumn1737320080282 implements MigrationInterface {
|
||||
name = 'AddBlacklistTagsColumn1737320080282';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blacklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "blacklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blacklistedTags", "createdAt", "userId", "mediaId" FROM "blacklist"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "temporary_blacklist"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_blacklist"`);
|
||||
}
|
||||
}
|
||||
@@ -19,54 +19,39 @@ export const blacklistAdd = z.object({
|
||||
user: z.coerce.number(),
|
||||
});
|
||||
|
||||
const blacklistGet = z.object({
|
||||
take: z.coerce.number().int().positive().default(25),
|
||||
skip: z.coerce.number().int().nonnegative().default(0),
|
||||
search: z.string().optional(),
|
||||
filter: z.enum(['all', 'manual', 'blacklistedTags']).optional(),
|
||||
});
|
||||
|
||||
blacklistRoutes.get(
|
||||
'/',
|
||||
isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
|
||||
type: 'or',
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
const { take, skip, search, filter } = blacklistGet.parse(req.query);
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 25;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
const search = (req.query.search as string) ?? '';
|
||||
|
||||
try {
|
||||
let query = getRepository(Blacklist)
|
||||
.createQueryBuilder('blacklist')
|
||||
.leftJoinAndSelect('blacklist.user', 'user')
|
||||
.where('1 = 1'); // Allow use of andWhere later
|
||||
.leftJoinAndSelect('blacklist.user', 'user');
|
||||
|
||||
switch (filter) {
|
||||
case 'manual':
|
||||
query = query.andWhere('blacklist.blacklistedTags IS NULL');
|
||||
break;
|
||||
case 'blacklistedTags':
|
||||
query = query.andWhere('blacklist.blacklistedTags IS NOT NULL');
|
||||
break;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query = query.andWhere('blacklist.title like :title', {
|
||||
if (search.length > 0) {
|
||||
query = query.where('blacklist.title like :title', {
|
||||
title: `%${search}%`,
|
||||
});
|
||||
}
|
||||
|
||||
const [blacklistedItems, itemsCount] = await query
|
||||
.orderBy('blacklist.createdAt', 'DESC')
|
||||
.take(take)
|
||||
.take(pageSize)
|
||||
.skip(skip)
|
||||
.getManyAndCount();
|
||||
|
||||
return res.status(200).json({
|
||||
pageInfo: {
|
||||
pages: Math.ceil(itemsCount / take),
|
||||
pageSize: take,
|
||||
pages: Math.ceil(itemsCount / pageSize),
|
||||
pageSize,
|
||||
results: itemsCount,
|
||||
page: Math.ceil(skip / take) + 1,
|
||||
page: Math.ceil(skip / pageSize) + 1,
|
||||
},
|
||||
results: blacklistedItems,
|
||||
} as BlacklistResultsResponse);
|
||||
|
||||
@@ -237,6 +237,19 @@ mediaRoutes.delete(
|
||||
}
|
||||
|
||||
if (isMovie) {
|
||||
// check if the movie exists
|
||||
try {
|
||||
await (service as RadarrAPI).getMovie({
|
||||
id: parseInt(
|
||||
is4k
|
||||
? (media.externalServiceSlug4k as string)
|
||||
: (media.externalServiceSlug as string)
|
||||
),
|
||||
});
|
||||
} catch {
|
||||
return res.status(204).send();
|
||||
}
|
||||
// remove the movie
|
||||
await (service as RadarrAPI).removeMovie(
|
||||
parseInt(
|
||||
is4k
|
||||
@@ -251,6 +264,13 @@ mediaRoutes.delete(
|
||||
if (!tvdbId) {
|
||||
throw new Error('TVDB ID not found');
|
||||
}
|
||||
// check if the series exists
|
||||
try {
|
||||
await (service as SonarrAPI).getSeriesByTvdbId(tvdbId);
|
||||
} catch {
|
||||
return res.status(204).send();
|
||||
}
|
||||
// remove the series
|
||||
await (service as SonarrAPI).removeSerie(tvdbId);
|
||||
}
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
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) {
|
||||
if (account.Configuration.GroupedFolders?.length > 0) {
|
||||
return next({
|
||||
status: 501,
|
||||
message: ApiErrorCode.SyncErrorGroupedFolders,
|
||||
|
||||
@@ -32,7 +32,14 @@ const router = Router();
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 10;
|
||||
const includeIds = [
|
||||
...new Set(
|
||||
req.query.includeIds ? req.query.includeIds.toString().split(',') : []
|
||||
),
|
||||
];
|
||||
const pageSize = req.query.take
|
||||
? Number(req.query.take)
|
||||
: Math.max(10, includeIds.length);
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
|
||||
let query = getRepository(User).createQueryBuilder('user');
|
||||
@@ -44,27 +51,33 @@ router.get('/', async (req, res, next) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (includeIds.length > 0) {
|
||||
query.andWhereInIds(includeIds);
|
||||
}
|
||||
|
||||
switch (req.query.sort) {
|
||||
case 'updated':
|
||||
query = query.orderBy('user.updatedAt', 'DESC');
|
||||
break;
|
||||
case 'displayname':
|
||||
query = query.orderBy(
|
||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||
"user"."email"
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.username)
|
||||
END`,
|
||||
'ASC'
|
||||
);
|
||||
query = query
|
||||
.addSelect(
|
||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||
"user"."email"
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.username)
|
||||
END`,
|
||||
'displayname_sort_key'
|
||||
)
|
||||
.orderBy('displayname_sort_key', 'ASC');
|
||||
break;
|
||||
case 'requests':
|
||||
query = query
|
||||
@@ -84,6 +97,7 @@ router.get('/', async (req, res, next) => {
|
||||
const [users, userCount] = await query
|
||||
.take(pageSize)
|
||||
.skip(skip)
|
||||
.distinct(true)
|
||||
.getManyAndCount();
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
4
server/types/custom.d.ts
vendored
Normal file
4
server/types/custom.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '@dr.pogodin/csurf' {
|
||||
import csrf = require('csurf');
|
||||
export = csrf;
|
||||
}
|
||||
@@ -8,8 +8,9 @@ export default async function createCustomProxyAgent(
|
||||
) {
|
||||
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
||||
|
||||
const skipUrl = (url: string) => {
|
||||
const hostname = new URL(url).hostname;
|
||||
const skipUrl = (url: string | URL) => {
|
||||
const hostname =
|
||||
typeof url === 'string' ? new URL(url).hostname : url.hostname;
|
||||
|
||||
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
||||
return true;
|
||||
@@ -38,8 +39,7 @@ export default async function createCustomProxyAgent(
|
||||
dispatch: Dispatcher['dispatch']
|
||||
): Dispatcher['dispatch'] => {
|
||||
return (opts, handler) => {
|
||||
const url = opts.origin?.toString();
|
||||
return url && skipUrl(url)
|
||||
return opts.origin && skipUrl(opts.origin)
|
||||
? defaultAgent.dispatch(opts, handler)
|
||||
: dispatch(opts, handler);
|
||||
};
|
||||
@@ -60,13 +60,10 @@ export default async function createCustomProxyAgent(
|
||||
':' +
|
||||
proxySettings.port,
|
||||
token,
|
||||
interceptors: {
|
||||
Client: [noProxyInterceptor],
|
||||
},
|
||||
keepAliveTimeout: 5000,
|
||||
});
|
||||
|
||||
setGlobalDispatcher(proxyAgent);
|
||||
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||
} catch (e) {
|
||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
||||
label: 'Proxy',
|
||||
@@ -95,7 +92,11 @@ export default async function createCustomProxyAgent(
|
||||
}
|
||||
|
||||
function isLocalAddress(hostname: string) {
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
if (
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '::1'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ class RestartFlag {
|
||||
this.networkSettings.csrfProtection !== networkSettings.csrfProtection ||
|
||||
this.networkSettings.trustProxy !== networkSettings.trustProxy ||
|
||||
this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled ||
|
||||
this.networkSettings.forceIpv4First !== networkSettings.forceIpv4First ||
|
||||
this.networkSettings.dnsServers !== networkSettings.dnsServers
|
||||
this.networkSettings.forceIpv4First !== networkSettings.forceIpv4First
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,13 @@ type AirDateBadgeProps = {
|
||||
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
||||
const WEEK = 1000 * 60 * 60 * 24 * 8;
|
||||
const intl = useIntl();
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const dAirDate = new Date(airDate);
|
||||
const nowDate = new Date();
|
||||
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
|
||||
|
||||
const compareWeek = new Date(
|
||||
alreadyAired ? Date.now() - WEEK : Date.now() + WEEK
|
||||
);
|
||||
|
||||
let showRelative = false;
|
||||
|
||||
if (
|
||||
(alreadyAired && dAirDate.getTime() > compareWeek.getTime()) ||
|
||||
(!alreadyAired && dAirDate.getTime() < compareWeek.getTime())
|
||||
@@ -32,6 +28,10 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
||||
showRelative = true;
|
||||
}
|
||||
|
||||
const diffInDays = Math.round(
|
||||
(dAirDate.getTime() - nowDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge badgeType="light">
|
||||
@@ -39,7 +39,7 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone,
|
||||
timeZone: 'UTC',
|
||||
})}
|
||||
</Badge>
|
||||
{showRelative && (
|
||||
@@ -49,9 +49,9 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
||||
{
|
||||
relativeTime: (
|
||||
<FormattedRelativeTime
|
||||
value={(dAirDate.getTime() - Date.now()) / 1000}
|
||||
value={diffInDays}
|
||||
unit="day"
|
||||
numeric="auto"
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import BlacklistedTagsBadge from '@app/components/BlacklistedTagsBadge';
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
@@ -15,7 +14,6 @@ import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FunnelIcon,
|
||||
MagnifyingGlassIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
@@ -43,17 +41,8 @@ const messages = defineMessages('components.Blacklist', {
|
||||
blacklistdate: 'date',
|
||||
blacklistedby: '{date} by {user}',
|
||||
blacklistNotFoundError: '<strong>{title}</strong> is not blacklisted.',
|
||||
filterManual: 'Manual',
|
||||
filterBlacklistedTags: 'Blacklisted Tags',
|
||||
showAllBlacklisted: 'Show All Blacklisted Media',
|
||||
});
|
||||
|
||||
enum Filter {
|
||||
ALL = 'all',
|
||||
MANUAL = 'manual',
|
||||
BLACKLISTEDTAGS = 'blacklistedTags',
|
||||
}
|
||||
|
||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
return (movie as MovieDetails).title !== undefined;
|
||||
};
|
||||
@@ -62,7 +51,6 @@ const Blacklist = () => {
|
||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||
const [searchFilter, debouncedSearchFilter, setSearchFilter] =
|
||||
useDebouncedState('');
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.MANUAL);
|
||||
const router = useRouter();
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -75,11 +63,9 @@ const Blacklist = () => {
|
||||
error,
|
||||
mutate: revalidate,
|
||||
} = useSWR<BlacklistResultsResponse>(
|
||||
`/api/v1/blacklist/?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}${
|
||||
debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''
|
||||
}`,
|
||||
`/api/v1/blacklist/?take=${currentPageSize}
|
||||
&skip=${pageIndex * currentPageSize}
|
||||
${debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''}`,
|
||||
{
|
||||
refreshInterval: 0,
|
||||
revalidateOnFocus: false,
|
||||
@@ -107,52 +93,19 @@ const Blacklist = () => {
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={[intl.formatMessage(globalMessages.blacklist)]} />
|
||||
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
|
||||
<Header>{intl.formatMessage(globalMessages.blacklist)}</Header>
|
||||
<Header>{intl.formatMessage(globalMessages.blacklist)}</Header>
|
||||
|
||||
<div className="mt-2 flex flex-grow flex-col sm:flex-row lg:flex-grow-0">
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<FunnelIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="filter"
|
||||
name="filter"
|
||||
onChange={(e) => {
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
});
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="all">
|
||||
{intl.formatMessage(globalMessages.all)}
|
||||
</option>
|
||||
<option value="manual">
|
||||
{intl.formatMessage(messages.filterManual)}
|
||||
</option>
|
||||
<option value="blacklistedTags">
|
||||
{intl.formatMessage(messages.filterBlacklistedTags)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 md:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<MagnifyingGlassIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="rounded-r-only"
|
||||
value={searchFilter}
|
||||
onChange={(e) => searchItem(e)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 md:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<MagnifyingGlassIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="rounded-r-only"
|
||||
value={searchFilter}
|
||||
onChange={(e) => searchItem(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,16 +116,6 @@ const Blacklist = () => {
|
||||
<span className="text-2xl text-gray-400">
|
||||
{intl.formatMessage(globalMessages.noresults)}
|
||||
</span>
|
||||
{currentFilter !== Filter.ALL && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
onClick={() => setCurrentFilter(Filter.ALL)}
|
||||
>
|
||||
{intl.formatMessage(messages.showAllBlacklisted)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
data.results.map((item: BlacklistItem) => {
|
||||
@@ -410,7 +353,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
|
||||
numeric="auto"
|
||||
/>
|
||||
),
|
||||
user: item.user ? (
|
||||
user: (
|
||||
<Link href={`/users/${item.user.id}`}>
|
||||
<span className="group flex items-center truncate">
|
||||
<CachedImage
|
||||
@@ -427,14 +370,6 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
) : item.blacklistedTags ? (
|
||||
<span className="ml-1">
|
||||
<BlacklistedTagsBadge data={item} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="ml-1 truncate text-sm font-semibold">
|
||||
???
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import BlacklistedTagsBadge from '@app/components/BlacklistedTagsBadge';
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
@@ -79,33 +78,22 @@ const BlacklistBlock = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
|
||||
<div className="white mb-1 flex flex-nowrap">
|
||||
{data.user ? (
|
||||
<>
|
||||
<Tooltip content={intl.formatMessage(messages.blacklistedby)}>
|
||||
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
<Link
|
||||
href={
|
||||
data.user.id === user?.id
|
||||
? '/profile'
|
||||
: `/users/${data.user.id}`
|
||||
}
|
||||
>
|
||||
<span className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
|
||||
{data.user.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
<Tooltip content={intl.formatMessage(messages.blacklistedby)}>
|
||||
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
<Link
|
||||
href={
|
||||
data.user.id === user?.id
|
||||
? '/profile'
|
||||
: `/users/${data.user.id}`
|
||||
}
|
||||
>
|
||||
<span className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
|
||||
{data.user.displayName}
|
||||
</span>
|
||||
</>
|
||||
) : data.blacklistedTags ? (
|
||||
<>
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
{intl.formatMessage(messages.blacklistedby)}:
|
||||
</span>
|
||||
<BlacklistedTagsBadge data={data} />
|
||||
</>
|
||||
) : null}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-shrink-0 flex-wrap">
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { TagIcon } from '@heroicons/react/20/solid';
|
||||
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
|
||||
import type { Keyword } from '@server/models/common';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages('components.Settings', {
|
||||
blacklistedTagsText: 'Blacklisted Tags',
|
||||
});
|
||||
|
||||
interface BlacklistedTagsBadgeProps {
|
||||
data: BlacklistItem;
|
||||
}
|
||||
|
||||
const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
|
||||
const [tagNamesBlacklistedFor, setTagNamesBlacklistedFor] =
|
||||
useState<string>('Loading...');
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data.blacklistedTags) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keywordIds = data.blacklistedTags.slice(1, -1).split(',');
|
||||
Promise.all(
|
||||
keywordIds.map(async (keywordId) => {
|
||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
||||
if (!res.ok) {
|
||||
return '';
|
||||
}
|
||||
const keyword: Keyword = await res.json();
|
||||
|
||||
return keyword.name;
|
||||
})
|
||||
).then((keywords) => {
|
||||
setTagNamesBlacklistedFor(keywords.join(', '));
|
||||
});
|
||||
}, [data.blacklistedTags]);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tagNamesBlacklistedFor}
|
||||
tooltipConfig={{ followCursor: false }}
|
||||
>
|
||||
<Badge
|
||||
badgeType="dark"
|
||||
className="items-center border border-red-500 !text-red-400"
|
||||
>
|
||||
<TagIcon className="mr-1 h-4" />
|
||||
{intl.formatMessage(messages.blacklistedTagsText)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlacklistedTagsBadge;
|
||||
@@ -1,407 +0,0 @@
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import CopyButton from '@app/components/Settings/CopyButton';
|
||||
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { ArrowDownIcon } from '@heroicons/react/24/solid';
|
||||
import type { TmdbKeywordSearchResponse } from '@server/api/themoviedb/interfaces';
|
||||
import type { Keyword } from '@server/models/common';
|
||||
import { useFormikContext } from 'formik';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import type { ClearIndicatorProps, GroupBase, MultiValue } from 'react-select';
|
||||
import { components } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
const messages = defineMessages('components.Settings', {
|
||||
copyBlacklistedTags: 'Copied blacklisted tags to clipboard.',
|
||||
copyBlacklistedTagsTip: 'Copy blacklisted tag configuration',
|
||||
copyBlacklistedTagsEmpty: 'Nothing to copy',
|
||||
importBlacklistedTagsTip: 'Import blacklisted tag configuration',
|
||||
clearBlacklistedTagsConfirm:
|
||||
'Are you sure you want to clear the blacklisted tags?',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
searchKeywords: 'Search keywords…',
|
||||
starttyping: 'Starting typing to search.',
|
||||
nooptions: 'No results.',
|
||||
blacklistedTagImportTitle: 'Import Blacklisted Tag Configuration',
|
||||
blacklistedTagImportInstructions: 'Paste blacklist tag configuration below.',
|
||||
valueRequired: 'You must provide a value.',
|
||||
noSpecialCharacters:
|
||||
'Configuration must be a comma delimited list of TMDB keyword ids, and must not start or end with a comma.',
|
||||
invalidKeyword: '{keywordId} is not a TMDB keyword.',
|
||||
});
|
||||
|
||||
type SingleVal = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type BlacklistedTagsSelectorProps = {
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
const BlacklistedTagsSelector = ({
|
||||
defaultValue,
|
||||
}: BlacklistedTagsSelectorProps) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [value, setValue] = useState<string | undefined>(defaultValue);
|
||||
const intl = useIntl();
|
||||
const [selectorValue, setSelectorValue] =
|
||||
useState<MultiValue<SingleVal> | null>(null);
|
||||
|
||||
const update = useCallback(
|
||||
(value: MultiValue<SingleVal> | null) => {
|
||||
const strVal = value?.map((v) => v.value).join(',');
|
||||
setSelectorValue(value);
|
||||
setValue(strVal);
|
||||
setFieldValue('blacklistedTags', strVal);
|
||||
},
|
||||
[setSelectorValue, setValue, setFieldValue]
|
||||
);
|
||||
|
||||
const copyDisabled = value === null || value?.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ControlledKeywordSelector
|
||||
value={selectorValue}
|
||||
onChange={update}
|
||||
defaultValue={defaultValue}
|
||||
components={{
|
||||
DropdownIndicator: undefined,
|
||||
IndicatorSeparator: undefined,
|
||||
ClearIndicator: VerifyClearIndicator,
|
||||
}}
|
||||
/>
|
||||
|
||||
<CopyButton
|
||||
textToCopy={value ?? ''}
|
||||
disabled={copyDisabled}
|
||||
toastMessage={intl.formatMessage(messages.copyBlacklistedTags)}
|
||||
tooltipContent={intl.formatMessage(
|
||||
copyDisabled
|
||||
? messages.copyBlacklistedTagsEmpty
|
||||
: messages.copyBlacklistedTagsTip
|
||||
)}
|
||||
tooltipConfig={{ followCursor: false }}
|
||||
/>
|
||||
<BlacklistedTagsImportButton setSelector={update} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type BaseSelectorMultiProps = {
|
||||
defaultValue?: string;
|
||||
value: MultiValue<SingleVal> | null;
|
||||
onChange: (value: MultiValue<SingleVal> | null) => void;
|
||||
components?: Partial<typeof components>;
|
||||
};
|
||||
|
||||
const ControlledKeywordSelector = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
components,
|
||||
value,
|
||||
}: BaseSelectorMultiProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefaultKeywords = async (): Promise<void> => {
|
||||
if (!defaultValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keywords = await Promise.all(
|
||||
defaultValue.split(',').map(async (keywordId) => {
|
||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const keyword: Keyword = await res.json();
|
||||
|
||||
return keyword;
|
||||
})
|
||||
);
|
||||
|
||||
onChange(
|
||||
keywords.map((keyword) => ({
|
||||
label: keyword.name,
|
||||
value: keyword.id,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
loadDefaultKeywords();
|
||||
}, [defaultValue, onChange]);
|
||||
|
||||
const loadKeywordOptions = async (inputValue: string) => {
|
||||
const res = await fetch(
|
||||
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const results: TmdbKeywordSearchResponse = await res.json();
|
||||
|
||||
return results.results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
key={`keyword-select-blacklistedTags`}
|
||||
inputId="data"
|
||||
isMulti
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
noOptionsMessage={({ inputValue }) =>
|
||||
inputValue === ''
|
||||
? intl.formatMessage(messages.starttyping)
|
||||
: intl.formatMessage(messages.nooptions)
|
||||
}
|
||||
value={value}
|
||||
loadOptions={loadKeywordOptions}
|
||||
placeholder={intl.formatMessage(messages.searchKeywords)}
|
||||
onChange={onChange}
|
||||
components={components}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type BlacklistedTagsImportButtonProps = {
|
||||
setSelector: (value: MultiValue<SingleVal>) => void;
|
||||
};
|
||||
|
||||
const BlacklistedTagsImportButton = ({
|
||||
setSelector,
|
||||
}: BlacklistedTagsImportButtonProps) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const intl = useIntl();
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
if (formRef.current) {
|
||||
if (await formRef.current.submitForm()) {
|
||||
setShow(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setShow(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
>
|
||||
<Modal
|
||||
title={intl.formatMessage(messages.blacklistedTagImportTitle)}
|
||||
okText="Confirm"
|
||||
onOk={onConfirm}
|
||||
onCancel={() => setShow(false)}
|
||||
>
|
||||
<BlacklistedTagImportForm ref={formRef} setSelector={setSelector} />
|
||||
</Modal>
|
||||
</Transition>
|
||||
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.importBlacklistedTagsTip)}
|
||||
tooltipConfig={{ followCursor: false }}
|
||||
>
|
||||
<button className="input-action" onClick={onClick} type="button">
|
||||
<ArrowDownIcon />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type BlacklistedTagImportFormProps = BlacklistedTagsImportButtonProps;
|
||||
|
||||
const BlacklistedTagImportForm = forwardRef<
|
||||
Partial<HTMLFormElement>,
|
||||
BlacklistedTagImportFormProps
|
||||
>((props, ref) => {
|
||||
const { setSelector } = props;
|
||||
const intl = useIntl();
|
||||
const [formValue, setFormValue] = useState('');
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submitForm: handleSubmit,
|
||||
formValue,
|
||||
}));
|
||||
|
||||
const validate = async () => {
|
||||
if (formValue.length === 0) {
|
||||
setErrors([intl.formatMessage(messages.valueRequired)]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^(?:\d+,)*\d+$/.test(formValue)) {
|
||||
setErrors([intl.formatMessage(messages.noSpecialCharacters)]);
|
||||
return false;
|
||||
}
|
||||
|
||||
const keywords = await Promise.allSettled(
|
||||
formValue.split(',').map(async (keywordId) => {
|
||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
||||
if (!res.ok) {
|
||||
throw intl.formatMessage(messages.invalidKeyword, { keywordId });
|
||||
}
|
||||
|
||||
const keyword: Keyword = await res.json();
|
||||
return {
|
||||
label: keyword.name,
|
||||
value: keyword.id,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const failures = keywords.filter(
|
||||
(res) => res.status === 'rejected'
|
||||
) as PromiseRejectedResult[];
|
||||
if (failures.length > 0) {
|
||||
setErrors(failures.map((failure) => `${failure.reason}`));
|
||||
return false;
|
||||
}
|
||||
|
||||
setSelector(
|
||||
(keywords as PromiseFulfilledResult<SingleVal>[]).map(
|
||||
(keyword) => keyword.value
|
||||
)
|
||||
);
|
||||
|
||||
setErrors([]);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = validate;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="value">
|
||||
{intl.formatMessage(messages.blacklistedTagImportInstructions)}
|
||||
</label>
|
||||
<textarea
|
||||
id="value"
|
||||
value={formValue}
|
||||
onChange={(e) => setFormValue(e.target.value)}
|
||||
className="h-20"
|
||||
/>
|
||||
{errors.length > 0 && (
|
||||
<div className="error">
|
||||
{errors.map((error) => (
|
||||
<div key={error}>{error}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
const VerifyClearIndicator = <
|
||||
Option,
|
||||
IsMuti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: ClearIndicatorProps<Option, IsMuti, Group>
|
||||
) => {
|
||||
const { clearValue } = props;
|
||||
const [show, setShow] = useState(false);
|
||||
const intl = useIntl();
|
||||
|
||||
const openForm = useCallback(() => {
|
||||
setShow(true);
|
||||
}, [setShow]);
|
||||
|
||||
const openFormKey = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (show) return;
|
||||
|
||||
if (event.key === 'Enter' || event.key === 'Space') {
|
||||
setShow(true);
|
||||
}
|
||||
},
|
||||
[setShow, show]
|
||||
);
|
||||
|
||||
const acceptForm = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
clearValue();
|
||||
}
|
||||
},
|
||||
[clearValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
window.addEventListener('keydown', acceptForm);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', acceptForm);
|
||||
}, [show, acceptForm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openForm}
|
||||
onKeyDown={openFormKey}
|
||||
className="react-select__indicator react-select__clear-indicator css-1xc3v61-indicatorContainer cursor-pointer"
|
||||
>
|
||||
<components.CrossIcon />
|
||||
</button>
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
>
|
||||
<Modal
|
||||
subTitle={intl.formatMessage(messages.clearBlacklistedTagsConfirm)}
|
||||
okText={intl.formatMessage(messages.yes)}
|
||||
cancelText={intl.formatMessage(messages.no)}
|
||||
onOk={clearValue}
|
||||
onCancel={() => setShow(false)}
|
||||
>
|
||||
<form />{' '}
|
||||
{/* Form prevents accidentally saving settings when pressing enter */}
|
||||
</Modal>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlacklistedTagsSelector;
|
||||
@@ -26,6 +26,7 @@ const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => {
|
||||
<>
|
||||
<Component
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -124,7 +124,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
{({ errors, touched, isSubmitting, isValid }) => {
|
||||
return (
|
||||
<>
|
||||
<Form>
|
||||
<Form data-form-type="login">
|
||||
<div>
|
||||
<h2 className="mb-6 -mt-1 text-center text-lg font-bold text-neutral-200">
|
||||
{intl.formatMessage(messages.loginwithapp, {
|
||||
@@ -140,6 +140,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||
data-form-type="username"
|
||||
/>
|
||||
</div>
|
||||
{errors.username && touched.username && (
|
||||
@@ -157,6 +158,9 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
autoComplete="current-password"
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||
data-form-type="password"
|
||||
data-1pignore="false"
|
||||
data-lpignore="false"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
@@ -75,7 +75,7 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
{({ errors, touched, isSubmitting, isValid }) => {
|
||||
return (
|
||||
<>
|
||||
<Form>
|
||||
<Form data-form-type="login">
|
||||
<div>
|
||||
<h2 className="mb-6 -mt-1 text-center text-lg font-bold text-neutral-200">
|
||||
{intl.formatMessage(messages.loginwithapp, {
|
||||
@@ -94,6 +94,7 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
type="text"
|
||||
inputMode="email"
|
||||
data-testid="email"
|
||||
data-form-type="username,email"
|
||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
@@ -113,10 +114,10 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
autoComplete="current-password"
|
||||
data-testid="password"
|
||||
data-form-type="password"
|
||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||
data-1pignore="false"
|
||||
data-lpignore="false"
|
||||
data-bwignore="false"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
@@ -590,7 +590,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
buttonSize={'md'}
|
||||
onClick={() => setShowBlacklistModal(true)}
|
||||
>
|
||||
<EyeSlashIcon className={'h-3'} />
|
||||
<EyeSlashIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -608,9 +608,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
onClick={onClickWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
<Spinner />
|
||||
) : (
|
||||
<StarIcon className={'h-3 text-amber-300'} />
|
||||
<StarIcon className={'text-amber-300'} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -623,17 +623,15 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
buttonSize={'md'}
|
||||
onClick={onClickDeleteWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
) : (
|
||||
<MinusCircleIcon className={'h-3'} />
|
||||
)}
|
||||
{isUpdating ? <Spinner /> : <MinusCircleIcon />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<PlayButton links={mediaLinks} />
|
||||
<div className="z-20">
|
||||
<PlayButton links={mediaLinks} />
|
||||
</div>
|
||||
<RequestButton
|
||||
mediaType="movie"
|
||||
media={data.mediaInfo}
|
||||
|
||||
@@ -17,9 +17,10 @@ import {
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import Link from 'next/link';
|
||||
@@ -293,9 +294,16 @@ const RequestItemError = ({
|
||||
interface RequestItemProps {
|
||||
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
||||
revalidateList: () => void;
|
||||
radarrData?: RadarrSettings[];
|
||||
sonarrData?: SonarrSettings[];
|
||||
}
|
||||
|
||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
const RequestItem = ({
|
||||
request,
|
||||
revalidateList,
|
||||
radarrData,
|
||||
sonarrData,
|
||||
}: RequestItemProps) => {
|
||||
const settings = useSettings();
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
@@ -390,6 +398,23 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const serviceExists = () => {
|
||||
if (title?.mediaInfo) {
|
||||
if (title?.mediaInfo.mediaType === MediaType.MOVIE) {
|
||||
return (
|
||||
radarrData?.find((radarr) => radarr.id === request.serverId) !==
|
||||
undefined
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
sonarrData?.find((sonarr) => sonarr.id === request.serverId) !==
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!title && !error) {
|
||||
return (
|
||||
<div
|
||||
@@ -697,28 +722,30 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
)}
|
||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<>
|
||||
<ConfirmButton
|
||||
onClick={() => deleteRequest()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</ConfirmButton>
|
||||
<ConfirmButton
|
||||
onClick={() => deleteMediaFile()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>
|
||||
{intl.formatMessage(messages.removearr, {
|
||||
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
|
||||
})}
|
||||
</span>
|
||||
</ConfirmButton>
|
||||
</>
|
||||
<ConfirmButton
|
||||
onClick={() => deleteRequest()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
title?.mediaInfo?.serviceId &&
|
||||
serviceExists() && (
|
||||
<ConfirmButton
|
||||
onClick={() => deleteMediaFile()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>
|
||||
{intl.formatMessage(messages.removearr, {
|
||||
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
|
||||
})}
|
||||
</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
FunnelIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -51,7 +53,7 @@ const RequestList = () => {
|
||||
const { user } = useUser({
|
||||
id: Number(router.query.userId),
|
||||
});
|
||||
const { user: currentUser } = useUser();
|
||||
const { user: currentUser, hasPermission } = useUser();
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||
const [currentSortDirection, setCurrentSortDirection] =
|
||||
@@ -62,6 +64,13 @@ const RequestList = () => {
|
||||
const pageIndex = page - 1;
|
||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||
|
||||
const { data: radarrData } = useSWR<RadarrSettings[]>(
|
||||
hasPermission(Permission.ADMIN) ? '/api/v1/settings/radarr' : null
|
||||
);
|
||||
const { data: sonarrData } = useSWR<SonarrSettings[]>(
|
||||
hasPermission(Permission.ADMIN) ? '/api/v1/settings/sonarr' : null
|
||||
);
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
@@ -245,6 +254,8 @@ const RequestList = () => {
|
||||
<RequestItem
|
||||
request={request}
|
||||
revalidateList={() => revalidate()}
|
||||
radarrData={radarrData}
|
||||
sonarrData={sonarrData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -578,7 +578,10 @@ export const UserSelector = ({
|
||||
|
||||
const users = defaultValue.split(',');
|
||||
|
||||
const res = await fetch(`/api/v1/user`);
|
||||
const res = await fetch(
|
||||
`/api/v1/user?includeIds=${encodeURIComponent(defaultValue)}`
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
@@ -1,54 +1,40 @@
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/solid';
|
||||
import React, { useEffect } from 'react';
|
||||
import type { Config } from 'react-popper-tooltip';
|
||||
import { useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useClipboard from 'react-use-clipboard';
|
||||
|
||||
type CopyButtonProps = {
|
||||
textToCopy: string;
|
||||
disabled?: boolean;
|
||||
toastMessage?: string;
|
||||
const messages = defineMessages('components.Settings', {
|
||||
copied: 'Copied API key to clipboard.',
|
||||
});
|
||||
|
||||
tooltipContent?: React.ReactNode;
|
||||
tooltipConfig?: Partial<Config>;
|
||||
};
|
||||
|
||||
const CopyButton = ({
|
||||
textToCopy,
|
||||
disabled,
|
||||
toastMessage,
|
||||
tooltipContent,
|
||||
tooltipConfig,
|
||||
}: CopyButtonProps) => {
|
||||
const CopyButton = ({ textToCopy }: { textToCopy: string }) => {
|
||||
const intl = useIntl();
|
||||
const [isCopied, setCopied] = useClipboard(textToCopy, {
|
||||
successDuration: 1000,
|
||||
});
|
||||
const { addToast } = useToasts();
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied && toastMessage) {
|
||||
addToast(toastMessage, {
|
||||
if (isCopied) {
|
||||
addToast(intl.formatMessage(messages.copied), {
|
||||
appearance: 'info',
|
||||
autoDismiss: true,
|
||||
});
|
||||
}
|
||||
}, [isCopied, addToast, toastMessage]);
|
||||
}, [isCopied, addToast, intl]);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltipContent} tooltipConfig={tooltipConfig}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCopied();
|
||||
}}
|
||||
className="input-action"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
>
|
||||
<ClipboardDocumentIcon />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCopied();
|
||||
}}
|
||||
className="input-action"
|
||||
>
|
||||
<ClipboardDocumentIcon />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@ const NotificationsDiscord = () => {
|
||||
type="text"
|
||||
placeholder={settings.currentSettings.applicationTitle}
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -221,6 +221,7 @@ const NotificationsEmail = () => {
|
||||
requireTls: values.encryption === 'opportunistic',
|
||||
authUser: values.authUser,
|
||||
authPass: values.authPass,
|
||||
allowSelfSigned: values.allowSelfSigned,
|
||||
senderName: values.senderName,
|
||||
pgpPrivateKey: values.pgpPrivateKey,
|
||||
pgpPassword: values.pgpPassword,
|
||||
@@ -296,6 +297,7 @@ const NotificationsEmail = () => {
|
||||
type="text"
|
||||
inputMode="email"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -321,6 +323,7 @@ const NotificationsEmail = () => {
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -346,6 +349,7 @@ const NotificationsEmail = () => {
|
||||
inputMode="numeric"
|
||||
className="short"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -407,6 +411,7 @@ const NotificationsEmail = () => {
|
||||
name="authUser"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -446,6 +451,7 @@ const NotificationsEmail = () => {
|
||||
rows="10"
|
||||
className="font-mono text-xs"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -477,6 +483,7 @@ const NotificationsEmail = () => {
|
||||
id="pgpPassword"
|
||||
name="pgpPassword"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -269,6 +269,7 @@ const NotificationsTelegram = () => {
|
||||
name="botUsername"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -307,6 +308,7 @@ const NotificationsTelegram = () => {
|
||||
name="chatId"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -127,18 +127,18 @@ const OverrideRuleTiles = ({
|
||||
})
|
||||
);
|
||||
setKeywords(keywords);
|
||||
const users = await Promise.all(
|
||||
rules
|
||||
.map((rule) => rule.users?.split(','))
|
||||
.flat()
|
||||
.filter((userId) => userId)
|
||||
.map(async (userId) => {
|
||||
const res = await fetch(`/api/v1/user/${userId}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const user: User = await res.json();
|
||||
return user;
|
||||
})
|
||||
);
|
||||
const allUsersFromRules = rules
|
||||
.map((rule) => rule.users)
|
||||
.filter((users) => users)
|
||||
.join(',');
|
||||
if (allUsersFromRules) {
|
||||
const res = await fetch(
|
||||
`/api/v1/user?includeIds=${encodeURIComponent(allUsersFromRules)}`
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
const users: User[] = (await res.json()).results;
|
||||
setUsers(users);
|
||||
}
|
||||
setUsers(users);
|
||||
})();
|
||||
}, [rules]);
|
||||
|
||||
@@ -383,6 +383,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -67,14 +67,11 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
|
||||
'download-sync': 'Download Sync',
|
||||
'download-sync-reset': 'Download Sync Reset',
|
||||
'image-cache-cleanup': 'Image Cache Cleanup',
|
||||
'process-blacklisted-tags': 'Process Blacklisted Tags',
|
||||
editJobSchedule: 'Modify Job',
|
||||
jobScheduleEditSaved: 'Job edited successfully!',
|
||||
jobScheduleEditFailed: 'Something went wrong while saving the job.',
|
||||
editJobScheduleCurrent: 'Current Frequency',
|
||||
editJobSchedulePrompt: 'New Frequency',
|
||||
editJobScheduleSelectorDays:
|
||||
'Every {jobScheduleDays, plural, one {day} other {{jobScheduleDays} days}}',
|
||||
editJobScheduleSelectorHours:
|
||||
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
||||
editJobScheduleSelectorMinutes:
|
||||
@@ -94,7 +91,7 @@ interface Job {
|
||||
id: JobId;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'seconds' | 'minutes' | 'hours' | 'days' | 'fixed';
|
||||
interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
|
||||
cronSchedule: string;
|
||||
nextExecutionTime: string;
|
||||
running: boolean;
|
||||
@@ -103,20 +100,13 @@ interface Job {
|
||||
type JobModalState = {
|
||||
isOpen?: boolean;
|
||||
job?: Job;
|
||||
scheduleDays: number;
|
||||
scheduleHours: number;
|
||||
scheduleMinutes: number;
|
||||
scheduleSeconds: number;
|
||||
};
|
||||
|
||||
type JobModalAction =
|
||||
| {
|
||||
type: 'set';
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
}
|
||||
| { type: 'set'; hours?: number; minutes?: number; seconds?: number }
|
||||
| {
|
||||
type: 'close';
|
||||
}
|
||||
@@ -137,7 +127,6 @@ const jobModalReducer = (
|
||||
return {
|
||||
isOpen: true,
|
||||
job: action.job,
|
||||
scheduleDays: 1,
|
||||
scheduleHours: 1,
|
||||
scheduleMinutes: 5,
|
||||
scheduleSeconds: 30,
|
||||
@@ -146,7 +135,6 @@ const jobModalReducer = (
|
||||
case 'set':
|
||||
return {
|
||||
...state,
|
||||
scheduleDays: action.days ?? state.scheduleDays,
|
||||
scheduleHours: action.hours ?? state.scheduleHours,
|
||||
scheduleMinutes: action.minutes ?? state.scheduleMinutes,
|
||||
scheduleSeconds: action.seconds ?? state.scheduleSeconds,
|
||||
@@ -175,7 +163,6 @@ const SettingsJobs = () => {
|
||||
|
||||
const [jobModalState, dispatch] = useReducer(jobModalReducer, {
|
||||
isOpen: false,
|
||||
scheduleDays: 1,
|
||||
scheduleHours: 1,
|
||||
scheduleMinutes: 5,
|
||||
scheduleSeconds: 30,
|
||||
@@ -260,9 +247,6 @@ const SettingsJobs = () => {
|
||||
jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`;
|
||||
} else if (jobModalState.job?.interval === 'hours') {
|
||||
jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`;
|
||||
} else if (jobModalState.job?.interval === 'days') {
|
||||
jobScheduleCron[2] = '1';
|
||||
jobScheduleCron[3] = `*/${jobModalState.scheduleDays}`;
|
||||
} else {
|
||||
// jobs with interval: fixed should not be editable
|
||||
throw new Error();
|
||||
@@ -398,29 +382,6 @@ const SettingsJobs = () => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : jobModalState.job?.interval === 'days' ? (
|
||||
<select
|
||||
name="jobScheduleDays"
|
||||
className="inline"
|
||||
value={jobModalState.scheduleDays}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: 'set',
|
||||
days: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 10, 14, 21].map((v) => (
|
||||
<option value={v} key={`jobScheduleDays-${v}`}>
|
||||
{intl.formatMessage(
|
||||
messages.editJobScheduleSelectorDays,
|
||||
{
|
||||
jobScheduleDays: v,
|
||||
}
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<select
|
||||
name="jobScheduleHours"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import BlacklistedTagsSelector from '@app/components/BlacklistedTagsSelector';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
@@ -29,19 +28,12 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
||||
generalsettingsDescription:
|
||||
'Configure global and default settings for Jellyseerr.',
|
||||
apikey: 'API Key',
|
||||
apikeyCopied: 'Copied API key to clipboard.',
|
||||
applicationTitle: 'Application Title',
|
||||
applicationurl: 'Application URL',
|
||||
discoverRegion: 'Discover Region',
|
||||
discoverRegionTip: 'Filter content by regional availability',
|
||||
originallanguage: 'Discover Language',
|
||||
originallanguageTip: 'Filter content by original language',
|
||||
blacklistedTags: 'Blacklist Content with Tags',
|
||||
blacklistedTagsTip:
|
||||
'Automatically add content with tags to the blacklist using the "Process Blacklisted Tags" job',
|
||||
blacklistedTagsLimit: 'Limit Content Blacklisted per Tag',
|
||||
blacklistedTagsLimitTip:
|
||||
'The "Process Blacklisted Tags" job will blacklist this many pages into each sort. Larger numbers will create a more accurate blacklist, but use more space.',
|
||||
streamingRegion: 'Streaming Region',
|
||||
streamingRegionTip: 'Show streaming sites by regional availability',
|
||||
toastApiKeySuccess: 'New API key generated successfully!',
|
||||
@@ -88,17 +80,6 @@ const SettingsMain = () => {
|
||||
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
blacklistedTagsLimit: Yup.number()
|
||||
.test(
|
||||
'positive',
|
||||
'Number must be greater than 0.',
|
||||
(value) => (value ?? 0) >= 0
|
||||
)
|
||||
.test(
|
||||
'lte-250',
|
||||
'Number must be less than or equal to 250.',
|
||||
(value) => (value ?? 0) <= 250
|
||||
),
|
||||
});
|
||||
|
||||
const regenerate = async () => {
|
||||
@@ -151,8 +132,6 @@ const SettingsMain = () => {
|
||||
discoverRegion: data?.discoverRegion,
|
||||
originalLanguage: data?.originalLanguage,
|
||||
streamingRegion: data?.streamingRegion || 'US',
|
||||
blacklistedTags: data?.blacklistedTags,
|
||||
blacklistedTagsLimit: data?.blacklistedTagsLimit || 50,
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
||||
cacheImages: data?.cacheImages,
|
||||
@@ -174,8 +153,6 @@ const SettingsMain = () => {
|
||||
discoverRegion: values.discoverRegion,
|
||||
streamingRegion: values.streamingRegion,
|
||||
originalLanguage: values.originalLanguage,
|
||||
blacklistedTags: values.blacklistedTags,
|
||||
blacklistedTagsLimit: values.blacklistedTagsLimit,
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
enableSpecialEpisodes: values.enableSpecialEpisodes,
|
||||
cacheImages: values.cacheImages,
|
||||
@@ -233,9 +210,6 @@ const SettingsMain = () => {
|
||||
/>
|
||||
<CopyButton
|
||||
textToCopy={data?.apiKey ?? ''}
|
||||
toastMessage={intl.formatMessage(
|
||||
messages.apikeyCopied
|
||||
)}
|
||||
key={data?.apiKey}
|
||||
/>
|
||||
<button
|
||||
@@ -244,7 +218,6 @@ const SettingsMain = () => {
|
||||
regenerate();
|
||||
}}
|
||||
className="input-action"
|
||||
type="button"
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
</button>
|
||||
@@ -388,49 +361,6 @@ const SettingsMain = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="blacklistedTags" className="text-label">
|
||||
<span>{intl.formatMessage(messages.blacklistedTags)}</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.blacklistedTagsTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field relative z-10">
|
||||
<BlacklistedTagsSelector
|
||||
defaultValue={values.blacklistedTags}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="blacklistedTagsLimit" className="text-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.blacklistedTagsLimit)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.blacklistedTagsLimitTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
id="blacklistedTagsLimit"
|
||||
name="blacklistedTagsLimit"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className="short"
|
||||
placeholder={50}
|
||||
/>
|
||||
{errors.blacklistedTagsLimit &&
|
||||
touched.blacklistedTagsLimit &&
|
||||
typeof errors.blacklistedTagsLimit === 'string' && (
|
||||
<div className="error">
|
||||
{errors.blacklistedTagsLimit}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="hideAvailable" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
|
||||
@@ -45,9 +45,6 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
|
||||
forceIpv4First: 'Force IPv4 Resolution First',
|
||||
forceIpv4FirstTip:
|
||||
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
|
||||
dnsServers: 'Custom DNS Servers',
|
||||
dnsServersTip:
|
||||
'Comma-separated list of custom DNS servers, e.g. "1.1.1.1,[2606:4700:4700::1111]"',
|
||||
});
|
||||
|
||||
const SettingsNetwork = () => {
|
||||
@@ -93,7 +90,6 @@ const SettingsNetwork = () => {
|
||||
initialValues={{
|
||||
csrfProtection: data?.csrfProtection,
|
||||
forceIpv4First: data?.forceIpv4First,
|
||||
dnsServers: data?.dnsServers,
|
||||
trustProxy: data?.trustProxy,
|
||||
proxyEnabled: data?.proxy?.enabled,
|
||||
proxyHostname: data?.proxy?.hostname,
|
||||
@@ -116,7 +112,6 @@ const SettingsNetwork = () => {
|
||||
body: JSON.stringify({
|
||||
csrfProtection: values.csrfProtection,
|
||||
forceIpv4First: values.forceIpv4First,
|
||||
dnsServers: values.dnsServers,
|
||||
trustProxy: values.trustProxy,
|
||||
proxy: {
|
||||
enabled: values.proxyEnabled,
|
||||
@@ -426,34 +421,6 @@ const SettingsNetwork = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="dnsServers" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.dnsServers)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
<SettingsBadge badgeType="experimental" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.dnsServersTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="dnsServers"
|
||||
name="dnsServers"
|
||||
type="text"
|
||||
inputMode="url"
|
||||
/>
|
||||
</div>
|
||||
{errors.dnsServers &&
|
||||
touched.dnsServers &&
|
||||
typeof errors.dnsServers === 'string' && (
|
||||
<div className="error">{errors.dnsServers}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -873,6 +873,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
name="tautulliPort"
|
||||
className="short"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -914,6 +915,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
id="tautulliUrlBase"
|
||||
name="tautulliUrlBase"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
@@ -958,6 +960,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
id="tautulliExternalUrl"
|
||||
name="tautulliExternalUrl"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -528,6 +528,7 @@ const SettingsServices = () => {
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
disabled={!radarrData?.length && !sonarrData?.length}
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({
|
||||
open: true,
|
||||
|
||||
@@ -416,6 +416,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -198,6 +198,11 @@ function JellyfinSetup({
|
||||
messages.hostname,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname && touched.hostname && (
|
||||
@@ -282,6 +287,11 @@ function JellyfinSetup({
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(messages.email)}
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
/>
|
||||
</div>
|
||||
{errors.email && touched.email && (
|
||||
@@ -298,6 +308,11 @@ function JellyfinSetup({
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
/>
|
||||
</div>
|
||||
{errors.username && touched.username && (
|
||||
@@ -314,6 +329,11 @@ function JellyfinSetup({
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
/>
|
||||
</div>
|
||||
{errors.password && touched.password && (
|
||||
|
||||
@@ -373,11 +373,10 @@ const TitleCard = ({
|
||||
: intl.formatMessage(globalMessages.tvshow)}
|
||||
</div>
|
||||
</div>
|
||||
{showDetail &&
|
||||
currentStatus !== MediaStatus.BLACKLISTED &&
|
||||
user?.userType !== UserType.PLEX && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{toggleWatchlist ? (
|
||||
{showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{user?.userType !== UserType.PLEX &&
|
||||
(toggleWatchlist ? (
|
||||
<Button
|
||||
buttonType={'ghost'}
|
||||
className="z-40"
|
||||
@@ -394,23 +393,23 @@ const TitleCard = ({
|
||||
>
|
||||
<MinusCircleIcon className={'h-3'} />
|
||||
</Button>
|
||||
))}
|
||||
{showHideButton &&
|
||||
currentStatus !== MediaStatus.PROCESSING &&
|
||||
currentStatus !== MediaStatus.AVAILABLE &&
|
||||
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
|
||||
currentStatus !== MediaStatus.PENDING && (
|
||||
<Button
|
||||
buttonType={'ghost'}
|
||||
className="z-40"
|
||||
buttonSize={'sm'}
|
||||
onClick={() => setShowBlacklistModal(true)}
|
||||
>
|
||||
<EyeSlashIcon className={'h-3'} />
|
||||
</Button>
|
||||
)}
|
||||
{showHideButton &&
|
||||
currentStatus !== MediaStatus.PROCESSING &&
|
||||
currentStatus !== MediaStatus.AVAILABLE &&
|
||||
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
|
||||
currentStatus !== MediaStatus.PENDING && (
|
||||
<Button
|
||||
buttonType={'ghost'}
|
||||
className="z-40"
|
||||
buttonSize={'sm'}
|
||||
onClick={() => setShowBlacklistModal(true)}
|
||||
>
|
||||
<EyeSlashIcon className={'h-3'} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showDetail &&
|
||||
showHideButton &&
|
||||
currentStatus == MediaStatus.BLACKLISTED && (
|
||||
|
||||
@@ -632,7 +632,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
buttonSize={'md'}
|
||||
onClick={() => setShowBlacklistModal(true)}
|
||||
>
|
||||
<EyeSlashIcon className={'h-3'} />
|
||||
<EyeSlashIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -650,9 +650,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
onClick={onClickWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
<Spinner />
|
||||
) : (
|
||||
<StarIcon className={'h-3 text-amber-300'} />
|
||||
<StarIcon className={'text-amber-300'} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -665,17 +665,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
buttonSize={'md'}
|
||||
onClick={onClickDeleteWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
) : (
|
||||
<MinusCircleIcon className={'h-3'} />
|
||||
)}
|
||||
{isUpdating ? <Spinner /> : <MinusCircleIcon />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<PlayButton links={mediaLinks} />
|
||||
<div className="z-20">
|
||||
<PlayButton links={mediaLinks} />
|
||||
</div>
|
||||
<RequestButton
|
||||
mediaType="tv"
|
||||
onUpdate={() => revalidate()}
|
||||
|
||||
@@ -400,6 +400,7 @@ const UserList = () => {
|
||||
type="text"
|
||||
inputMode="email"
|
||||
autoComplete="off"
|
||||
data-form-type="other"
|
||||
data-1pignore="true"
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
|
||||
@@ -415,7 +415,7 @@ const UserGeneralSettings = () => {
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<div className="form-input-field relative z-30">
|
||||
<RegionSelector
|
||||
name="discoverRegion"
|
||||
value={values.discoverRegion ?? ''}
|
||||
@@ -451,7 +451,7 @@ const UserGeneralSettings = () => {
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<div className="form-input-field relative z-20">
|
||||
<RegionSelector
|
||||
name="streamingRegion"
|
||||
value={values.streamingRegion || ''}
|
||||
|
||||
@@ -781,7 +781,7 @@
|
||||
"components.Settings.externalUrl": "Externí adresa URL",
|
||||
"components.Settings.hostname": "Název hostitele nebo IP adresa",
|
||||
"components.Settings.manualscan": "Manuální skenování knihovny",
|
||||
"components.Settings.plexlibrariesDescription": "Knihovny Jellyseerr vyhledává tituly. Nastavte a uložte nastavení připojení k systému Plex a poté klikněte na tlačítko níže, pokud nejsou v seznamu uvedeny žádné knihovny.",
|
||||
"components.Settings.plexlibrariesDescription": "Knihovny ve kterých Jellyseerr vyhledává tituly. Nastavte a uložte nastavení připojení k Plex serveru a poté klikněte na tlačítko níže, pokud nejsou v seznamu uvedeny žádné knihovny.",
|
||||
"components.Settings.serverpresetLoad": "Stisknutím tlačítka načtete dostupné servery",
|
||||
"components.Settings.toastTautulliSettingsFailure": "Při ukládání nastavení Tautulli se něco pokazilo.",
|
||||
"components.Settings.webAppUrl": "<WebAppLink>Webová aplikace</WebAppLink> Adresa URL",
|
||||
@@ -795,7 +795,7 @@
|
||||
"components.UserList.deleteconfirm": "Opravdu chcete tohoto uživatele odstranit? Všechny údaje o jeho žádosti budou trvale odstraněny.",
|
||||
"components.UserList.localLoginDisabled": "Nastavení <strong>Povolit místní přihlášení</strong> je v současné době zakázáno.",
|
||||
"components.UserList.userssaved": "Uživatelská oprávnění byla úspěšně uložena!",
|
||||
"components.UserList.validationEmail": "Musíte zadat platnou e-mailovou adresu",
|
||||
"components.UserList.validationEmail": "Emailová adresa je povinná",
|
||||
"components.UserProfile.ProfileHeader.userid": "ID uživatele: {userid}",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Limit požadavků na filmy",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Limit požadavků na sérii",
|
||||
@@ -831,7 +831,7 @@
|
||||
"components.Settings.noDefault4kServer": "Server 4K {serverType} musí být označen jako výchozí, aby uživatelé mohli odesílat požadavky 4K {mediaType}.",
|
||||
"components.Settings.noDefaultNon4kServer": "Pokud máte pouze jeden server {serverType} pro obsah jiný než 4K i 4K (nebo pokud stahujete pouze obsah 4K), váš server {serverType} by <strong>neměl</strong> být označen jako server 4K.",
|
||||
"components.Settings.noDefaultServer": "Aby mohly být zpracovány požadavky typu {mediaType}, musí být alespoň jeden server typu {serverType} označen jako výchozí.",
|
||||
"components.Settings.plexsettingsDescription": "Knihovny Jellyseerr vyhledává tituly. Nastavte a uložte nastavení připojení k systému Plex a poté klikněte na tlačítko níže, pokud nejsou v seznamu uvedeny žádné knihovny.",
|
||||
"components.Settings.plexsettingsDescription": "Nastavte připojení k Plex serveru. Jellyseerr prohledá knihovny Plex serveru aby zjistil dostupnost obsahu.",
|
||||
"components.Settings.toastPlexRefresh": "Získání seznamu serverů z aplikace Plex…",
|
||||
"components.Settings.toastPlexRefreshSuccess": "Seznam serverů Plex úspěšně načten!",
|
||||
"components.UserList.passwordinfodescription": "Nakonfigurujte adresu URL aplikace a povolte e-mailová oznámení, která umožní automatické generování hesla.",
|
||||
@@ -1023,7 +1023,7 @@
|
||||
"components.PermissionEdit.autorequestMovies": "Automatické vyžádání filmů",
|
||||
"components.PermissionEdit.autorequestMoviesDescription": "Udělte oprávnění k automatickému odesílání žádostí o filmy v jiném rozlišení než 4K prostřednictvím Plex Watchlistu.",
|
||||
"components.PermissionEdit.viewrecentDescription": "Udělte oprávnění k zobrazení seznamu nedávno přidaných médií.",
|
||||
"components.Settings.restartrequiredTooltip": "Aby se změny tohoto nastavení projevily, musí být Overserr restartován",
|
||||
"components.Settings.restartrequiredTooltip": "Aby se projevily změny tohoto nastavení, musí být Jellyseerr restartován",
|
||||
"components.StatusChecker.reloadApp": "Znovu načíst {applicationTitle}",
|
||||
"components.TitleCard.cleardata": "Vyčistit data",
|
||||
"components.TitleCard.mediaerror": "{mediaType} Nenalezeno",
|
||||
@@ -1231,5 +1231,165 @@
|
||||
"components.Settings.SonarrModal.animeSeriesType": "Typ anime série",
|
||||
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Výchozí zařízení",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Výchozí zařízení",
|
||||
"components.Settings.SonarrModal.seriesType": "Typ série"
|
||||
"components.Settings.SonarrModal.seriesType": "Typ série",
|
||||
"components.Login.servertype": "Typ serveru",
|
||||
"components.Login.validationEmailFormat": "Neplatný email",
|
||||
"components.Login.validationUrlBaseLeadingSlash": "Základ URL adresy musí obsahovat lomítko",
|
||||
"components.Login.validationEmailRequired": "Musíte poskytnout email",
|
||||
"components.Login.validationPortRequired": "Musíte poskytnout platné číslo portu",
|
||||
"components.Login.back": "Vrátit se",
|
||||
"components.Login.validationUrlBaseTrailingSlash": "Základ URL adresy nesmí končit lomítkem",
|
||||
"components.Setup.back": "Vrátit se",
|
||||
"components.Login.validationusernamerequired": "Uživatelské jméno je povinné",
|
||||
"components.Setup.configemby": "Nastavit Emby",
|
||||
"components.PermissionEdit.blacklistedItemsDescription": "Udělit oprávnění přidávat média na černou listinu.",
|
||||
"components.Login.signinwithjellyfin": "Použít Váš {mediaServerName} účet",
|
||||
"components.Settings.Notifications.userEmailRequired": "Vyžadovat email uživatelů",
|
||||
"components.TitleCard.addToWatchList": "Přidat na seznam sledování",
|
||||
"components.TvDetails.removefromwatchlist": "Odstranit ze seznamu sledování",
|
||||
"components.Login.credentialerror": "Nesprávné uživatelské jméno nebo heslo.",
|
||||
"components.Setup.servertype": "Zvolte typ serveru",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Obvykle se provádí pouze jednou za 24 hodin. Jellyseerr bude kontrolovat nedávno přidané položky vašeho {mediaServerName} serveru agresivněji. Pokud nastavujete Jellyseerr poprvé, doporučujeme provést jednorázovou úplnou ruční kontrolu knihoven!",
|
||||
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Toto nenávratně odstraní tento {mediaType} z {arr}, včetně všech souborů.",
|
||||
"components.Blacklist.blacklistdate": "datum",
|
||||
"components.Blacklist.mediaName": "Jméno",
|
||||
"components.MovieDetails.watchlistDeleted": "<strong>{title}</strong> úspěšně odstraněno ze seznamu sledování!",
|
||||
"components.Settings.SettingsMain.validationProxyPort": "Musíte poskytnout platný port",
|
||||
"components.Settings.Notifications.validationWebhookRoleId": "Musíte poskytnout platné ID Discord role",
|
||||
"components.Blacklist.blacklistedby": "{date} uživatelem {user}",
|
||||
"components.Layout.UserWarnings.passwordRequired": "Heslo je povinné.",
|
||||
"components.Login.validationHostnameRequired": "Musíte poskytnout platné hostitelské jméno nebo IP adresu",
|
||||
"components.Selector.searchStatus": "Vyberte status…",
|
||||
"components.TvDetails.watchlistSuccess": "<strong>{title}</strong> úspěšně přidáno na seznam sledování!",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> není na černé listině.",
|
||||
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> úspěšně odstraněno ze seznamu sledování!",
|
||||
"component.BlacklistBlock.blacklistdate": "Datum přidání na černou listinu",
|
||||
"component.BlacklistBlock.blacklistedby": "Přidáno na černou listinu uživatelem",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Spravovat média na černé listině.",
|
||||
"components.Blacklist.mediaTmdbId": "tmdb ID",
|
||||
"components.Blacklist.mediaType": "Typ",
|
||||
"components.Blacklist.blacklistsettings": "Nastavení černé listiny",
|
||||
"components.Discover.FilterSlideover.status": "Status",
|
||||
"components.Layout.Sidebar.blacklist": "Černá listina",
|
||||
"components.Layout.UserWarnings.emailInvalid": "Emailová adresa je neplatná.",
|
||||
"components.Layout.UserWarnings.emailRequired": "Emailová adresa je povinná.",
|
||||
"components.Login.emailtooltip": "Adresa nemusí být asociována s Vaší {mediaServerName} instancí.",
|
||||
"components.Login.enablessl": "Používat SSL",
|
||||
"components.Login.initialsignin": "Připojit",
|
||||
"components.Login.initialsigningin": "Připojování…",
|
||||
"components.Login.invalidurlerror": "Nelze se připojit k {mediaServerName} serveru.",
|
||||
"components.Login.port": "Port",
|
||||
"components.Login.save": "Přidat",
|
||||
"components.Login.saving": "Přidávání…",
|
||||
"components.Login.username": "Uživatelské jméno",
|
||||
"components.Login.validationemailformat": "Je požadován platný email",
|
||||
"components.Login.validationhostformat": "Je požadována platná URL",
|
||||
"components.Login.validationhostrequired": "Je požadována {mediaServerName} URL",
|
||||
"components.Login.validationservertyperequired": "Prosím zvolte typ serveru",
|
||||
"components.Login.hostname": "{mediaServerName} URL",
|
||||
"components.Login.urlBase": "Základ URL adresy",
|
||||
"components.Login.validationUrlTrailingSlash": "URL adresa nesmí končit lomítkem",
|
||||
"components.ManageSlideOver.removearr": "Odstranit z {arr}",
|
||||
"components.ManageSlideOver.removearr4k": "Odstranit z 4K {arr}",
|
||||
"components.MovieDetails.addtowatchlist": "Přidat na seznam sledování",
|
||||
"components.MovieDetails.downloadstatus": "Stav stahování",
|
||||
"components.MovieDetails.openradarr": "Otevřít film v Radarr",
|
||||
"components.MovieDetails.openradarr4k": "Otevřít film ve 4K Radarr",
|
||||
"components.MovieDetails.removefromwatchlist": "Odstranit ze seznamu sledování",
|
||||
"components.MovieDetails.watchlistError": "Něco se pokazilo. Zkuste to znovu.",
|
||||
"components.MovieDetails.watchlistSuccess": "<strong>{title}</strong> úspěšně přidáno na seznam sledování!",
|
||||
"components.PermissionEdit.blacklistedItems": "Přidat média na černou listinu.",
|
||||
"components.PermissionEdit.manageblacklist": "Spravovat černou listinu",
|
||||
"components.PermissionEdit.manageblacklistDescription": "Udělit oprávnění spravovat černou listinu.",
|
||||
"components.PermissionEdit.viewblacklistedItems": "Zobrazit černou listinu.",
|
||||
"components.PermissionEdit.viewblacklistedItemsDescription": "Udělit oprávnění zobrazit černou listinu.",
|
||||
"components.RequestList.RequestItem.profileName": "Profil",
|
||||
"components.RequestList.RequestItem.removearr": "Odstranit z {arr}",
|
||||
"components.RequestList.sortDirection": "Přepnout směr řazení",
|
||||
"components.Selector.canceled": "Zrušeno",
|
||||
"components.Selector.inProduction": "V produkci",
|
||||
"components.Settings.SettingsAbout.supportjellyseerr": "Podpořit Jellyseerr",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Sken nedávno přidaných na Jellyfin",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Kompletní sken knihoven Jellyfin",
|
||||
"components.Settings.SettingsJobsCache.plex-refresh-token": "Obnovení Plex tokenu",
|
||||
"components.Settings.SettingsMain.discoverRegionTip": "Filtrovat obsah podle dostupnosti v regionu",
|
||||
"components.Settings.SettingsMain.proxyEnabled": "HTTP(S) proxy",
|
||||
"components.Settings.SettingsMain.proxySsl": "Používat SSL pro proxy",
|
||||
"components.Settings.SettingsMain.proxyPort": "Port proxy",
|
||||
"components.Settings.SettingsMain.proxyPassword": "Heslo proxy",
|
||||
"components.Settings.SettingsMain.proxyUser": "Uživatelské jméno proxy",
|
||||
"components.Settings.SettingsMain.streamingRegion": "Streamovací region",
|
||||
"components.Settings.SettingsMain.streamingRegionTip": "Zobrazit streamovací služby podle dostupnosti v regionu",
|
||||
"components.Settings.SettingsMain.discoverRegion": "Region objevování",
|
||||
"components.Settings.SettingsMain.proxyBypassLocalAddresses": "Obcházet proxy pro lokální adresy",
|
||||
"components.Settings.SettingsMain.proxyHostname": "Hostitelské jméno proxy",
|
||||
"components.Settings.apiKey": "API klíč",
|
||||
"components.Settings.invalidurlerror": "Nelze se připojit k {mediaServerName} serveru.",
|
||||
"components.Settings.jellyfinForgotPasswordUrl": "URL pro zapomenuté heslo",
|
||||
"components.Settings.jellyfinSettings": "Nastavení {mediaServerName}",
|
||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} nastavení úspěšně uloženo!",
|
||||
"components.Settings.jellyfinSyncFailedGenericError": "Něco se pokazilo při synchronizaci knihoven",
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "Nebyly nalezeny žádné knihovny",
|
||||
"components.Settings.jellyfinlibraries": "{mediaServerName} knihovny",
|
||||
"components.Settings.jellyfinsettings": "Nastavení {mediaServerName}",
|
||||
"components.Settings.manualscanJellyfin": "Manuální skenování knihovny",
|
||||
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
||||
"components.Settings.jellyfinlibrariesDescription": "Knihovny, ve kterých {mediaServerName} hledá tituly. Pokud nejsou v seznamu žádné knihovny, klikněte na tlačítko níže.",
|
||||
"components.Settings.jellyfinsettingsDescription": "Konfigurujte nastavení svého {mediaServerName} serveru. {mediaServerName} skenuje Vaše {mediaServerName} knihovny pro zjištění dostupnosti obsahu.",
|
||||
"components.Settings.save": "Uložit změny",
|
||||
"components.Settings.saving": "Ukládání…",
|
||||
"components.Settings.syncJellyfin": "Synchronizovat knihovny",
|
||||
"components.Settings.syncing": "Synchronizace",
|
||||
"components.Settings.tip": "Tip",
|
||||
"components.Setup.configjellyfin": "Nastavit Jellyfin",
|
||||
"components.Setup.configplex": "Nastavit Plex",
|
||||
"components.Setup.configuremediaserver": "Nastavit média server",
|
||||
"components.Setup.signin": "Přihlásit se",
|
||||
"components.Setup.signinWithEmby": "Zadejte údaje k Emby",
|
||||
"components.Setup.signinWithJellyfin": "Zadejte údaje k Jellyfin",
|
||||
"components.Setup.signinWithPlex": "Zadejte údaje k Plex",
|
||||
"components.Setup.subtitle": "Začněte výběrem média serveru",
|
||||
"components.StatusBadge.seasonnumber": "S{seasonNumber}",
|
||||
"components.TitleCard.watchlistError": "Něco se pokazilo. Zkuste to znovu.",
|
||||
"components.TvDetails.addtowatchlist": "Přidat na seznam sledování",
|
||||
"components.TitleCard.watchlistCancel": "seznam sledování pro <strong>{title}</strong> zrušen.",
|
||||
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> úspěšně přidáno na seznam sledování!",
|
||||
"components.TvDetails.watchlistDeleted": "<strong>{title}</strong> úspěšně odstraněno ze seznamu sledování!",
|
||||
"components.TvDetails.watchlistError": "Něco se pokazilo. Zkuste to znovu.",
|
||||
"components.UserList.importfromJellyfin": "Import uživatelů z {mediaServerName}",
|
||||
"components.UserList.importfromJellyfinerror": "Něco se pokazilo během importování uživatelů z {mediaServerName}.",
|
||||
"components.UserList.mediaServerUser": "{mediaServerName} uživatel",
|
||||
"components.UserList.noJellyfinuserstoimport": "{mediaServerName} nemá žádné uživatele k importování.",
|
||||
"components.UserList.username": "Uživatelské jméno",
|
||||
"components.UserList.validationUsername": "Musíte poskytnout uživatelské jméno",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Region pro objevování",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filtrovat obsah podle dostupnosti v regionu",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "{mediaServerName} uživatel",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Uložit změny",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Ukládání…",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Streamovací region",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Zobrazit streamovací služby podle dostupnosti v regionu",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "Tento email už je používán!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Jiný uživatel už používá toto uživatelské jméno. Musíte si nastavit email",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Je požadován platný email",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Emailová adresa je povinná",
|
||||
"components.UserProfile.localWatchlist": "Seznam sledování uživatele {username}",
|
||||
"i18n.addToBlacklist": "Přidat na černou listinu",
|
||||
"i18n.blacklistError": "Něco se pokazilo. Zkuste to znovu.",
|
||||
"i18n.blacklist": "Černá listina",
|
||||
"i18n.blacklistDuplicateError": "<strong>{title}</strong> už se nachází na černé listině.",
|
||||
"i18n.blacklistSuccess": "<strong>{title}</strong> bylo úspěšně přidáno na černou listinu.",
|
||||
"i18n.blacklisted": "Na černé listině",
|
||||
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> bylo úspěšně odstraněno z černé listiny.",
|
||||
"i18n.removefromBlacklist": "Odstranit z černé listiny",
|
||||
"components.Login.title": "Přidat email",
|
||||
"components.Login.adminerror": "Musíte se přihlásit administrátorským účtem.",
|
||||
"components.Login.description": "Protože je toto Vaše první přihlášení do {applicationName}, musíte přidat platnou emailovou adresu.",
|
||||
"components.Settings.jellyfinSettingsFailure": "Něco se pokazilo při ukládání nastavení {mediaServerName}.",
|
||||
"components.Settings.timeout": "Časový limit",
|
||||
"components.Selector.ended": "Ukončeno",
|
||||
"components.Selector.returningSeries": "Vracející se seriál",
|
||||
"components.Settings.SettingsJobsCache.usersavatars": "Uživatelské avatary",
|
||||
"i18n.specials": "Speciály"
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
"components.RequestModal.requestcancelled": "Forespørgslen for <strong>{title}</strong> er annulleret.",
|
||||
"components.RequestModal.requestedited": "Forespørgslen for <strong>{title}</strong> er redigeret!",
|
||||
"components.RequestModal.requesterror": "Noget gik galt under indsendelsen af forespørgslen.",
|
||||
"components.RequestModal.requestfrom": "{username}s forespørgsel afventer godkendelse.",
|
||||
"components.RequestModal.requestfrom": "{username}'s forespørgsel afventer godkendelse.",
|
||||
"components.RequestModal.requestseasons": "Forespørg om {seasonCount} {seasonCount, plural, one {Sæson} other {Sæsoner}}",
|
||||
"components.RequestModal.season": "Sæson",
|
||||
"components.ResetPassword.passwordreset": "Nulstil Kodeord",
|
||||
@@ -1227,5 +1227,13 @@
|
||||
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Enhedsstandard",
|
||||
"components.Settings.Notifications.NotificationsPushover.sound": "Notifikationslyd",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Enhedsstandard",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Notifikationslyd"
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Notifikationslyd",
|
||||
"components.UserList.validationUsername": "Angiv et brugernavn",
|
||||
"components.Login.signinwithjellyfin": "Brug din {mediaServerName} konto",
|
||||
"components.Login.username": "Brugernavn",
|
||||
"components.Login.validationEmailFormat": "Ugyldig e-mail",
|
||||
"components.Login.validationEmailRequired": "Angiv en e-mail",
|
||||
"components.Login.validationusernamerequired": "Brugernavn kræves",
|
||||
"components.UserList.username": "Brugernavn",
|
||||
"components.Login.credentialerror": "Brugernavnet eller kodeordet er forkert."
|
||||
}
|
||||
|
||||
@@ -449,8 +449,8 @@
|
||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Pushbullet-Benachrichtigungseinstellungen konnten nicht gespeichert werden.",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Pushbullet-Benachrichtigungseinstellungen erfolgreich gespeichert!",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Pushbullet Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Pushbullet test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Pushbullet Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Du musst ein Zugangstoken angeben",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "Sie müssen mindestens einen Benachrichtigungstypen auswählen",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessToken": "Anwendungs API-Token",
|
||||
@@ -479,8 +479,8 @@
|
||||
"components.Settings.Notifications.NotificationsWebPush.agentenabled": "Agent aktivieren",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Jellyseerr muss via HTTPS bereitgestellt werden, um Web-Push Benachrichtigungen empfangen zu können.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web push Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Web push test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Web push Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "Web push Benachrichtigungseinstellungen konnten nicht gespeichert werden.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "Web push Benachrichtigungseinstellungen erfolgreich gespeichert!",
|
||||
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Dienst aktivieren",
|
||||
@@ -489,8 +489,8 @@
|
||||
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Auf Standard zurücksetzen",
|
||||
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON-Inhalt erfolgreich zurückgesetzt!",
|
||||
"components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Hilfe zu Vorlagenvariablen",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "Webhook Benachrichtigungseinstellungen konnten nicht gespeichert werden.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Webhook test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "Webhook Test Benachrichtigung konnte nicht gesendet werden.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Webhook Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Webhook Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Du musst einen gültigen JSON-Inhalt angeben",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationTypes": "Sie müssen mindestens einen Benachrichtigungstypen auswählen",
|
||||
@@ -532,15 +532,15 @@
|
||||
"components.Settings.Notifications.smtpPort": "SMTP-Port",
|
||||
"components.Settings.Notifications.telegramsettingsfailed": "Telegram-Benachrichtigungseinstellungen konnten nicht gespeichert werden.",
|
||||
"components.Settings.Notifications.telegramsettingssaved": "Telegram-Benachrichtigungseinstellungen erfolgreich gespeichert!",
|
||||
"components.Settings.Notifications.toastDiscordTestFailed": "Discord test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.toastDiscordTestSending": "Discord test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.toastDiscordTestSuccess": "Discord test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.toastEmailTestFailed": "E-Mail test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.toastEmailTestSending": "Email test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.toastEmailTestSuccess": "Email test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.toastTelegramTestFailed": "Telegram test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.toastTelegramTestSending": "Telegram test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.toastTelegramTestSuccess": "Telegram test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.toastDiscordTestFailed": "Discord Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.toastDiscordTestSending": "Discord Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.toastDiscordTestSuccess": "Discord Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.toastEmailTestFailed": "E-Mail Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.toastEmailTestSending": "Email Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.toastEmailTestSuccess": "Email Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.toastTelegramTestFailed": "Telegram Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.toastTelegramTestSending": "Telegram Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.toastTelegramTestSuccess": "Telegram Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.validationBotAPIRequired": "Du musst ein Bot-Autorisierungstoken angeben",
|
||||
"components.Settings.Notifications.validationChatIdRequired": "Du musst eine gültige Chat-ID angeben",
|
||||
"components.Settings.Notifications.validationEmail": "Du musst eine gültige E-Mail-Adresse angeben",
|
||||
@@ -1071,7 +1071,7 @@
|
||||
"components.Discover.tmdbtvgenre": "TMDB Serien Genre",
|
||||
"components.Discover.tmdbtvkeyword": "TMDB Serien Keyword",
|
||||
"components.Discover.tvgenres": "Serien Genre",
|
||||
"components.Settings.SettingsMain.apikey": "API Schlüssel",
|
||||
"components.Settings.SettingsMain.apikey": "API-Schlüssel",
|
||||
"components.Settings.SettingsMain.csrfProtection": "Aktivere CSRF Schutz",
|
||||
"components.Settings.SettingsMain.applicationTitle": "Anwendungstitel",
|
||||
"components.Settings.SettingsMain.csrfProtectionTip": "Limitiere externen API Zugriff auf Lese-Operationen (erfordert HTTPS)",
|
||||
@@ -1272,7 +1272,7 @@
|
||||
"components.Settings.jellyfinSettingsFailure": "Beim Speichern der Einstellungen von {mediaServerName} ist ein Fehler aufgetreten.",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Normalerweise wird dieser Vorgang nur einmal alle 24 Stunden durchgeführt. Jellyseerr wird die kürzlich hinzugefügten Bibliotheken deines {mediaServerName} Servers aggressiver überprüfen. Wenn dies das erste Mal ist, dass du Jellyseerr konfigurierst, wird ein einmaliger vollständiger manueller Bibliotheks-Scan empfohlen!",
|
||||
"components.Settings.save": "Änderungen speichern",
|
||||
"components.Settings.Notifications.userEmailRequired": "Benutzer-E-Mai",
|
||||
"components.Settings.Notifications.userEmailRequired": "Benutzer-E-Mail erforderlich",
|
||||
"components.Settings.Notifications.NotificationsPushover.sound": "Benachrichtigungston",
|
||||
"components.Settings.SonarrModal.seriesType": "TV-Serie Typ",
|
||||
"components.Settings.jellyfinlibrariesDescription": "Die Bibliotheken {mediaServerName} werden nach Titeln durchsucht. Klicke auf die Schaltfläche unten, wenn keine Bibliotheken aufgelistet sind.",
|
||||
@@ -1346,5 +1346,62 @@
|
||||
"components.Login.enablessl": "Benutze SSL",
|
||||
"components.Settings.jellyfinForgotPasswordUrl": "Passwort vergessen URL",
|
||||
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Eine benutzerdefinierte Authentifizierung mit automatischer Bibliotheksbündelung wird nicht unterstützt",
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "Es wurden keine Bibliotheken gefunden"
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "Es wurden keine Bibliotheken gefunden",
|
||||
"components.Settings.scanbackground": "Der Scanvorgang wird im Hintergrund ausgeführt. Sie können in der Zwischenzeit den Einrichtungsprozess fortsetzen.",
|
||||
"components.Blacklist.blacklistdate": "Datum",
|
||||
"components.PermissionEdit.viewblacklistedItems": "Medien auf der Sperrliste anzeigen.",
|
||||
"components.Settings.SettingsMain.discoverRegion": "Region entdecken",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> ist nicht auf der Sperrliste.",
|
||||
"components.PermissionEdit.manageblacklist": "Sperrliste verwalten",
|
||||
"components.Settings.SettingsJobsCache.plex-refresh-token": "Jellyfin Refresh Token",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Region entdecken",
|
||||
"i18n.blacklistDuplicateError": "<strong>{title}</strong> wurde bereits auf die Sperrliste gesetzt.",
|
||||
"components.Settings.Notifications.validationWebhookRoleId": "Sie müssen eine gültige Discord Rollen-ID angeben",
|
||||
"components.Settings.Notifications.webhookRoleIdTip": "Die Rollen ID, die in der Webhook Nachricht erwähnt werden soll. Leer lassen, um Erwähnungen zu deaktivieren",
|
||||
"i18n.addToBlacklist": "Zur Sperrliste hinzufügen",
|
||||
"components.PermissionEdit.blacklistedItemsDescription": "Autorisierung zum Sperren von Medien.",
|
||||
"components.Settings.SettingsMain.proxyBypassFilterTip": "Verwenden Sie ',' als Trennzeichen und '*.' als Platzhalter für Subdomains",
|
||||
"components.Settings.SettingsMain.streamingRegion": "Streaming Region",
|
||||
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> wurde erfolgreich von der Sperrliste entfernt.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Streaming Region",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Streaming Seiten nach regionaler Verfügbarkeit anzeigen",
|
||||
"components.Blacklist.blacklistedby": "{date} durch {user}",
|
||||
"components.Blacklist.mediaName": "Name",
|
||||
"components.Blacklist.mediaTmdbId": "tmdb Id",
|
||||
"components.Blacklist.mediaType": "Typ",
|
||||
"component.BlacklistBlock.blacklistdate": "Sperrdatum",
|
||||
"component.BlacklistBlock.blacklistedby": "Gesperrt durch",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Medien auf der Sperrliste verwalten.",
|
||||
"components.Blacklist.blacklistsettings": "Sperrlisteneinstellungen",
|
||||
"component.BlacklistModal.blacklisting": "Sperrliste",
|
||||
"components.Layout.Sidebar.blacklist": "Sperrliste",
|
||||
"components.PermissionEdit.manageblacklistDescription": "Erlaubnis zur Verwaltung von Medien auf der Sperrliste erteilen.",
|
||||
"components.PermissionEdit.blacklistedItems": "Medien sperren.",
|
||||
"components.PermissionEdit.viewblacklistedItemsDescription": "Erlaubnis zum Anzeigen von Medien auf der Sperrliste erteilen.",
|
||||
"components.RequestList.RequestItem.removearr": "Von {arr} entfernen",
|
||||
"components.RequestList.sortDirection": "Sortierrichtung umschalten",
|
||||
"components.Settings.Notifications.webhookRoleId": "Benachrichtigung Rollen ID",
|
||||
"components.Settings.SettingsJobsCache.usersavatars": "Avatare der Nutzer",
|
||||
"components.Settings.SettingsMain.discoverRegionTip": "Inhalte nach regionaler Verfügbarkeit filtern",
|
||||
"components.Settings.SettingsMain.proxyBypassLocalAddresses": "Proxy für lokale Adressen umgehen",
|
||||
"components.Settings.SettingsMain.proxyEnabled": "HTTP(S) Proxy",
|
||||
"components.Settings.SettingsMain.proxyHostname": "Proxy Hostname",
|
||||
"components.Settings.SettingsMain.proxyPassword": "Proxy Passwort",
|
||||
"components.Settings.SettingsMain.proxyPort": "Proxy Port",
|
||||
"components.Settings.SettingsMain.proxySsl": "SSL für Proxy verwenden",
|
||||
"components.Settings.SettingsMain.proxyUser": "Proxy Benutzername",
|
||||
"components.Settings.SettingsMain.proxyBypassFilter": "vom Proxy ignorierte Adressen",
|
||||
"components.Settings.SettingsMain.streamingRegionTip": "Streaming Seiten nach regionaler Verfügbarkeit anzeigen",
|
||||
"components.Settings.SettingsMain.validationProxyPort": "Sie müssen einen gültigen Port angeben",
|
||||
"components.Settings.apiKey": "API-Schlüssel",
|
||||
"components.Settings.tip": "Tipp",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Inhalte nach regionaler Verfügbarkeit filtern",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "Diese E-Mail ist bereits vergeben!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Ein anderer Benutzer hat bereits diesen Benutzernamen. Sie müssen eine E-Mail festlegen",
|
||||
"i18n.blacklist": "Sperrliste",
|
||||
"i18n.blacklistError": "Etwas ist schief gelaufen, versuchen Sie es noch einmal.",
|
||||
"i18n.blacklistSuccess": "<strong>{title}</strong> wurde erfolgreich auf die Sperrliste gesetzt.",
|
||||
"i18n.blacklisted": "Gesperrt",
|
||||
"i18n.removefromBlacklist": "Von der Sperrliste entfernen",
|
||||
"i18n.specials": "Besonderheiten"
|
||||
}
|
||||
|
||||
@@ -10,12 +10,9 @@
|
||||
"components.Blacklist.blacklistdate": "date",
|
||||
"components.Blacklist.blacklistedby": "{date} by {user}",
|
||||
"components.Blacklist.blacklistsettings": "Blacklist Settings",
|
||||
"components.Blacklist.filterBlacklistedTags": "Blacklisted Tags",
|
||||
"components.Blacklist.filterManual": "Manual",
|
||||
"components.Blacklist.mediaName": "Name",
|
||||
"components.Blacklist.mediaTmdbId": "tmdb Id",
|
||||
"components.Blacklist.mediaType": "Type",
|
||||
"components.Blacklist.showAllBlacklisted": "Show All Blacklisted Media",
|
||||
"components.CollectionDetails.numberofmovies": "{count} Movies",
|
||||
"components.CollectionDetails.overview": "Overview",
|
||||
"components.CollectionDetails.requestcollection": "Request Collection",
|
||||
@@ -103,6 +100,7 @@
|
||||
"components.Discover.StudioSlider.studios": "Studios",
|
||||
"components.Discover.TvGenreList.seriesgenres": "Series Genres",
|
||||
"components.Discover.TvGenreSlider.tvgenres": "Series Genres",
|
||||
"components.DiscoverTvUpcoming.upcomingtv": "Upcoming Series",
|
||||
"components.Discover.createnewslider": "Create New Slider",
|
||||
"components.Discover.customizediscover": "Customize Discover",
|
||||
"components.Discover.discover": "Discover",
|
||||
@@ -136,7 +134,6 @@
|
||||
"components.Discover.upcomingtv": "Upcoming Series",
|
||||
"components.Discover.updatefailed": "Something went wrong updating the discover customization settings.",
|
||||
"components.Discover.updatesuccess": "Updated discover customization settings.",
|
||||
"components.DiscoverTvUpcoming.upcomingtv": "Upcoming Series",
|
||||
"components.DownloadBlock.estimatedtime": "Estimated {time}",
|
||||
"components.DownloadBlock.formattedTitle": "{title}: Season {seasonNumber} Episode {episodeNumber}",
|
||||
"components.IssueDetails.IssueComment.areyousuredelete": "Are you sure you want to delete this comment?",
|
||||
@@ -865,7 +862,6 @@
|
||||
"components.Settings.SettingsJobsCache.editJobSchedule": "Modify Job",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Current Frequency",
|
||||
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "New Frequency",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Every {jobScheduleDays, plural, one {day} other {{jobScheduleDays} days}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
|
||||
@@ -892,7 +888,6 @@
|
||||
"components.Settings.SettingsJobsCache.plex-refresh-token": "Plex Refresh Token",
|
||||
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Watchlist Sync",
|
||||
"components.Settings.SettingsJobsCache.process": "Process",
|
||||
"components.Settings.SettingsJobsCache.process-blacklisted-tags": "Process Blacklisted Tags",
|
||||
"components.Settings.SettingsJobsCache.radarr-scan": "Radarr Scan",
|
||||
"components.Settings.SettingsJobsCache.runnow": "Run Now",
|
||||
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
|
||||
@@ -917,13 +912,8 @@
|
||||
"components.Settings.SettingsLogs.time": "Timestamp",
|
||||
"components.Settings.SettingsLogs.viewdetails": "View Details",
|
||||
"components.Settings.SettingsMain.apikey": "API Key",
|
||||
"components.Settings.SettingsMain.apikeyCopied": "Copied API key to clipboard.",
|
||||
"components.Settings.SettingsMain.applicationTitle": "Application Title",
|
||||
"components.Settings.SettingsMain.applicationurl": "Application URL",
|
||||
"components.Settings.SettingsMain.blacklistedTags": "Blacklist Content with Tags",
|
||||
"components.Settings.SettingsMain.blacklistedTagsLimit": "Limit Content Blacklisted per Tag",
|
||||
"components.Settings.SettingsMain.blacklistedTagsLimitTip": "The \"Process Blacklisted Tags\" job will blacklist this many pages into each sort. Larger numbers will create a more accurate blacklist, but use more space.",
|
||||
"components.Settings.SettingsMain.blacklistedTagsTip": "Automatically add content with tags to the blacklist using the \"Process Blacklisted Tags\" job",
|
||||
"components.Settings.SettingsMain.cacheImages": "Enable Image Caching",
|
||||
"components.Settings.SettingsMain.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)",
|
||||
"components.Settings.SettingsMain.discoverRegion": "Discover Region",
|
||||
@@ -950,8 +940,6 @@
|
||||
"components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||
"components.Settings.SettingsNetwork.dnsServers": "Custom DNS Servers",
|
||||
"components.Settings.SettingsNetwork.dnsServersTip": "Comma-separated list of custom DNS servers, e.g. \"1.1.1.1,[2606:4700:4700::1111]\"",
|
||||
"components.Settings.SettingsNetwork.docs": "documentation",
|
||||
"components.Settings.SettingsNetwork.forceIpv4First": "Force IPv4 Resolution First",
|
||||
"components.Settings.SettingsNetwork.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6",
|
||||
@@ -1054,14 +1042,8 @@
|
||||
"components.Settings.addsonarr": "Add Sonarr Server",
|
||||
"components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality",
|
||||
"components.Settings.apiKey": "API key",
|
||||
"components.Settings.blacklistedTagImportInstructions": "Paste blacklist tag configuration below.",
|
||||
"components.Settings.blacklistedTagImportTitle": "Import Blacklisted Tag Configuration",
|
||||
"components.Settings.blacklistedTagsText": "Blacklisted Tags",
|
||||
"components.Settings.cancelscan": "Cancel Scan",
|
||||
"components.Settings.clearBlacklistedTagsConfirm": "Are you sure you want to clear the blacklisted tags?",
|
||||
"components.Settings.copyBlacklistedTags": "Copied blacklisted tags to clipboard.",
|
||||
"components.Settings.copyBlacklistedTagsEmpty": "Nothing to copy",
|
||||
"components.Settings.copyBlacklistedTagsTip": "Copy blacklisted tag configuration",
|
||||
"components.Settings.copied": "Copied API key to clipboard.",
|
||||
"components.Settings.currentlibrary": "Current Library: {name}",
|
||||
"components.Settings.default": "Default",
|
||||
"components.Settings.default4k": "Default 4K",
|
||||
@@ -1072,8 +1054,6 @@
|
||||
"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.importBlacklistedTagsTip": "Import blacklisted tag configuration",
|
||||
"components.Settings.invalidKeyword": "{keywordId} is not a TMDB keyword.",
|
||||
"components.Settings.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||
"components.Settings.is4k": "4K",
|
||||
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
||||
@@ -1105,12 +1085,9 @@
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuServices": "Services",
|
||||
"components.Settings.menuUsers": "Users",
|
||||
"components.Settings.no": "No",
|
||||
"components.Settings.noDefault4kServer": "A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.",
|
||||
"components.Settings.noDefaultNon4kServer": "If you only have a single {serverType} server for both non-4K and 4K content (or if you only download 4K content), your {serverType} server should <strong>NOT</strong> be designated as a 4K server.",
|
||||
"components.Settings.noDefaultServer": "At least one {serverType} server must be marked as default in order for {mediaType} requests to be processed.",
|
||||
"components.Settings.noSpecialCharacters": "Configuration must be a comma delimited list of TMDB keyword ids, and must not start or end with a comma.",
|
||||
"components.Settings.nooptions": "No results.",
|
||||
"components.Settings.notificationAgentSettingsDescription": "Configure and enable notification agents.",
|
||||
"components.Settings.notifications": "Notifications",
|
||||
"components.Settings.notificationsettings": "Notification Settings",
|
||||
@@ -1130,7 +1107,6 @@
|
||||
"components.Settings.scan": "Sync Libraries",
|
||||
"components.Settings.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
|
||||
"components.Settings.scanning": "Syncing…",
|
||||
"components.Settings.searchKeywords": "Search keywords…",
|
||||
"components.Settings.serverLocal": "local",
|
||||
"components.Settings.serverRemote": "remote",
|
||||
"components.Settings.serverSecure": "secure",
|
||||
@@ -1144,7 +1120,6 @@
|
||||
"components.Settings.sonarrsettings": "Sonarr Settings",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.startscan": "Start Scan",
|
||||
"components.Settings.starttyping": "Starting typing to search.",
|
||||
"components.Settings.syncJellyfin": "Sync Libraries",
|
||||
"components.Settings.syncing": "Syncing",
|
||||
"components.Settings.tautulliApiKey": "API Key",
|
||||
@@ -1168,12 +1143,10 @@
|
||||
"components.Settings.validationUrlBaseLeadingSlash": "URL base must have a leading slash",
|
||||
"components.Settings.validationUrlBaseTrailingSlash": "URL base must not end in a trailing slash",
|
||||
"components.Settings.validationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
"components.Settings.valueRequired": "You must provide a value.",
|
||||
"components.Settings.webAppUrl": "<WebAppLink>Web App</WebAppLink> URL",
|
||||
"components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app",
|
||||
"components.Settings.webhook": "Webhook",
|
||||
"components.Settings.webpush": "Web Push",
|
||||
"components.Settings.yes": "Yes",
|
||||
"components.Setup.back": "Go back",
|
||||
"components.Setup.configemby": "Configure Emby",
|
||||
"components.Setup.configjellyfin": "Configure Jellyfin",
|
||||
@@ -1186,7 +1159,7 @@
|
||||
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
|
||||
"components.Setup.servertype": "Choose Server Type",
|
||||
"components.Setup.setup": "Setup",
|
||||
"components.Setup.signin": "Sign in to your account",
|
||||
"components.Setup.signin": "Sign In",
|
||||
"components.Setup.signinMessage": "Get started by signing in",
|
||||
"components.Setup.signinWithEmby": "Enter your Emby details",
|
||||
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -79,20 +79,20 @@
|
||||
"components.Discover.FilterSlideover.tmdbuservotecount": "Nombre de votes utilisateur TMDB",
|
||||
"components.Discover.FilterSlideover.to": "À",
|
||||
"components.Discover.FilterSlideover.voteCount": "Nombre de votes entre {minValue} et {maxValue}",
|
||||
"components.Discover.MovieGenreList.moviegenres": "Genres de films",
|
||||
"components.Discover.MovieGenreSlider.moviegenres": "Genres de films",
|
||||
"components.Discover.MovieGenreList.moviegenres": "Films par genres",
|
||||
"components.Discover.MovieGenreSlider.moviegenres": "Films par genres",
|
||||
"components.Discover.NetworkSlider.networks": "Diffuseurs",
|
||||
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Les médias ajoutés à votre <PlexWatchlistSupportLink>liste de lecture Plex</PlexWatchlistSupportLink> apparaîtront ici.",
|
||||
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Votre liste de lecture",
|
||||
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Récemment ajoutés",
|
||||
"components.Discover.StudioSlider.studios": "Studios",
|
||||
"components.Discover.TvGenreList.seriesgenres": "Genres de séries",
|
||||
"components.Discover.TvGenreSlider.tvgenres": "Genres de séries",
|
||||
"components.Discover.TvGenreList.seriesgenres": "Séries par genres",
|
||||
"components.Discover.TvGenreSlider.tvgenres": "Séries par genres",
|
||||
"components.Discover.createnewslider": "Créer un nouveau slider",
|
||||
"components.Discover.customizediscover": "Personnaliser Découvrir",
|
||||
"components.Discover.discover": "Découvrir",
|
||||
"components.Discover.emptywatchlist": "Les médias ajoutés à votre <PlexWatchlistSupportLink>liste de lecture Plex</PlexWatchlistSupportLink> apparaîtront ici.",
|
||||
"components.Discover.moviegenres": "Genres de films",
|
||||
"components.Discover.moviegenres": "Films par genres",
|
||||
"components.Discover.networks": "Diffuseurs",
|
||||
"components.Discover.plexwatchlist": "Votre liste de lecture",
|
||||
"components.Discover.popularmovies": "Films populaires",
|
||||
@@ -115,7 +115,7 @@
|
||||
"components.Discover.tmdbtvkeyword": "Mot-clé de la série TMDB",
|
||||
"components.Discover.tmdbtvstreamingservices": "Services de streaming TMDB TV",
|
||||
"components.Discover.trending": "Tendances",
|
||||
"components.Discover.tvgenres": "Genres de séries",
|
||||
"components.Discover.tvgenres": "Séries par genres",
|
||||
"components.Discover.upcoming": "Films à venir",
|
||||
"components.Discover.upcomingmovies": "Films à venir",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Rechercher films et séries",
|
||||
@@ -219,7 +219,7 @@
|
||||
"components.Settings.hostname": "Nom d'hôte ou adresse IP",
|
||||
"components.Settings.librariesRemaining": "Bibliothèques restantes : {count}",
|
||||
"components.Settings.manualscan": "Scan manuel des bibliothèques",
|
||||
"components.Settings.manualscanDescription": "Normalement, le scan sera effectué une fois toutes les 24 heures seulement. Jellyseerr vérifiera les ajouts récents de votre serveur Plex de façon proactive. Si c'est la première fois que vous configurez Plex, un scan complet de la bibliothèque est recommandé!",
|
||||
"components.Settings.manualscanDescription": "Normalement, le scan sera effectué une fois toutes les 24 heures seulement. Jellyseerr vérifiera les ajouts récents de votre serveur Plex de façon proactive. Si c'est la première fois que vous configurez Plex, un scan complet de la bibliothèque est recommandé !",
|
||||
"components.Settings.menuAbout": "À propos",
|
||||
"components.Settings.menuGeneralSettings": "Général",
|
||||
"components.Settings.menuJobs": "Tâches et cache",
|
||||
@@ -425,7 +425,7 @@
|
||||
"components.RequestBlock.profilechanged": "Profil de qualité",
|
||||
"components.NotificationTypeSelector.mediadeclined": "Demande refusée",
|
||||
"components.NotificationTypeSelector.mediadeclinedDescription": "Envoyer des notifications lorsqu'une demande de média est refusée.",
|
||||
"i18n.experimental": "Expérimentale",
|
||||
"i18n.experimental": "Expérimental",
|
||||
"components.RequestModal.requesterror": "Une erreur s'est produite lors de la demande.",
|
||||
"components.RequestModal.SearchByNameModal.notvdbiddescription": "Nous n'avons pas pu associer cette série automatiquement. Veuillez sélectionner l'association correcte dans la liste ci-dessous.",
|
||||
"components.Login.signinwithplex": "Utilisez votre compte Plex",
|
||||
@@ -499,7 +499,7 @@
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "Cache de {cachename} vidé.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr met en cache les demandes aux points de terminaison d'API externes pour optimiser les performances et éviter de faire des appels d'API inutiles.",
|
||||
"components.Settings.SettingsJobsCache.cache": "Cache",
|
||||
"i18n.advanced": "Avancés",
|
||||
"i18n.advanced": "Avancé",
|
||||
"components.UserList.users": "Utilisateurs",
|
||||
"components.Setup.setup": "Configuration",
|
||||
"components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "L'URL ne doit pas se terminer par une barre oblique finale",
|
||||
@@ -628,7 +628,7 @@
|
||||
"components.Settings.scan": "Synchroniser les bibliothèques",
|
||||
"components.Settings.SettingsJobsCache.sonarr-scan": "Scan de Sonarr",
|
||||
"components.Settings.SettingsJobsCache.radarr-scan": "Scan de Radarr",
|
||||
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Scan des ajouts récents Plex.",
|
||||
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Scan des ajouts récents Plex",
|
||||
"components.Settings.SettingsJobsCache.plex-full-scan": "Scan complet des bibliothèques Plex.",
|
||||
"components.Settings.Notifications.validationUrl": "Vous devez fournir une URL valide",
|
||||
"components.Settings.Notifications.botAvatarUrl": "L'URL de l'avatar de votre Bot",
|
||||
@@ -778,7 +778,7 @@
|
||||
"components.Settings.Notifications.botUsernameTip": "Permet aux utilisateurs de démarrer également une conversation avec votre bot et de configurer leurs propres notifications personnelles",
|
||||
"components.RequestModal.pendingapproval": "Votre demande est en attente de validation.",
|
||||
"components.RequestList.RequestItem.mediaerror": "{mediaType} non trouvé",
|
||||
"components.RequestList.RequestItem.deleterequest": "Supprimer la Demande",
|
||||
"components.RequestList.RequestItem.deleterequest": "Supprimer la demande",
|
||||
"components.RequestList.RequestItem.cancelRequest": "Annuler la demande",
|
||||
"components.RequestCard.mediaerror": "{mediaType} non trouvé",
|
||||
"components.RequestCard.deleterequest": "Supprimer la demande",
|
||||
@@ -1141,7 +1141,7 @@
|
||||
"components.Login.validationhostformat": "URL valide requise",
|
||||
"components.Login.validationhostrequired": "{mediaServerName} URL requise",
|
||||
"components.Login.validationusernamerequired": "Nom d'utilisateur requis",
|
||||
"components.MovieDetails.imdbuserscore": "Note Utilisateurs",
|
||||
"components.MovieDetails.imdbuserscore": "Note des utilisateurs IMDB",
|
||||
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Cela supprimera irréversiblement ce(tte) {mediaType} de {arr}, y compris tous les fichiers.",
|
||||
"components.ManageSlideOver.removearr": "Supprimer de {arr}",
|
||||
"components.ManageSlideOver.removearr4k": "Supprimer de {arr} 4K",
|
||||
@@ -1152,8 +1152,8 @@
|
||||
"components.MovieDetails.play": "Lire sur {mediaServerName}",
|
||||
"components.MovieDetails.play4k": "Lire en 4k sur {mediaServerName}",
|
||||
"components.MovieDetails.reportissue": "Signaler un problème",
|
||||
"components.MovieDetails.rtaudiencescore": "Score d’audience de Rotten Tomatoes",
|
||||
"components.MovieDetails.rtcriticsscore": "Rotten Tomatoes Tomatomètre",
|
||||
"components.MovieDetails.rtaudiencescore": "Score d’audience Rotten Tomatoes",
|
||||
"components.MovieDetails.rtcriticsscore": "Tomatomètre Rotten Tomatoes",
|
||||
"components.MovieDetails.tmdbuserscore": "Note des utilisateurs TMDB",
|
||||
"components.PermissionEdit.viewwatchlistsDescription": "Autorise à voir les listes de lecture {mediaServerName} des autres utilisateurs.",
|
||||
"components.RequestBlock.approve": "Approuver la demande",
|
||||
@@ -1234,30 +1234,30 @@
|
||||
"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.jellyfinSettings": "Paramètres {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'analyse. Cliquez sur le bouton ci-dessous si aucune bibliothèque n'est répertoriée.",
|
||||
"components.Settings.jellyfinsettings": "Paramètres pour {mediaServerName}",
|
||||
"components.Settings.jellyfinsettings": "Paramètres {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": "Renseigner vos informations d'identification de Plex",
|
||||
"components.Setup.signinWithPlex": "Entrez vos identifiants 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.addToWatchList": "Ajouter à la liste de surveillance",
|
||||
"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.play": "Lire sur {mediaServerName}",
|
||||
"components.TvDetails.play4k": "Lire en 4K sur {mediaServerName}",
|
||||
"components.TvDetails.rtcriticsscore": "Tomatometer sur Rotten Tomatoes",
|
||||
"components.TvDetails.seasonnumber": "Saison {seasonNumber}",
|
||||
"components.TvDetails.seasonstitle": "Saisons",
|
||||
@@ -1287,8 +1287,8 @@
|
||||
"components.Setup.configuremediaserver": "Configurer le serveur multimédia",
|
||||
"components.TvDetails.rtaudiencescore": "Score de l'audience sur Rotten Tomatoes",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Utilisateur {mediaServerName}",
|
||||
"components.Setup.signinWithJellyfin": "Renseigner vos informations d'identification de Jellyfin",
|
||||
"components.UserList.mediaServerUser": "Utilisateur de {mediaServerName}",
|
||||
"components.Setup.signinWithJellyfin": "Entrez vos identifiants Jellyfin",
|
||||
"components.UserList.mediaServerUser": "Utilisateur {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",
|
||||
@@ -1343,7 +1343,7 @@
|
||||
"components.Setup.configemby": "Configurer Emby",
|
||||
"components.Setup.configjellyfin": "Configurer Jellyfin",
|
||||
"components.Setup.configplex": "Configurer Plex",
|
||||
"components.Setup.signinWithEmby": "Renseigner vos informations d'identification d'Emby",
|
||||
"components.Setup.signinWithEmby": "Entrez vos identifiants Emby",
|
||||
"components.Setup.subtitle": "Commencez par choisir votre serveur multimédia",
|
||||
"components.StatusBadge.seasonnumber": "S{seasonNumber}",
|
||||
"components.Discover.FilterSlideover.status": "Statut",
|
||||
@@ -1355,8 +1355,53 @@
|
||||
"component.BlacklistBlock.blacklistdate": "Date de mise en liste noire",
|
||||
"component.BlacklistBlock.blacklistedby": "Mis en liste noire par",
|
||||
"component.BlacklistModal.blacklisting": "Ajout en liste noire",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> n'est pas en liste noire.",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Gérer le contenu en liste noire",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> n'est pas dans la liste noire.",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Gérer le contenu en liste noire.",
|
||||
"components.Blacklist.blacklistsettings": "Paramètres de la liste noire",
|
||||
"components.Layout.Sidebar.blacklist": "Liste noir"
|
||||
"components.Layout.Sidebar.blacklist": "Liste noire",
|
||||
"components.PermissionEdit.viewblacklistedItems": "Voir les médias dans la liste noire.",
|
||||
"i18n.blacklistDuplicateError": "<strong>{title}</strong> est déjà dans la liste noire.",
|
||||
"i18n.blacklistError": "Une erreur s'est produite, réessayez.",
|
||||
"components.PermissionEdit.blacklistedItems": "Ajouter le média à la liste noire.",
|
||||
"components.Settings.Notifications.validationWebhookRoleId": "Vous devez fournir un identifiant Discord valide",
|
||||
"components.Settings.SettingsMain.discoverRegion": "Pays à découvrir",
|
||||
"components.PermissionEdit.manageblacklistDescription": "Accorder la permission de gérer la liste noire.",
|
||||
"components.Settings.SettingsMain.proxyPassword": "Mot de passe du proxy",
|
||||
"components.RequestList.RequestItem.removearr": "Supprimer de {arr}",
|
||||
"components.Settings.SettingsMain.streamingRegionTip": "Afficher les sites de streaming par disponibilité dans les pays",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Afficher les sites de streaming par disponibilité régionale",
|
||||
"components.PermissionEdit.blacklistedItemsDescription": "Accorder la permission de mettre les médias sur liste noire.",
|
||||
"components.PermissionEdit.manageblacklist": "Gérer la liste noire",
|
||||
"components.PermissionEdit.viewblacklistedItemsDescription": "Accorder la permission de voir la liste noire.",
|
||||
"components.RequestList.sortDirection": "Inverser la direction du tri",
|
||||
"components.Settings.SettingsJobsCache.usersavatars": "Avatars des utilisateurs",
|
||||
"components.Settings.SettingsMain.discoverRegionTip": "Filtrer le contenu par disponibilité dans les pays",
|
||||
"components.Settings.SettingsMain.proxyBypassFilterTip": "Utilisez ',' comme séparateur et '*.' comme caractère générique pour les sous-domaines",
|
||||
"components.Settings.SettingsMain.proxyEnabled": "Proxy HTTP(S)",
|
||||
"components.Settings.SettingsMain.proxyHostname": "Nom d'hôte du proxy",
|
||||
"components.Settings.SettingsMain.proxyPort": "Port du proxy",
|
||||
"components.Settings.SettingsMain.proxySsl": "Utiliser SSL pour le proxy",
|
||||
"components.Settings.SettingsMain.proxyUser": "Nom d'utilisateur du proxy",
|
||||
"components.Settings.SettingsMain.streamingRegion": "Pays de diffusion",
|
||||
"components.Settings.SettingsMain.proxyBypassLocalAddresses": "Contourner le proxy pour les adresses locales",
|
||||
"components.Settings.SettingsMain.proxyBypassFilter": "Adresses proxy ignorées",
|
||||
"components.Settings.SettingsMain.validationProxyPort": "Vous devez fournir un port valide",
|
||||
"components.Settings.apiKey": "Clé API",
|
||||
"components.Settings.scanbackground": "L'analyse s'exécutera en arrière-plan. Vous pouvez poursuivre la configuration en attendant.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Pays à découvrir",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filtrer le contenu par disponibilité régionale",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Pays de diffusion",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "Cet email est déjà pris !",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Quelqu'un d'autre possède déjà ce nom d'utilisateur. Vous devez utiliser une adresse e-mail",
|
||||
"i18n.blacklistSuccess": "<strong>{title}</strong> a été ajouté dans la liste noire avec succès.",
|
||||
"i18n.blacklisted": "Sur liste noire",
|
||||
"i18n.addToBlacklist": "Ajouter à la liste noire",
|
||||
"i18n.blacklist": "Liste noire",
|
||||
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> a été retiré de la liste noire avec succès.",
|
||||
"i18n.removefromBlacklist": "Retirer de la liste noire",
|
||||
"i18n.specials": "Hors-série",
|
||||
"components.Settings.Notifications.webhookRoleIdTip": "L'ID à mentionner dans le message du webhook. Laissez ce champ vide pour désactiver les mentions",
|
||||
"components.Settings.Notifications.webhookRoleId": "ID de rôle de notification",
|
||||
"components.Settings.SettingsJobsCache.plex-refresh-token": "Rafraîchir le token Plex",
|
||||
"components.Settings.tip": "Conseil"
|
||||
}
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
"components.Layout.Sidebar.requests": "Verzoeken",
|
||||
"components.Layout.Sidebar.settings": "Instellingen",
|
||||
"components.Layout.Sidebar.users": "Gebruikers",
|
||||
"components.Layout.UserDropdown.signout": "Uitloggen",
|
||||
"components.Layout.UserDropdown.signout": "Afmelden",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.cast": "Cast",
|
||||
"components.MovieDetails.originallanguage": "Oorspronkelijke taal",
|
||||
"components.MovieDetails.overview": "Overzicht",
|
||||
"components.MovieDetails.overviewunavailable": "Overzicht niet beschikbaar.",
|
||||
"components.MovieDetails.recommendations": "Aanbevelingen",
|
||||
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Releasedatum} other {Releasedata}}",
|
||||
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Verschijningsdatum} other {Verschijningsdata}}",
|
||||
"components.MovieDetails.revenue": "Omzet",
|
||||
"components.MovieDetails.runtime": "{minutes} minuten",
|
||||
"components.MovieDetails.similar": "Vergelijkbare titels",
|
||||
"components.PersonDetails.appearsin": "Verschijningen",
|
||||
"components.PersonDetails.ascharacter": "als {character}",
|
||||
"components.RequestBlock.seasons": "{seasonCount, plural, one {seizoen} other {seizoenen}}",
|
||||
"components.RequestCard.seasons": "{seasonCount, plural, one {seizoen} other {seizoenen}}",
|
||||
"components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {seizoen} other {seizoenen}}",
|
||||
"components.RequestBlock.seasons": "{seasonCount, plural, one {Seizoen} other {Seizoenen}}",
|
||||
"components.RequestCard.seasons": "{seasonCount, plural, one {Seizoen} other {Seizoenen}}",
|
||||
"components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Seizoen} other {Seizoenen}}",
|
||||
"components.RequestList.requests": "Verzoeken",
|
||||
"components.RequestModal.cancel": "Verzoek annuleren",
|
||||
"components.RequestModal.numberofepisodes": "Aantal afleveringen",
|
||||
@@ -34,11 +34,11 @@
|
||||
"components.RequestModal.requestCancel": "Verzoek voor <strong>{title}</strong> is geannuleerd.",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> is succesvol aangevraagd!",
|
||||
"components.RequestModal.requestadmin": "Dit verzoek zal automatisch goedgekeurd worden.",
|
||||
"components.RequestModal.requestfrom": "Het verzoek van {user} is in behandeling.",
|
||||
"components.RequestModal.requestfrom": "Het verzoek van {user} is in afwachting van goedkeuring.",
|
||||
"components.RequestModal.requestseasons": "{seasonCount} {seasonCount, plural, one {seizoen} other {seizoenen}} aanvragen",
|
||||
"components.RequestModal.season": "Seizoen",
|
||||
"components.RequestModal.seasonnumber": "Seizoen {number}",
|
||||
"components.RequestModal.selectseason": "Seizoen(en) selecteren",
|
||||
"components.RequestModal.selectseason": "Seizoenen selecteren",
|
||||
"components.Search.searchresults": "Zoekresultaten",
|
||||
"components.Settings.Notifications.agentenabled": "Agent inschakelen",
|
||||
"components.Settings.Notifications.authPass": "SMTP-wachtwoord",
|
||||
@@ -240,7 +240,7 @@
|
||||
"components.MovieDetails.MovieCrew.fullcrew": "Volledige crew",
|
||||
"components.CollectionDetails.requestcollection": "Collectie aanvragen",
|
||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex-{userCount, plural, one {gebruiker} other {gebruikers}} succesvol geïmporteerd!",
|
||||
"components.Settings.SettingsAbout.Releases.versionChangelog": "{version} changelog",
|
||||
"components.Settings.SettingsAbout.Releases.versionChangelog": "Changelog voor {version}",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Versies",
|
||||
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Instellingen voor Slack-meldingen succesvol opgeslagen!",
|
||||
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Instellingen voor Slack-meldingen konden niet opgeslagen worden.",
|
||||
@@ -261,7 +261,7 @@
|
||||
"components.RequestButton.requestmore4k": "Meer in 4K aanvragen",
|
||||
"components.RequestButton.approverequests": "{requestCount, plural, one {Verzoek} other {{requestCount} Verzoeken}} goedkeuren",
|
||||
"components.RequestButton.approve4krequests": "{requestCount, plural, one {4K-verzoek} other {{requestCount} 4K-verzoeken}} goedkeuren",
|
||||
"components.RequestButton.declinerequests": "{requestCount, plural, one {verzoek} other {{requestCount} verzoeken}} weigeren",
|
||||
"components.RequestButton.declinerequests": "{requestCount, plural, one {Verzoek} other {{requestCount} Verzoeken}} weigeren",
|
||||
"components.RequestButton.decline4krequests": "{requestCount, plural, one {4K-verzoek} other {{requestCount} 4K-verzoeken}} weigeren",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "Instellingen voor webhook-meldingen succesvol opgeslagen!",
|
||||
@@ -297,7 +297,7 @@
|
||||
"components.Login.validationemailrequired": "Je moet een geldig e-mailadres opgeven",
|
||||
"components.Login.signinwithoverseerr": "{applicationTitle}-account gebruiken",
|
||||
"components.Login.password": "Wachtwoord",
|
||||
"components.Login.loginerror": "Er ging iets mis bij het inloggen.",
|
||||
"components.Login.loginerror": "Er ging iets mis bij het aanmelden.",
|
||||
"components.Login.email": "E-mailadres",
|
||||
"components.MediaSlider.ShowMoreCard.seemore": "Meer",
|
||||
"i18n.edit": "Bewerken",
|
||||
@@ -323,9 +323,9 @@
|
||||
"components.Login.signinwithplex": "Plex-account gebruiken",
|
||||
"components.Login.signinheader": "Log in om verder te gaan",
|
||||
"components.Login.signingin": "Aanmelden…",
|
||||
"components.Login.signin": "Inloggen",
|
||||
"components.Login.signin": "Aanmelden",
|
||||
"components.Settings.notificationAgentSettingsDescription": "Meldingsagenten configureren en inschakelen.",
|
||||
"components.PlexLoginButton.signinwithplex": "Inloggen",
|
||||
"components.PlexLoginButton.signinwithplex": "Aanmelden",
|
||||
"components.PlexLoginButton.signingin": "Aanmelden…",
|
||||
"components.PermissionEdit.advancedrequest": "Geavanceerde aanvragen",
|
||||
"components.PermissionEdit.admin": "Beheerder",
|
||||
@@ -407,7 +407,7 @@
|
||||
"components.Settings.RadarrModal.validationApplicationUrl": "Je moet een geldige URL opgeven",
|
||||
"components.PermissionEdit.viewrequestsDescription": "Toestemming geven om mediaverzoeken van andere gebruikers te bekijken.",
|
||||
"components.PermissionEdit.viewrequests": "Verzoeken bekijken",
|
||||
"components.UserList.validationEmail": "E-mailadres verplicht",
|
||||
"components.UserList.validationEmail": "E-mailadres vereist",
|
||||
"components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "Basis-URL mag niet eindigen op een schuine streep",
|
||||
"components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "Basis-URL moet met een schuine streep beginnen",
|
||||
"components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "URL-basis mag niet eindigen op een schuine streep",
|
||||
@@ -425,7 +425,7 @@
|
||||
"components.ResetPassword.resetpasswordsuccessmessage": "Wachtwoord is succesvol opnieuw ingesteld!",
|
||||
"components.ResetPassword.requestresetlinksuccessmessage": "Er wordt een link om het wachtwoord te resetten naar het opgegeven e-mailadres gestuurd als dat gekoppeld is aan een geldige gebruiker.",
|
||||
"components.ResetPassword.password": "Wachtwoord",
|
||||
"components.ResetPassword.gobacklogin": "Terug naar inlogpagina",
|
||||
"components.ResetPassword.gobacklogin": "Terug naar aanmeldpagina",
|
||||
"components.ResetPassword.emailresetlink": "Herstellink e-mailen",
|
||||
"components.ResetPassword.confirmpassword": "Wachtwoord bevestigen",
|
||||
"components.Login.forgotpassword": "Wachtwoord vergeten?",
|
||||
@@ -569,7 +569,7 @@
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Hervatten",
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Pauze",
|
||||
"components.Settings.SettingsLogs.message": "Bericht",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Je kunt deze logs ook rechtstreeks bekijken via <code>stdout</code>, of in <code>{appDataPath}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Je kunt deze logboeken ook rechtstreeks bekijken via <code>stdout</code>, of in <code>{appDataPath}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logs": "Logboeken",
|
||||
"components.Settings.SettingsLogs.level": "Ernst",
|
||||
"components.Settings.SettingsLogs.label": "Label",
|
||||
@@ -616,7 +616,7 @@
|
||||
"components.Settings.SettingsUsers.tvRequestLimitLabel": "Globale aanvraaglimiet voor series",
|
||||
"components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {seizoen} other {seizoenen}}",
|
||||
"components.RequestModal.QuotaDisplay.season": "seizoen",
|
||||
"components.RequestModal.QuotaDisplay.requiredquota": "Je moet nog minstens <strong>{seasons}</strong> {seasons, plural, one {seizoensverzoek} other {seizoensverzoek}} over hebben om deze serie aan te vragen.",
|
||||
"components.RequestModal.QuotaDisplay.requiredquota": "Je moet nog minstens <strong>{seasons}</strong> {seasons, plural, one {seizoensverzoek} other {seizoensverzoeken}} over hebben om deze serie aan te vragen.",
|
||||
"components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {Geen} other {<strong>#</strong>}} {type}{remaining, plural, one {verzoek} other {verzoeken}} resterend",
|
||||
"components.RequestModal.QuotaDisplay.quotaLinkUser": "Je kan een overzicht van de aanvraaglimieten van deze gebruiker bekijken op hun <ProfileLink>profielpagina</ProfileLink>.",
|
||||
"components.RequestModal.QuotaDisplay.quotaLink": "Je kan een overzicht van je aanvraaglimieten bekijken op jouw <ProfileLink>profielpagina</ProfileLink>.",
|
||||
@@ -691,7 +691,7 @@
|
||||
"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. Stel hieronder een wachtwoord in om aanmelden als \"lokale gebruiker\" (met je e-mailadres) in te schakelen.",
|
||||
"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.noDefaultServer": "Ten minste één {serverType} server moet als standaard worden gemarkeerd om {mediaType}verzoeken te kunnen verwerken.",
|
||||
@@ -784,7 +784,7 @@
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Om web-pushmeldingen te ontvangen, moet Jellyseerr via HTTPS worden weergegeven.",
|
||||
"components.RequestList.RequestItem.requesteddate": "Aangevraagd",
|
||||
"components.RequestCard.failedretry": "Er ging opnieuw iets mis tijdens het aanvragen.",
|
||||
"components.Settings.SettingsUsers.localLoginTip": "Sta gebruikers toe om in te loggen met hun e-mailadres en wachtwoord, in plaats van met Plex OAuth",
|
||||
"components.Settings.SettingsUsers.localLoginTip": "Sta gebruikers toe om zich aan te melden met hun e-mailadres en wachtwoord, in plaats van met {mediaServerName} OAuth",
|
||||
"components.Settings.SettingsUsers.defaultPermissionsTip": "Initiële machtigingen toegekend aan nieuwe gebruikers",
|
||||
"components.QuotaSelector.tvRequests": "{quotaLimit} <quotaUnits>{seasons} per {quotaDays} {days}</quotaUnits>",
|
||||
"components.QuotaSelector.seasons": "{count, plural, one {seizoen} other {seizoenen}}",
|
||||
@@ -814,7 +814,7 @@
|
||||
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Nieuwe frequentie",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Er ging iets mis bij het opslaan van de taak.",
|
||||
"components.Settings.SettingsJobsCache.editJobSchedule": "Taak wijzigen",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Elk(e) {jobScheduleHours, plural, one {uur} other {{jobScheduleHours} uren}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Elk(e) {jobScheduleHours, plural, one {uur} other {{jobScheduleHours} uur}}",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "Je voert de <code>develop</code>versie van Jellyseerr uit, die alleen wordt aanbevolen als je bijdraagt aan de ontwikkeling of de allereerste versies helpt testen.",
|
||||
"components.StatusBadge.status": "{status}",
|
||||
"components.IssueDetails.IssueComment.areyousuredelete": "Weet je zeker dat je deze opmerking wilt verwijderen?",
|
||||
@@ -857,7 +857,7 @@
|
||||
"components.IssueDetails.toaststatusupdated": "Probleemstatus succesvol bijgewerkt!",
|
||||
"components.IssueDetails.closeissueandcomment": "Afsluiten met opmerking",
|
||||
"components.IssueModal.CreateIssueModal.problemseason": "Getroffen seizoen",
|
||||
"components.IssueDetails.openedby": "#{issueId} {relativeTime} ingediend door {username}",
|
||||
"components.IssueDetails.openedby": "#{issueId} - {relativeTime} ingediend door {username}",
|
||||
"components.IssueDetails.IssueDescription.description": "Beschrijving",
|
||||
"components.NotificationTypeSelector.issuecommentDescription": "Melding sturen wanneer problemen nieuwe opmerkingen krijgen.",
|
||||
"components.IssueModal.CreateIssueModal.toastviewissue": "Probleem bekijken",
|
||||
@@ -868,13 +868,13 @@
|
||||
"components.IssueDetails.episode": "Aflevering {episodeNumber}",
|
||||
"components.IssueDetails.issuepagetitle": "Probleem",
|
||||
"components.IssueDetails.issuetype": "Type",
|
||||
"components.IssueDetails.leavecomment": "Opmerking geven",
|
||||
"components.IssueDetails.leavecomment": "Opmerking plaatsen",
|
||||
"components.IssueDetails.deleteissueconfirm": "Weet je zeker dat je dit probleem wilt verwijderen?",
|
||||
"components.IssueDetails.unknownissuetype": "Onbekend",
|
||||
"components.IssueDetails.openinarr": "Openen in {arr}",
|
||||
"components.IssueDetails.toasteditdescriptionfailed": "Er ging iets mis bij het bewerken van de beschrijving van het probleem.",
|
||||
"components.IssueList.IssueItem.issuetype": "Type",
|
||||
"components.IssueList.IssueItem.opened": "Onopgelost",
|
||||
"components.IssueList.IssueItem.opened": "Ingediend",
|
||||
"components.IssueDetails.reopenissue": "Probleem opnieuw indienen",
|
||||
"components.IssueDetails.reopenissueandcomment": "Opnieuw indienen met opmerking",
|
||||
"components.IssueDetails.season": "Seizoen {seasonNumber}",
|
||||
@@ -892,7 +892,7 @@
|
||||
"components.IssueModal.CreateIssueModal.problemepisode": "Getroffen aflevering",
|
||||
"components.IssueModal.CreateIssueModal.toastSuccessCreate": "Probleemmelding voor <strong>{title}</strong> succesvol ingediend!",
|
||||
"components.PermissionEdit.viewissues": "Problemen weergeven",
|
||||
"components.IssueModal.issueOther": "Andere",
|
||||
"components.IssueModal.issueOther": "Anders",
|
||||
"components.Layout.Sidebar.issues": "Problemen",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Gegevens wissen",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Hiermee worden alle gegevens voor deze {mediaType} onomkeerbaar verwijderd, inclusief eventuele verzoeken. Als dit item in je {mediaServerName}-bibliotheek staat, worden de mediagegevens opnieuw aangemaakt tijdens de volgende scan.",
|
||||
@@ -900,9 +900,9 @@
|
||||
"components.ManageSlideOver.manageModalTitle": "{mediaType} beheren",
|
||||
"components.ManageSlideOver.tvshow": "serie",
|
||||
"components.NotificationTypeSelector.userissueresolvedDescription": "Ontvang een melding wanneer problemen die jij hebt gemeld, opgelost zijn.",
|
||||
"components.NotificationTypeSelector.issuecomment": "Opmerking op probleem",
|
||||
"components.NotificationTypeSelector.issuecomment": "Reactie op probleem",
|
||||
"components.NotificationTypeSelector.issueresolvedDescription": "Stuur meldingen wanneer problemen opgelost zijn.",
|
||||
"components.ManageSlideOver.openarr4k": "Openen in 4K {arr}",
|
||||
"components.ManageSlideOver.openarr4k": "Openen in 4K-{arr}",
|
||||
"components.NotificationTypeSelector.adminissuecommentDescription": "Ontvang een melding wanneer andere gebruikers reageren op problemen.",
|
||||
"components.NotificationTypeSelector.userissuecommentDescription": "Ontvang een melding wanneer er nieuwe reacties komen op problemen die jij hebt gemeld.",
|
||||
"components.NotificationTypeSelector.userissuecreatedDescription": "Ontvang een melding wanneer andere gebruikers problemen melden.",
|
||||
@@ -929,8 +929,8 @@
|
||||
"components.IssueDetails.playonplex": "Afspelen op {mediaServerName}",
|
||||
"components.IssueDetails.play4konplex": "Afspelen op {mediaServerName} in 4K",
|
||||
"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}}",
|
||||
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {Aflevering} other {Afleveringen}}",
|
||||
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {Seizoen} other {Seizoenen}}",
|
||||
"components.ManageSlideOver.manageModalIssues": "Onopgeloste problemen",
|
||||
"components.IssueModal.CreateIssueModal.extras": "Extra's",
|
||||
"components.NotificationTypeSelector.adminissueresolvedDescription": "Ontvang een melding wanneer problemen worden opgelost door andere gebruikers.",
|
||||
@@ -940,15 +940,15 @@
|
||||
"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.requestmovies": "{count} {count, plural, one {film} other {films}} aanvragen",
|
||||
"components.RequestModal.selectmovies": "Film(s) selecteren",
|
||||
"components.RequestModal.selectmovies": "Films selecteren",
|
||||
"components.MovieDetails.productioncountries": "Productie{countryCount, plural, one {land} other {landen}}",
|
||||
"components.RequestModal.requestmovies4k": "{count} {count, plural, one {film} other {films}} in 4K aanvragen",
|
||||
"components.TvDetails.productioncountries": "Productie{countryCount, plural, one {land} other {landen}}",
|
||||
"components.IssueDetails.commentplaceholder": "Voeg een opmerking toe…",
|
||||
"components.IssueDetails.commentplaceholder": "Opmerking toevoegen…",
|
||||
"components.RequestModal.requestApproved": "Verzoek voor <strong>{title}</strong> goedgekeurd!",
|
||||
"components.RequestModal.approve": "Verzoek goedkeuren",
|
||||
"components.Settings.RadarrModal.inCinemas": "In de bioscoop",
|
||||
"components.Settings.RadarrModal.released": "Uitgekomen",
|
||||
"components.Settings.RadarrModal.released": "Uitgebracht",
|
||||
"components.Settings.RadarrModal.announced": "Aangekondigd",
|
||||
"components.Settings.Notifications.enableMentions": "Vermeldingen inschakelen",
|
||||
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Agent inschakelen",
|
||||
@@ -969,7 +969,7 @@
|
||||
"components.ManageSlideOver.manageModalAdvanced": "Geavanceerd",
|
||||
"components.ManageSlideOver.alltime": "Altijd",
|
||||
"components.ManageSlideOver.markallseasons4kavailable": "Alle seizoenen markeren als beschikbaar in 4K",
|
||||
"components.ManageSlideOver.opentautulli": "In Tautulli openen",
|
||||
"components.ManageSlideOver.opentautulli": "Openen in Tautulli",
|
||||
"components.ManageSlideOver.pastdays": "Afgelopen {days, number} dagen",
|
||||
"components.ManageSlideOver.playedby": "Afgespeeld door",
|
||||
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {keer afgespeeld} other {keer afgespeeld}}",
|
||||
@@ -999,8 +999,8 @@
|
||||
"components.StatusBadge.managemedia": "{mediaType} beheren",
|
||||
"components.StatusBadge.openinarr": "Openen in {arr}",
|
||||
"components.StatusBadge.playonplex": "Afspelen op {mediaServerName}",
|
||||
"components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.MovieDetails.digitalrelease": "Digitale release",
|
||||
"components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex-kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.MovieDetails.digitalrelease": "Digitale uitgave",
|
||||
"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",
|
||||
@@ -1030,27 +1030,27 @@
|
||||
"components.TvDetails.seasonstitle": "Seizoenen",
|
||||
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Jouw kijklijst",
|
||||
"components.Discover.plexwatchlist": "Jouw kijklijst",
|
||||
"components.MovieDetails.physicalrelease": "Fysieke release",
|
||||
"components.MovieDetails.physicalrelease": "Fysieke uitgave",
|
||||
"components.PermissionEdit.autorequest": "Automatisch aanvragen",
|
||||
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Kijklijst synchroniseren",
|
||||
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex-kijklijst synchroniseren",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Series automatisch aanvragen",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Automatisch series op je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> aanvragen",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Automatisch films op je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> aanvragen",
|
||||
"components.PermissionEdit.autorequestDescription": "Toestemming geven om niet-4K media in je Plex Kijklijst automatisch aan te vragen.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Automatisch series op je <PlexWatchlistSupportLink>Plex-kijklijst</PlexWatchlistSupportLink> aanvragen",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Automatisch films op je <PlexWatchlistSupportLink>Plex-kijklijst</PlexWatchlistSupportLink> aanvragen",
|
||||
"components.PermissionEdit.autorequestDescription": "Toestemming geven om niet-4K media in je Plex-kijklijst automatisch aan te vragen.",
|
||||
"components.RequestCard.tvdbid": "TheTVDB ID",
|
||||
"components.Discover.DiscoverWatchlist.watchlist": "Plex-kijklijst",
|
||||
"components.MovieDetails.theatricalrelease": "Bioscooprelease",
|
||||
"components.MovieDetails.theatricalrelease": "Bioscoopuitgave",
|
||||
"components.NotificationTypeSelector.mediaautorequested": "Aanvraag automatisch ingediend",
|
||||
"components.NotificationTypeSelector.mediaautorequestedDescription": "Ontvang een melding wanneer er automatisch nieuwe mediaverzoeken worden ingediend voor items op je Plex Kijklijst.",
|
||||
"components.PermissionEdit.autorequestSeriesDescription": "Toestemming geven om niet-4K series in je Plex Kijklijst automatisch aan te vragen.",
|
||||
"components.PermissionEdit.viewwatchlists": "Plex Kijklijsten bekijken",
|
||||
"components.PermissionEdit.viewwatchlistsDescription": "Toestemming verlenen om de Plex Kijklijsten van andere gebruikers te bekijken.",
|
||||
"components.NotificationTypeSelector.mediaautorequestedDescription": "Ontvang een melding wanneer er automatisch nieuwe mediaverzoeken worden ingediend voor items op je Plex-kijklijst.",
|
||||
"components.PermissionEdit.autorequestSeriesDescription": "Toestemming geven om niet-4K series in je Plex-kijklijst automatisch aan te vragen.",
|
||||
"components.PermissionEdit.viewwatchlists": "Plex-kijklijsten bekijken",
|
||||
"components.PermissionEdit.viewwatchlistsDescription": "Toestemming verlenen om de Plex-kijklijsten van andere gebruikers te bekijken.",
|
||||
"components.Settings.SettingsLogs.viewdetails": "Details bekijken",
|
||||
"components.Settings.advancedTooltip": "Deze instelling onjuist configureren, kan resulteren in gebroken functionaliteit",
|
||||
"components.StatusChecker.reloadApp": "{applicationTitle} opnieuw laden",
|
||||
"components.TitleCard.tmdbid": "TMDB ID",
|
||||
"components.StatusChecker.appUpdatedDescription": "Klik op de onderstaande knop om de toepassing opnieuw te laden.",
|
||||
"components.UserProfile.plexwatchlist": "Plex Kijklijst",
|
||||
"components.UserProfile.plexwatchlist": "Plex-kijklijst",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Films automatisch aanvragen",
|
||||
"components.TvDetails.manageseries": "Serie beheren",
|
||||
"components.MovieDetails.managemovie": "Film beheren",
|
||||
@@ -1075,7 +1075,7 @@
|
||||
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Filmverzoeken",
|
||||
"components.Layout.UserDropdown.requests": "Verzoeken",
|
||||
"components.RequestBlock.decline": "Verzoek weigeren",
|
||||
"components.Discover.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.Discover.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex-kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.RequestBlock.delete": "Verzoek verwijderen",
|
||||
"components.RequestBlock.edit": "Verzoek bewerken",
|
||||
"components.RequestBlock.lastmodifiedby": "Laatst gewijzigd door",
|
||||
@@ -1107,7 +1107,7 @@
|
||||
"components.Discover.customizediscover": "Ontdekken aanpassen",
|
||||
"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.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.networks": "Netwerken",
|
||||
@@ -1138,7 +1138,7 @@
|
||||
"components.Discover.updatesuccess": "Instellingen Ontdekken bijgewerkt.",
|
||||
"components.Discover.DiscoverMovies.sortPopularityAsc": "Populariteit oplopend",
|
||||
"components.Discover.DiscoverMovies.sortPopularityDesc": "Populariteit aflopend",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Releasedatum oplopend",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Verschijningsdatum oplopend",
|
||||
"components.Discover.DiscoverMovies.sortTitleAsc": "Titel oplopend (A-Z)",
|
||||
"components.Discover.DiscoverMovies.sortTitleDesc": "Titel aflopend (Z-A)",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMDB-beoordeling oplopend",
|
||||
@@ -1148,8 +1148,8 @@
|
||||
"components.Discover.DiscoverTv.discovertv": "Series",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Eerste uitzenddatum oplopend",
|
||||
"components.Discover.DiscoverTv.sortPopularityDesc": "Populariteit aflopend",
|
||||
"components.Discover.DiscoverTv.sortTitleAsc": "Titel (A-Z) oplopend",
|
||||
"components.Discover.DiscoverTv.sortTitleDesc": "Titel (Z-A) aflopend",
|
||||
"components.Discover.DiscoverTv.sortTitleAsc": "Titel oplopend (A-Z)",
|
||||
"components.Discover.DiscoverTv.sortTitleDesc": "Titel aflopend (Z-A)",
|
||||
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# filter actief} other {# filters actief}}",
|
||||
"components.Discover.FilterSlideover.clearfilters": "Actieve filters wissen",
|
||||
"components.Discover.FilterSlideover.filters": "Filters",
|
||||
@@ -1159,8 +1159,8 @@
|
||||
"components.Discover.FilterSlideover.keywords": "Trefwoorden",
|
||||
"components.Discover.FilterSlideover.originalLanguage": "Oorspronkelijke taal",
|
||||
"components.Discover.FilterSlideover.ratingText": "Beoordelingen tussen {minValue} en {maxValue}",
|
||||
"components.Discover.FilterSlideover.releaseDate": "Releasedatum",
|
||||
"components.Discover.FilterSlideover.runtime": "Duur",
|
||||
"components.Discover.FilterSlideover.releaseDate": "Verschijningsdatum",
|
||||
"components.Discover.FilterSlideover.runtime": "Speelduur",
|
||||
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minuten looptijd",
|
||||
"components.Discover.FilterSlideover.studio": "Studio",
|
||||
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB-gebruikersscore",
|
||||
@@ -1189,7 +1189,7 @@
|
||||
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# filter actief} other {# filters actief}}",
|
||||
"components.Discover.DiscoverMovies.discovermovies": "Films",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "TMDB-beoordeling aflopend",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Releasedatum aflopend",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Verschijningsdatum aflopend",
|
||||
"components.Discover.DiscoverTv.sortPopularityAsc": "Populariteit oplopend",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "TMDB-beoordeling oplopend",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Eerste uitzenddatum aflopend",
|
||||
@@ -1290,24 +1290,24 @@
|
||||
"i18n.collection": "Collectie",
|
||||
"components.UserProfile.localWatchlist": "Kijklijst van {username}",
|
||||
"components.Setup.signinWithPlex": "Vul de Plex gegevens in",
|
||||
"components.Settings.jellyfinSettingsDescription": "Configureer optioneel de interne en externe eindpunten voor de {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 je doorverwezen wilt worden naar een andere wachtwoord reset pagina. Je kunt ook de Jellyfin API-sleutel wijzigen, die eerder automatisch is gegenereerd.",
|
||||
"components.Settings.jellyfinSettingsDescription": "Configureer optioneel de interne en externe eindpunten voor je {mediaServerName}-server. Meestal verschilt de externe URL van de interne URL. Als je wilt doorverwijzen naar een andere pagina voor wachtwoordherstel, kun je een aangepaste URL voor wachtwoordherstel instellen voor het aanmelden met {mediaServerName}. Je kunt ook de API-sleutel voor Jellyfin wijzigen, die eerder automatisch is gegenereerd.",
|
||||
"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.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName}-{userCount, plural, one {gebruiker} other {gebruikers}} succesvol geïmporteerd!",
|
||||
"components.UserList.newJellyfinsigninenabled": "De instelling <strong>Nieuwe {mediaServerName}-aanmelding gebruiken</strong> is momenteel ingeschakeld. {mediaServerName}-gebruikers met toegang tot de bibliotheek hoeven niet geïmporteerd te worden om zich te kunnen aanmelden.",
|
||||
"components.Login.back": "Ga terug",
|
||||
"components.Login.invalidurlerror": "Kan geen verbinding maken met de {mediaServerName} server.",
|
||||
"components.TvDetails.addtowatchlist": "Toevoegen aan kijklijst",
|
||||
"components.Login.validationUrlBaseTrailingSlash": "URL basis mag niet eindigen met een schuine streep",
|
||||
"components.Selector.returningSeries": "Terugkerende serie",
|
||||
"components.MovieDetails.addtowatchlist": "Toevoegen aan kijklijst",
|
||||
"components.MovieDetails.watchlistDeleted": "<strong>{title}</strong> is succesvol verwijderd uit de kijklijst!",
|
||||
"components.MovieDetails.watchlistSuccess": "<strong>{title}</strong> is succesvol toegevoegd aan de kijklijst!",
|
||||
"components.TvDetails.watchlistDeleted": "<strong>{title}</strong> is succesvol verwijderd uit de kijklijst!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Geldig e-mailadres verplicht",
|
||||
"components.Login.adminerror": "Je moet een beheerdersaccount gebruiken om in te loggen.",
|
||||
"components.MovieDetails.watchlistDeleted": "<strong>{title}</strong> is succesvol verwijderd van je kijklijst!",
|
||||
"components.MovieDetails.watchlistSuccess": "<strong>{title}</strong> is succesvol toegevoegd aan je kijklijst!",
|
||||
"components.TvDetails.watchlistDeleted": "<strong>{title}</strong> is succesvol verwijderd van de kijklijst!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Geldig e-mailadres vereist",
|
||||
"components.Login.adminerror": "Je moet een beheerdersaccount gebruiken om je aan te melden.",
|
||||
"components.Selector.inProduction": "In productie",
|
||||
"components.Discover.FilterSlideover.status": "Status",
|
||||
"components.Login.hostname": "{mediaServerName} URL",
|
||||
@@ -1315,10 +1315,10 @@
|
||||
"components.Login.servertype": "Servertype",
|
||||
"components.Login.validationHostnameRequired": "Je moet een geldige hostnaam of IP-adres opgeven",
|
||||
"components.Login.validationPortRequired": "Je moet een geldig poortnummer opgeven",
|
||||
"components.Login.validationUrlBaseLeadingSlash": "URL basis moet beginnen met een schuine streep",
|
||||
"components.Login.validationUrlBaseLeadingSlash": "URL-basis moet beginnen met een schuine streep",
|
||||
"components.Login.validationUrlTrailingSlash": "URL mag niet eindigen met een schuine streep",
|
||||
"components.Login.validationservertyperequired": "Kies een servertype",
|
||||
"components.MovieDetails.removefromwatchlist": "Verwijderen uit kijklijst",
|
||||
"components.MovieDetails.removefromwatchlist": "Verwijderen van kijklijst",
|
||||
"components.MovieDetails.watchlistError": "Er is iets misgegaan, probeer het opnieuw.",
|
||||
"components.RequestList.RequestItem.profileName": "Profiel",
|
||||
"components.Selector.canceled": "Geannuleerd",
|
||||
@@ -1337,14 +1337,71 @@
|
||||
"components.Setup.configplex": "Configureer Plex",
|
||||
"components.Setup.servertype": "Kies servertype",
|
||||
"components.Setup.signinWithEmby": "Vul de Emby gegevens in",
|
||||
"components.Setup.subtitle": "egin met het kiezen van je mediaserver",
|
||||
"components.Setup.subtitle": "Begin met het kiezen van je mediaserver",
|
||||
"components.StatusBadge.seasonnumber": "S{seasonNumber}",
|
||||
"components.TvDetails.removefromwatchlist": "Verwijderen uit kijklijst",
|
||||
"components.TvDetails.watchlistError": "Er is iets misgegaan, probeer het opnieuw.",
|
||||
"components.TvDetails.watchlistSuccess": "<strong>{title}</strong> is succesvol toegevoegd aan de kijklijst!",
|
||||
"components.UserList.username": "Gebruikersnaam",
|
||||
"components.UserList.validationUsername": "Je moet een gebruikersnaam opgeven",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "E-mailadres verplicht",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "E-mailadres vereist",
|
||||
"components.Login.enablessl": "Gebruik SSL",
|
||||
"components.Login.urlBase": "URL basis"
|
||||
"components.Login.urlBase": "URL basis",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Streamingdiensten tonen op regionale beschikbaarheid",
|
||||
"component.BlacklistBlock.blacklistdate": "Datum geblokkeerd",
|
||||
"components.Settings.scanbackground": "Het scannen gebeurt op de achtergrond. In de tussentijd kun je verdergaan met het instelproces.",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> staat niet op de blokkeerlijst.",
|
||||
"components.Settings.SettingsMain.validationProxyPort": "Je moet een geldige poort opgeven",
|
||||
"components.Layout.Sidebar.blacklist": "Blokkeerlijst",
|
||||
"components.Settings.SettingsMain.streamingRegionTip": "Streamingdiensten tonen op regionale beschikbaarheid",
|
||||
"components.PermissionEdit.manageblacklistDescription": "Toestemming geven om geblokkeerde media te beheren.",
|
||||
"components.RequestList.RequestItem.removearr": "Verwijderen van {arr}",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Regio voor ontdekken",
|
||||
"components.Settings.apiKey": "API-sleutel",
|
||||
"i18n.blacklistSuccess": "<strong>{title}</strong> is succesvol geblokkeerd.",
|
||||
"i18n.addToBlacklist": "Toevoegen aan blokkeerlijst",
|
||||
"component.BlacklistBlock.blacklistedby": "Geblokkeerd door",
|
||||
"component.BlacklistModal.blacklisting": "Blokkeren",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Media op de blokkeerlijst beheren.",
|
||||
"components.Blacklist.blacklistdate": "datum",
|
||||
"components.Blacklist.blacklistedby": "{date} door {user}",
|
||||
"components.Blacklist.blacklistsettings": "Instellingen blokkeerlijst",
|
||||
"components.Blacklist.mediaName": "Naam",
|
||||
"components.Blacklist.mediaTmdbId": "tmdb-id",
|
||||
"components.Blacklist.mediaType": "Type",
|
||||
"components.PermissionEdit.blacklistedItems": "Media blokkeren.",
|
||||
"components.PermissionEdit.blacklistedItemsDescription": "Toestemming geven om media te blokkeren.",
|
||||
"components.PermissionEdit.manageblacklist": "Blokkeerlijst beheren",
|
||||
"components.PermissionEdit.viewblacklistedItems": "Geblokkeerde media inzien.",
|
||||
"components.PermissionEdit.viewblacklistedItemsDescription": "Toestemming geven om geblokkeerde media in te zien.",
|
||||
"components.RequestList.sortDirection": "Sorteerrichting wisselen",
|
||||
"components.Settings.Notifications.validationWebhookRoleId": "Je moet een geldige Discord Role ID opgeven",
|
||||
"components.Settings.Notifications.webhookRoleId": "Role-id melding",
|
||||
"components.Settings.Notifications.webhookRoleIdTip": "De role-id die in het webhook-bericht vermeld moet worden. Laat leeg om vermeldingen uit te zetten",
|
||||
"components.Settings.SettingsJobsCache.usersavatars": "Gebruikersavatars",
|
||||
"components.Settings.SettingsMain.discoverRegion": "Regio voor Ontdekken",
|
||||
"components.Settings.SettingsMain.discoverRegionTip": "Inhoud filteren op regionale beschikbaarheid",
|
||||
"components.Settings.SettingsMain.streamingRegion": "Regio voor streamen",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Inhoud filteren op regionale beschikbaarheid",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Regio voor streamen",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "Deze e-mail is bezet!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Een andere gebruiker heeft deze gebruikersnaam al. Je moet een e-mail instellen",
|
||||
"i18n.blacklist": "Blokkeerlijst",
|
||||
"i18n.blacklistDuplicateError": "<strong>{title}</strong> staat al op de blokkeerlijst.",
|
||||
"i18n.blacklistError": "Er ging iets mis; probeer opnieuw.",
|
||||
"i18n.blacklisted": "Geblokkeerd",
|
||||
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> is succesvol verwijderd van de blokkeerlijst.",
|
||||
"i18n.removefromBlacklist": "Verwijderen van blokkeerlijst",
|
||||
"i18n.specials": "Specials",
|
||||
"components.Settings.SettingsJobsCache.plex-refresh-token": "Plex-verversingstoken",
|
||||
"components.Settings.SettingsMain.proxyBypassFilter": "Genegeerde proxy-adressen",
|
||||
"components.Settings.SettingsMain.proxyBypassFilterTip": "Gebruik ',' als scheidingsteken en '*' als joker voor subdomeinen",
|
||||
"components.Settings.SettingsMain.proxyBypassLocalAddresses": "Proxy omzeilen voor lokale adressen",
|
||||
"components.Settings.SettingsMain.proxyEnabled": "HTTP(S)-proxy",
|
||||
"components.Settings.SettingsMain.proxyHostname": "Hostnaam proxy",
|
||||
"components.Settings.SettingsMain.proxyPassword": "Wachtwoord proxy",
|
||||
"components.Settings.SettingsMain.proxyPort": "Proxypoort",
|
||||
"components.Settings.SettingsMain.proxySsl": "SSL/TLS gebruiken voor proxy",
|
||||
"components.Settings.SettingsMain.proxyUser": "Gebruikersnaam proxy",
|
||||
"components.Settings.tip": "Tip"
|
||||
}
|
||||
|
||||
@@ -1211,5 +1211,14 @@
|
||||
"components.Discover.emptywatchlist": "Mídia adicionadas à sua <PlexWatchlistSupportLink>Lista Para Assistir do Plex</PlexWatchlistSupportLink> aparecerão aqui.",
|
||||
"components.RequestList.RequestItem.unknowntitle": "Título Desconhecido",
|
||||
"components.Selector.searchGenres": "Selecione os gêneros…",
|
||||
"components.Selector.searchKeywords": "Pesquisar palavras-chave…"
|
||||
"components.Selector.searchKeywords": "Pesquisar palavras-chave…",
|
||||
"component.BlacklistBlock.blacklistdate": "Data de inclusão na lista negra",
|
||||
"components.Blacklist.blacklistsettings": "Definições da lista negra",
|
||||
"component.BlacklistBlock.blacklistedby": "Colocado na lista negra por",
|
||||
"component.BlacklistModal.blacklisting": "Lista negra",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}2</strong> não está na lista negra.",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Faça a gestão dos conteúdos multimédia colocados na lista negra.",
|
||||
"components.Blacklist.blacklistdate": "data",
|
||||
"components.Blacklist.blacklistedby": "{date} por {user}",
|
||||
"components.Blacklist.mediaName": "Nome"
|
||||
}
|
||||
|
||||
@@ -1336,5 +1336,23 @@
|
||||
"components.Settings.invalidurlerror": "Не удалось подключиться к {mediaServerName} серверу.",
|
||||
"components.Settings.jellyfinForgotPasswordUrl": "Забыли пароль URL",
|
||||
"components.Settings.jellyfinSyncFailedGenericError": "Что-то пошло не так при синхронизации библиотек",
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "Библиотеки не найдены"
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "Библиотеки не найдены",
|
||||
"component.BlacklistBlock.blacklistdate": "Дата внесения в чёрный список",
|
||||
"components.Blacklist.blacklistSettingsDescription": "Управлять медиа в чёрном списке.",
|
||||
"components.PermissionEdit.manageblacklistDescription": "Предоставить разрешение на управление чёрным списком.",
|
||||
"components.PermissionEdit.viewblacklistedItems": "Открыть чёрный список.",
|
||||
"components.Blacklist.mediaType": "Тип",
|
||||
"components.Layout.Sidebar.blacklist": "Чёрный список",
|
||||
"components.PermissionEdit.blacklistedItems": "Внести в чёрный список.",
|
||||
"components.Login.hostname": "Адрес {mediaServerName}",
|
||||
"component.BlacklistBlock.blacklistedby": "Внесено в чёрный список",
|
||||
"component.BlacklistModal.blacklisting": "Внесение в чёрный список",
|
||||
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> не в чёрном списке.",
|
||||
"components.Blacklist.blacklistdate": "дата",
|
||||
"components.Blacklist.blacklistedby": "{date}, {user}",
|
||||
"components.Blacklist.blacklistsettings": "Настройки чёрного списка",
|
||||
"components.Blacklist.mediaName": "Название",
|
||||
"components.Blacklist.mediaTmdbId": "tmdb Id",
|
||||
"components.PermissionEdit.blacklistedItemsDescription": "Дать права на внесение в чёрный список.",
|
||||
"components.PermissionEdit.manageblacklist": "Управлять чёрным списком"
|
||||
}
|
||||
|
||||
@@ -272,10 +272,10 @@
|
||||
"components.ManageSlideOver.removearr": "{arr}'dan Kaldır",
|
||||
"components.ManageSlideOver.tvshow": "dizi",
|
||||
"components.MediaSlider.ShowMoreCard.seemore": "Daha Fazla",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Tüm Kadro",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Tüm Oyuncular",
|
||||
"components.MovieDetails.MovieCrew.fullcrew": "Tüm Ekip",
|
||||
"components.MovieDetails.budget": "Bütçe",
|
||||
"components.MovieDetails.cast": "Kadro",
|
||||
"components.MovieDetails.cast": "Oyuncular",
|
||||
"components.MovieDetails.digitalrelease": "Dijital Sürüm",
|
||||
"components.MovieDetails.downloadstatus": "İndirme Durumu",
|
||||
"components.MovieDetails.managemovie": "Filmi Yönet",
|
||||
@@ -375,7 +375,7 @@
|
||||
"components.PersonDetails.appearsin": "Yer Aldığı İçerikler",
|
||||
"components.PersonDetails.ascharacter": "{character} rolüyle",
|
||||
"components.PersonDetails.birthdate": "Doğumu {birthdate}",
|
||||
"components.PersonDetails.crewmember": "Ekibi",
|
||||
"components.PersonDetails.crewmember": "Bilinen İşleri",
|
||||
"components.PersonDetails.lifespan": "{birthdate} – {deathdate}",
|
||||
"components.PlexLoginButton.signingin": "Giriş Yapılıyor…",
|
||||
"components.PlexLoginButton.signinwithplex": "Giriş Yap",
|
||||
@@ -929,7 +929,7 @@
|
||||
"components.Settings.Notifications.toastDiscordTestFailed": "Discord test bildirimi gönderilemedi.",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Asgari erişilebilirlik ayarını seç",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Geçerli bir sunucu adresi girmelisin",
|
||||
"components.Settings.SettingsAbout.betawarning": "Bu bir BETA yazılımdır. Özellikler hatalı ya da dengesiz olabilir. Bir hatala karşılaşırsanız lütfen GitHub'dan bildirin!",
|
||||
"components.Settings.SettingsAbout.betawarning": "Bu BETA yazılımdır. Özellikler bozuk ve/veya dengesiz olabilir. Karşılaştığınız herhangi bir sorunu lütfen GitHub'da bildirin!",
|
||||
"components.Settings.SettingsMain.cacheImagesTip": "Dış kaynaklardan alınan resimleri önbelleğe al (depolama kullanımını arttıracaktır)",
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "<WebhookLink>Bir Webhook</WebhookLink> entegrasyonu oluştur",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Discord bildirim ayarları kaydedildi!",
|
||||
@@ -938,7 +938,7 @@
|
||||
"components.Settings.Notifications.webhookUrlTip": "Sunucunuz için bir <DiscordWebhookLink>Webhook</DiscordWebhookLink> entegrasyonu oluştur",
|
||||
"components.Settings.SettingsAbout.outofdate": "Eski Sürüm",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "Jellyseerr'in yalnızca geliştirmeye katkıda bulunan veya en son testlere yardımcı olan kişiler için önerilen <code>develop</code> dalını çalıştırıyorsunuz.",
|
||||
"components.Settings.SettingsJobsCache.cachevsize": "Önbelleğin Büyüklüğü",
|
||||
"components.Settings.SettingsJobsCache.cachevsize": "Değer Boyutu",
|
||||
"components.Settings.SettingsJobsCache.imagecacheDescription": "Ayarlarda etkinleştirildiğinde, Jellyseerr önceden yapılandırılmış harici kaynaklardan gelen görüntüleri proxy'leyecek ve önbelleğe alacaktır. Önbelleğe alınan görüntüler yapılandırma klasörünüze kaydedilir. Dosyaları <code>{appDataPath}/cache/images</code> konumunda bulabilirsiniz.",
|
||||
"components.Settings.SettingsUsers.userSettingsDescription": "Genel ve varsayılan kullanıcı ayarlarını yapılandırın.",
|
||||
"components.Settings.SonarrModal.editsonarr": "Sonarr Sunucusunu Düzenle",
|
||||
@@ -1036,10 +1036,10 @@
|
||||
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> izleme listesine başarıyla eklendi!",
|
||||
"components.TvDetails.Season.noepisodes": "Bölüm listesi mevcut değil.",
|
||||
"components.TvDetails.Season.somethingwentwrong": "Sezon verisi alınırken bir şeyler ters gitti.",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Dizinin Tam Kadrosu",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Dizinin Tüm Oyuncuları",
|
||||
"components.TvDetails.TvCrew.fullseriescrew": "Dizinin Tam Ekibi",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.cast": "Kadro",
|
||||
"components.TvDetails.cast": "Oyuncular",
|
||||
"components.TvDetails.episodeRuntime": "Bölümün Süresi",
|
||||
"components.TvDetails.episodeRuntimeMinutes": "{runtime} dakika",
|
||||
"components.TvDetails.firstAirDate": "İlk Yayın Tarihi",
|
||||
@@ -1291,7 +1291,7 @@
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "30 karakterlik <UsersGroupsLink>kullanıcı ya da grup kimliği</UsersGroupsLink>",
|
||||
"components.UserProfile.emptywatchlist": "<PlexWatchlistSupportLink>Plex İzleme Listenize</PlexWatchlistSupportLink> eklenen içerikler burada gözükeceklerdir.",
|
||||
"i18n.restartRequired": "Yeniden Başlatma Gereklidir",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Discord hesabınız ile bağıntılı <FindDiscordIdLink>numaralardan oluşan kullanıcı ID'niz</FindDiscordIdLink>",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Discord kullanıcı hesabınızla ilişkili <FindDiscordIdLink>çok haneli kimlik numarası</FindDiscordIdLink>",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Pushbullet bildirim ayarları kaydedildi!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Bir sohbet başlatın</TelegramBotLink> ve <GetIdBotLink>@get_id_bot</GetIdBotLink> ID'li botunuzu ekleyin. Son olarak <code>/my_id</code> komutunu kullanın",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Geçerli bir uygulama token'i sağlamalısınız",
|
||||
|
||||
@@ -1375,5 +1375,8 @@
|
||||
"i18n.addToBlacklist": "Додати в чорний список",
|
||||
"i18n.blacklist": "Чорний список",
|
||||
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong>було успішно видалено з чорного списку.",
|
||||
"i18n.blacklisted": "У чорному списку"
|
||||
"i18n.blacklisted": "У чорному списку",
|
||||
"components.Settings.SettingsJobsCache.usersavatars": "Аватар користувача",
|
||||
"components.PermissionEdit.manageblacklist": "Керувати чорним списком",
|
||||
"components.Settings.Notifications.validationWebhookRoleId": "Ви повинні надати дійсний ідентифікатор ролі Discord"
|
||||
}
|
||||
|
||||
@@ -342,15 +342,7 @@
|
||||
}
|
||||
|
||||
button.input-action {
|
||||
@apply relative -ml-px inline-flex items-center border border-gray-500 bg-indigo-600 bg-opacity-80 px-3 py-2 text-sm font-medium leading-5 text-white last:rounded-r-md sm:px-3.5;
|
||||
}
|
||||
|
||||
button.input-action[disabled] {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
button.input-action:not([disabled]) {
|
||||
@apply transition duration-150 ease-in-out hover:bg-opacity-100 active:bg-gray-100 active:text-gray-700;
|
||||
@apply relative -ml-px inline-flex items-center border border-gray-500 bg-indigo-600 bg-opacity-80 px-3 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out last:rounded-r-md hover:bg-opacity-100 active:bg-gray-100 active:text-gray-700 sm:px-3.5;
|
||||
}
|
||||
|
||||
.button-md :where(svg),
|
||||
@@ -390,10 +382,6 @@
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.react-select-container:has(+ .input-action) .react-select__control {
|
||||
@apply rounded-r-none border border-gray-500 bg-gray-700 text-white hover:border-gray-500;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__control {
|
||||
@apply rounded-md border border-gray-500 bg-gray-700 text-white hover:border-gray-500;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES2021",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
Reference in New Issue
Block a user