Compare commits

...

13 Commits

Author SHA1 Message Date
Joaquin Olivero
818aa60aac feat: blacklist items from Discover page (#632)
* feat: blacklist media items

re #490

* feat: blacklist media items

* feat: blacklist media items

* style: formatting

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

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

* refactor: refactor component to accept show boolean

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

* style: formatting

---------

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

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

* Adding debug statements

* Updating

* feat: adding env var for API key

* feat: update

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

* Update en.json

* docs: added documentation about API_KEY environment variable

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

* feat: always check the API_KEY env var at startup

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

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

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

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

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

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

* feat(api): add severType to the api

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

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

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

* fix: issue page formatMessage for 4k media

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

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

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

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

* feat: add server type step to setup

* feat: migrate existing emby setups to use emby mediaServerType

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

* fix: emby media server type migration

* refactor: change emby logo to full logo

* style: decrease emby logo size in setup screen

* refactor: use title case for servertype i18n message

* refactor(i18n): fix a typo

* refactor: use enums instead of numbers

* fix: remove old references to JELLYFIN_TYPE environment variable

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

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

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

---------

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

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

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

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

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

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

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

* Added translation using Weblate (Turkish)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* style(i18n): ran prettier

* style(i18n): ran prettier

---------

Co-authored-by: Boštjan KOLAR <civywl@users.noreply.jellyseerr.borgcube.de>
Co-authored-by: Alex F <weblate@xathon.de>
Co-authored-by: Bas <910100490+weblate@proton.me>
Co-authored-by: N/A <me@puffin.icu>
Co-authored-by: Ramon Stohr <ramonstohr@gmail.com>
Co-authored-by: Cosmin Mocan <cosmin_mocan@hotmail.com>
Co-authored-by: Aleksandr <AlexZagric@users.noreply.jellyseerr.borgcube.de>
Co-authored-by: Aleksandr <alexzag2004@gmail.com>
Co-authored-by: Bas Muldder <bas.d.mulder@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Co-authored-by: Eduard Perez Mendez <eduardperezmendez@gmail.com>
Co-authored-by: Quack6765 <weblate@po-mail.com>
Co-authored-by: grayair <grayair@proton.me>
Co-authored-by: osh <osh@osh.cc>
Co-authored-by: uqlel <jellyseer.borgcube.de@uqlel.ovh>
Co-authored-by: Flashk <mevengar@gmail.com>
Co-authored-by: michael <michaelvelosk@gmail.com>
Co-authored-by: A a <arnau2106@gmail.com>
Co-authored-by: Albert Einstien <dbig350@gmail.com>
Co-authored-by: C W <the-eggs@163.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Adrian Konopczynski <adrikonop@anomalie.ga>
Co-authored-by: Wiktor Kowalski <a1opnxgtr@mozmail.com>
Co-authored-by: Jakob Števanec <jakolin98.windowslive@gmail.com>
Co-authored-by: Mattias Magnusson <mattish.91@gmail.com>
Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
2024-08-16 23:02:16 +05:00
83 changed files with 6465 additions and 3410 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,10 +6,6 @@ module.exports = {
commitTag: process.env.COMMIT_TAG || 'local',
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
},
publicRuntimeConfig: {
// Will be available on both server and client
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
},
images: {
remotePatterns: [
{ hostname: 'gravatar.com' },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ export interface DownloadingItem {
timeLeft: string;
estimatedCompletionTime: Date;
title: string;
downloadId: string;
episode?: EpisodeNumberResult;
}
@@ -95,6 +96,7 @@ class DownloadTracker {
status: item.status,
timeLeft: item.timeleft,
title: item.title,
downloadId: item.downloadId,
}));
if (queueItems.length > 0) {
@@ -172,6 +174,7 @@ class DownloadTracker {
timeLeft: item.timeleft,
title: item.title,
episode: item.episode,
downloadId: item.downloadId,
}));
if (queueItems.length > 0) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,6 @@ import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -108,7 +107,6 @@ const IssueDetails = () => {
(opt) => opt.issueType === issueData?.issueType
);
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -390,7 +388,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
@@ -437,7 +436,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})
@@ -662,7 +662,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
@@ -708,7 +709,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import Table from '@app/components/Common/Table';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { formatBytes } from '@app/utils/numberHelpers';
@@ -57,8 +58,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
'plex-recently-added-scan': 'Plex Recently Added Scan',
'plex-full-scan': 'Plex Full Library Scan',
'plex-watchlist-sync': 'Plex Watchlist Sync',
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
'jellyfin-full-scan': 'Jellyfin Full Library Scan',
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
'availability-sync': 'Media Availability Sync',
'radarr-scan': 'Radarr Scan',
'sonarr-scan': 'Sonarr Scan',
@@ -167,6 +168,20 @@ const SettingsJobs = () => {
const [isSaving, setIsSaving] = useState(false);
const settings = useSettings();
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
messages['jellyfin-recently-added-scan'] = {
id: 'jellyfin-recently-added-scan',
defaultMessage: 'Emby Recently Added Scan',
};
}
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
messages['jellyfin-full-scan'] = {
id: 'jellyfin-full-scan',
defaultMessage: 'Emby Full Library Scan',
};
}
if (!data && !error) {
return <LoadingSpinner />;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import getConfig from 'next/config';
import Image from 'next/image';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -36,7 +36,6 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
}) => {
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const [isImporting, setImporting] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -81,7 +80,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
userCount: createdUsers.length,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
@@ -96,7 +97,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
addToast(
intl.formatMessage(messages.importfromJellyfinerror, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
@@ -134,7 +137,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
loading={!data && !error}
title={intl.formatMessage(messages.importfromJellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
})}
onOk={() => {
importUsers();
@@ -151,7 +156,8 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<Alert
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
strong: (msg: React.ReactNode) => (
@@ -277,7 +283,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<Alert
title={intl.formatMessage(messages.noJellyfinuserstoimport, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
})}
type="info"
/>

View File

@@ -28,7 +28,6 @@ import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -90,7 +89,6 @@ const UserList = () => {
const intl = useIntl();
const router = useRouter();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
@@ -535,7 +533,8 @@ const UserList = () => {
>
<InboxArrowDownIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Emby',
})
@@ -690,7 +689,7 @@ const UserList = () => {
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
) : user.userType === UserType.EMBY ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Emby',

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,18 @@
{
"component.BlacklistBlock.blacklistdate": "Blacklisted date",
"component.BlacklistBlock.blacklistedby": "Blacklisted By",
"component.BlacklistModal.blacklisting": "Blacklisting",
"components.AirDateBadge.airedrelative": "Aired {relativeTime}",
"components.AirDateBadge.airsrelative": "Airing {relativeTime}",
"components.AppDataWarning.dockerVolumeMissingDescription": "The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.",
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> is not blacklisted.",
"components.Blacklist.blacklistSettingsDescription": "Manage blacklisted media.",
"components.Blacklist.blacklistdate": "date",
"components.Blacklist.blacklistedby": "{date} by {user}",
"components.Blacklist.blacklistsettings": "Blacklist Settings",
"components.Blacklist.mediaName": "Name",
"components.Blacklist.mediaTmdbId": "tmdb Id",
"components.Blacklist.mediaType": "Type",
"components.CollectionDetails.numberofmovies": "{count} Movies",
"components.CollectionDetails.overview": "Overview",
"components.CollectionDetails.requestcollection": "Request Collection",
@@ -73,6 +84,7 @@
"components.Discover.FilterSlideover.releaseDate": "Release Date",
"components.Discover.FilterSlideover.runtime": "Runtime",
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime",
"components.Discover.FilterSlideover.status": "Status",
"components.Discover.FilterSlideover.streamingservices": "Streaming Services",
"components.Discover.FilterSlideover.studio": "Studio",
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
@@ -199,6 +211,7 @@
"components.LanguageSelector.originalLanguageDefault": "All Languages",
"components.Layout.LanguagePicker.displaylanguage": "Display Language",
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
"components.Layout.Sidebar.blacklist": "Blacklist",
"components.Layout.Sidebar.browsemovies": "Movies",
"components.Layout.Sidebar.browsetv": "Series",
"components.Layout.Sidebar.dashboard": "Discover",
@@ -220,6 +233,7 @@
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
"components.Login.adminerror": "You must use an admin account to sign in.",
"components.Login.back": "Go back",
"components.Login.credentialerror": "The username or password is incorrect.",
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
"components.Login.email": "Email Address",
@@ -235,6 +249,7 @@
"components.Login.port": "Port",
"components.Login.save": "Add",
"components.Login.saving": "Adding…",
"components.Login.servertype": "Server Type",
"components.Login.signin": "Sign In",
"components.Login.signingin": "Signing In…",
"components.Login.signinheader": "Sign in to continue",
@@ -256,6 +271,7 @@
"components.Login.validationhostformat": "Valid URL required",
"components.Login.validationhostrequired": "{mediaServerName} URL required",
"components.Login.validationpasswordrequired": "You must provide a password",
"components.Login.validationservertyperequired": "Please select a server type",
"components.Login.validationusernamerequired": "Username required",
"components.ManageSlideOver.alltime": "All Time",
"components.ManageSlideOver.downloadstatus": "Downloads",
@@ -383,8 +399,12 @@
"components.PermissionEdit.autorequestMoviesDescription": "Grant permission to automatically submit requests for non-4K movies via Plex Watchlist.",
"components.PermissionEdit.autorequestSeries": "Auto-Request Series",
"components.PermissionEdit.autorequestSeriesDescription": "Grant permission to automatically submit requests for non-4K series via Plex Watchlist.",
"components.PermissionEdit.blacklistedItems": "Blacklist media.",
"components.PermissionEdit.blacklistedItemsDescription": "Grant permission to blacklist media.",
"components.PermissionEdit.createissues": "Report Issues",
"components.PermissionEdit.createissuesDescription": "Grant permission to report media issues.",
"components.PermissionEdit.manageblacklist": "Manage Blacklist",
"components.PermissionEdit.manageblacklistDescription": "Grant permission to manage blacklisted media.",
"components.PermissionEdit.manageissues": "Manage Issues",
"components.PermissionEdit.manageissuesDescription": "Grant permission to manage media issues.",
"components.PermissionEdit.managerequests": "Manage Requests",
@@ -403,6 +423,8 @@
"components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.",
"components.PermissionEdit.users": "Manage Users",
"components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
"components.PermissionEdit.viewblacklistedItems": "View blacklisted media.",
"components.PermissionEdit.viewblacklistedItemsDescription": "Grant permission to view blacklisted media.",
"components.PermissionEdit.viewissues": "View Issues",
"components.PermissionEdit.viewissuesDescription": "Grant permission to view media issues reported by other users.",
"components.PermissionEdit.viewrecent": "View Recently Added",
@@ -555,9 +577,16 @@
"components.ResetPassword.validationpasswordrequired": "You must provide a password",
"components.Search.search": "Search",
"components.Search.searchresults": "Search Results",
"components.Selector.canceled": "Canceled",
"components.Selector.ended": "Ended",
"components.Selector.inProduction": "In Production",
"components.Selector.nooptions": "No results.",
"components.Selector.pilot": "Pilot",
"components.Selector.planned": "Planned",
"components.Selector.returningSeries": "Returning Series",
"components.Selector.searchGenres": "Select genres…",
"components.Selector.searchKeywords": "Search keywords…",
"components.Selector.searchStatus": "Select status...",
"components.Selector.searchStudios": "Search studios…",
"components.Selector.showless": "Show Less",
"components.Selector.showmore": "Show More",
@@ -1039,23 +1068,31 @@
"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.Setup.back": "Go back",
"components.Setup.configemby": "Configure Emby",
"components.Setup.configjellyfin": "Configure Jellyfin",
"components.Setup.configplex": "Configure Plex",
"components.Setup.configuremediaserver": "Configure Media Server",
"components.Setup.configureservices": "Configure Services",
"components.Setup.continue": "Continue",
"components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing…",
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithJellyfin": "Use your {mediaServerName} account",
"components.Setup.signinWithPlex": "Use your Plex account",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
"components.Setup.signinWithPlex": "Enter your Plex details",
"components.Setup.subtitle": "Get started by choosing your media server",
"components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Jellyseerr",
"components.StatusBadge.managemedia": "Manage {mediaType}",
"components.StatusBadge.openinarr": "Open in {arr}",
"components.StatusBadge.playonplex": "Play on {mediaServerName}",
"components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}",
"components.StatusBadge.seasonnumber": "S{seasonNumber}",
"components.StatusBadge.status": "{status}",
"components.StatusBadge.status4k": "4K {status}",
"components.StatusChecker.appUpdated": "{applicationTitle} Updated",
@@ -1280,6 +1317,11 @@
"i18n.areyousure": "Are you sure?",
"i18n.available": "Available",
"i18n.back": "Back",
"i18n.blacklist": "Blacklist",
"i18n.blacklistDuplicateError": "<strong>{title}</strong> has already been blacklisted.",
"i18n.blacklistError": "Something went wrong try again.",
"i18n.blacklistSuccess": "<strong>{title}</strong> was successfully blacklisted.",
"i18n.blacklisted": "Blacklisted",
"i18n.cancel": "Cancel",
"i18n.canceling": "Canceling…",
"i18n.close": "Close",
@@ -1305,6 +1347,8 @@
"i18n.pending": "Pending",
"i18n.previous": "Previous",
"i18n.processing": "Processing",
"i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> was successfully removed from the Blacklist.",
"i18n.removefromBlacklist": "Remove from Blacklist",
"i18n.request": "Request",
"i18n.request4k": "Request in 4K",
"i18n.requested": "Requested",

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,10 @@
"components.Discover.DiscoverMovieLanguage.languageMovies": "Фільми мовою \"{language}\"",
"components.Discover.DiscoverNetwork.networkSeries": "Серіали {network}",
"components.Discover.DiscoverStudio.studioMovies": "Фільми {studio}",
"components.Discover.DiscoverTvGenre.genreSeries": "Серіали в жанрі \"{genre}\"",
"components.Discover.DiscoverTvLanguage.languageSeries": "Серіали мовою \"{language}\"",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Список спостереження Plex",
"components.Discover.DiscoverTvGenre.genreSeries": "Серіали в жанрі {genre}",
"components.Discover.DiscoverTvLanguage.languageSeries": "Серіали мовою {language}",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Ваш список перегляду Plex",
"components.Discover.DiscoverWatchlist.watchlist": "Список перегляду Plex",
"components.Discover.MovieGenreList.moviegenres": "Фільми за жанрами",
"components.Discover.MovieGenreSlider.moviegenres": "Фільми за жанрами",
"components.Discover.NetworkSlider.networks": "Телеканали",
@@ -25,7 +25,7 @@
"components.Discover.discovertv": "Популярні серіали",
"components.Discover.emptywatchlist": "Тут з’являться медіафайли, додані до вашого <PlexWatchlistSupportLink>списку спостереження Plex</PlexWatchlistSupportLink>.",
"components.Discover.noRequests": "Жодних запитів.",
"components.Discover.plexwatchlist": "Ваш список спостереження Plex",
"components.Discover.plexwatchlist": "Ваш список перегляду Plex",
"components.Discover.popularmovies": "Популярні фільми",
"components.Discover.populartv": "Популярні серіали",
"components.Discover.recentlyAdded": "Нещодавно додані",
@@ -118,7 +118,7 @@
"components.Layout.Sidebar.settings": "Налаштування",
"components.Layout.Sidebar.users": "Користувачі",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Запити на фільми",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Запити на серіали",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Запити на сезони",
"components.Layout.UserDropdown.myprofile": "Профіль",
"components.Layout.UserDropdown.requests": "Запити",
"components.Layout.UserDropdown.settings": "Налаштування",
@@ -142,24 +142,24 @@
"components.ManageSlideOver.downloadstatus": "Завантаження",
"components.ManageSlideOver.manageModalAdvanced": "Просунутий",
"components.ManageSlideOver.manageModalClearMedia": "Очистити дані",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Це призведе до незворотного видалення всіх даних для цього {mediaType}а, включаючи будь-які запити. Якщо цей елемент існує у вашій бібліотеці {mediaServerName}, мультимедійна інформація про нього буде відтворена під час наступного сканування. ",
"components.ManageSlideOver.manageModalClearMediaWarning": "* Це призведе до незворотного видалення всіх даних для цього {mediaType}а, включаючи будь-які запити. Якщо цей елемент існує у вашій бібліотеці {mediaServerName}, мультимедійна інформація про нього буде відтворена під час наступного сканування.",
"components.ManageSlideOver.manageModalIssues": "Відкриті проблеми",
"components.ManageSlideOver.manageModalMedia": "Media",
"components.ManageSlideOver.manageModalMedia4k": "4K Media",
"components.ManageSlideOver.manageModalNoRequests": "Запитів немає.",
"components.ManageSlideOver.manageModalRequests": "Запити",
"components.ManageSlideOver.manageModalTitle": "Управління {mediaType}",
"components.ManageSlideOver.manageModalTitle": "Управління {mediaType}ом",
"components.ManageSlideOver.mark4kavailable": "Позначити як доступний у 4К",
"components.ManageSlideOver.markallseasons4kavailable": "Позначити всі сезони як доступні в 4K",
"components.ManageSlideOver.markallseasonsavailable": "Mark All Seasons as Available",
"components.ManageSlideOver.markallseasonsavailable": "Позначити всі сезони як доступні",
"components.ManageSlideOver.markavailable": "Позначити як доступний",
"components.ManageSlideOver.movie": "фільм",
"components.ManageSlideOver.openarr": "Відкрити в {arr}",
"components.ManageSlideOver.openarr4k": "Відкрити в 4К {arr}",
"components.ManageSlideOver.opentautulli": "Відкрити в Tautulli",
"components.ManageSlideOver.pastdays": "Останні {days, number} днів",
"components.ManageSlideOver.playedby": "Грає",
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {грає} other {грають}}",
"components.ManageSlideOver.playedby": "Переглядає",
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {перегляд} other {переглядів}}",
"components.ManageSlideOver.tvshow": "серіал",
"components.MediaSlider.ShowMoreCard.seemore": "Подивитися більше",
"components.MovieDetails.MovieCast.fullcast": "Повний акторський склад",
@@ -210,7 +210,7 @@
"components.NotificationTypeSelector.mediaapproved": "Схвалення медіа-запитів",
"components.NotificationTypeSelector.mediaapprovedDescription": "Надсилати повідомлення, коли медіа-запити схвалюються вручну.",
"components.NotificationTypeSelector.mediaautorequested": "Запит надіслано автоматично",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Отримуйте сповіщення, коли нові медіа-запити автоматично надсилаються для елементів у вашому списку спостереження Plex.",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Отримуйте сповіщення, коли нові медіа-запити автоматично надсилаються для елементів у вашому списку перегляду Plex.",
"components.NotificationTypeSelector.mediaavailable": "Доступні нові медіафайли",
"components.NotificationTypeSelector.mediaavailableDescription": "Надсилати повідомлення, коли запитані медіафайли стають доступними.",
"components.NotificationTypeSelector.mediadeclined": "Відхилення медіа-запитів",
@@ -245,13 +245,13 @@
"components.PermissionEdit.autoapproveMovies": "Автоматичне схвалення фільмів",
"components.PermissionEdit.autoapproveMoviesDescription": "Надати дозвіл на автоматичне схвалення всіх фільмів, відмінних від 4К.",
"components.PermissionEdit.autoapproveSeries": "Автоматичне схвалення серіалів",
"components.PermissionEdit.autoapproveSeriesDescription": "Надати дозвіл на автоматичне схвалення всіх серіалів, відмінних від 4К.",
"components.PermissionEdit.autoapproveSeriesDescription": "Надати дозвіл на автоматичне схвалення всіх серіалів, відмінних від 4K.",
"components.PermissionEdit.autorequest": "Автоматичний запит",
"components.PermissionEdit.autorequestDescription": "Надайте дозвіл на автоматичне надсилання запитів на медіафайли, відмінні від 4K, через Plex Watchlist.",
"components.PermissionEdit.autorequestDescription": "Надайте дозвіл на автоматичне надсилання запитів на медіафайли, відмінні від 4K, через список перегляду Plex.",
"components.PermissionEdit.autorequestMovies": "Автоматичний запит фільмів",
"components.PermissionEdit.autorequestMoviesDescription": "Надайте дозвіл на автоматичне надсилання запитів на фільми, відмінні від 4K, через Plex Watchlist.",
"components.PermissionEdit.autorequestMoviesDescription": "Надайте дозвіл на автоматичне надсилання запитів на фільми, відмінні від 4K, через список перегляду Plex.",
"components.PermissionEdit.autorequestSeries": "Автоматичний запит Серіалів",
"components.PermissionEdit.autorequestSeriesDescription": "Надайте дозвіл на автоматичне надсилання запитів на серіали, відмінні від 4K, через Plex Watchlist.",
"components.PermissionEdit.autorequestSeriesDescription": "Надайте дозвіл на автоматичне надсилання запитів на серіали, відмінні від 4K, через список перегляду Plex.",
"components.PermissionEdit.createissues": "Повідомлення про проблеми",
"components.PermissionEdit.createissuesDescription": "Надати дозвіл на повідомлення про проблеми з медіафайлами.",
"components.PermissionEdit.manageissues": "Управління проблемами",
@@ -266,7 +266,7 @@
"components.PermissionEdit.request4kTv": "Запити серіалів у 4К",
"components.PermissionEdit.request4kTvDescription": "Надати дозвіл на надсилання запитів серіалів у 4К.",
"components.PermissionEdit.requestDescription": "Надати дозвіл на надсилання запитів усіх медіафайлів, відмінних від 4К.",
"components.PermissionEdit.requestMovies": "Замовити фільми",
"components.PermissionEdit.requestMovies": "Запити фільмів",
"components.PermissionEdit.requestMoviesDescription": "Надати дозвіл на надсилання запитів усіх фільмів, відмінних від 4К.",
"components.PermissionEdit.requestTv": "Запити серіалів",
"components.PermissionEdit.requestTvDescription": "Надати дозвіл на надсилання запитів усіх серіалів, відмінних від 4К.",
@@ -280,8 +280,8 @@
"components.PermissionEdit.viewrecentDescription": "Надайте дозвіл на перегляд списку нещодавно доданих медіа.",
"components.PermissionEdit.viewrequests": "Перегляд запитів",
"components.PermissionEdit.viewrequestsDescription": "Надати дозвіл на перегляд медіа-запитів, надісланих іншими користувачами.",
"components.PermissionEdit.viewwatchlists": "Перегляньте списки спостереження Plex",
"components.PermissionEdit.viewwatchlistsDescription": "Надайте дозвіл на перегляд списків спостереження Plex інших користувачів.",
"components.PermissionEdit.viewwatchlists": "Перегляд списків переглядів Plex",
"components.PermissionEdit.viewwatchlistsDescription": "Надайте дозвіл на перегляд списків перегляду Plex інших користувачів.",
"components.PersonDetails.alsoknownas": "Також відомий(а) як: {names}",
"components.PersonDetails.appearsin": "Появи у фільмах та серіалах",
"components.PersonDetails.ascharacter": "в ролі {character}",
@@ -291,10 +291,10 @@
"components.PlexLoginButton.signingin": "Виконується вхід...",
"components.PlexLoginButton.signinwithplex": "Увійти",
"components.QuotaSelector.days": "{count, plural, one {день} other {днів}}",
"components.QuotaSelector.movieRequests": "{quotaLimit} <quotaUnits>{фільмів} за {quotaDays} {днів}</quotaUnits>",
"components.QuotaSelector.movieRequests": "{quotaLimit} <quotaUnits>{movies} на {quotaDays} {days}</quotaUnits>",
"components.QuotaSelector.movies": "{count, plural, one {фільм} other {фільми}}",
"components.QuotaSelector.seasons": "{count, plural, one {сезон} other {сезони}}",
"components.QuotaSelector.tvRequests": "{quotaLimit} <quotaUnits>{сезонів} за {quotaDays} {днів}</quotaUnits>",
"components.QuotaSelector.tvRequests": "{quotaLimit} <quotaUnits>{seasons} на {quotaDays} {days}</quotaUnits>",
"components.QuotaSelector.unlimited": "Необмежено",
"components.RegionSelector.regionDefault": "Всі регіони",
"components.RegionSelector.regionServerDefault": "За замовчуванням ({region})",
@@ -329,7 +329,7 @@
"components.RequestCard.deleterequest": "Видалити запит",
"components.RequestCard.editrequest": "Редагувати запит",
"components.RequestCard.failedretry": "Щось пішло не так при спробі повторити запит.",
"components.RequestCard.mediaerror": "Назва, пов'язана з цим запитом, більше недоступна.",
"components.RequestCard.mediaerror": "{mediaType} Не знайдено",
"components.RequestCard.seasons": "{seasonCount, plural, one {Сезон} other {Сезони}}",
"components.RequestCard.tmdbid": "TMDB ID",
"components.RequestCard.tvdbid": "TheTVDB ID",
@@ -338,7 +338,7 @@
"components.RequestList.RequestItem.deleterequest": "Видалити запит",
"components.RequestList.RequestItem.editrequest": "Редагувати запит",
"components.RequestList.RequestItem.failedretry": "Щось пішло не так при спробі повторити запит.",
"components.RequestList.RequestItem.mediaerror": "Назва, пов'язана з цим запитом, більше недоступна.",
"components.RequestList.RequestItem.mediaerror": "{mediaType} Не знайдено",
"components.RequestList.RequestItem.modified": "Змінено",
"components.RequestList.RequestItem.modifieduserdate": "{date} користувачем {user}",
"components.RequestList.RequestItem.requested": "Запрошений",
@@ -366,17 +366,17 @@
"components.RequestModal.QuotaDisplay.allowedRequests": "Вам дозволено запитувати <strong>{limit}</strong> {type} кожні <strong>{days}</strong> днів.",
"components.RequestModal.QuotaDisplay.allowedRequestsUser": "Цьому користувачеві дозволено запитувати <strong>{limit}</strong> {type} кожні <strong>{days}</strong> днів.",
"components.RequestModal.QuotaDisplay.movie": "фільм",
"components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {фільм} other {фільми}}",
"components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {фільм} other {фільмів}}",
"components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Залишилося недостатньо запитів на сезони",
"components.RequestModal.QuotaDisplay.quotaLink": "Ви можете переглянути зведення ваших обмежень на кількість запитів на <ProfileLink>сторінці вашого профілю</ProfileLink>.",
"components.RequestModal.QuotaDisplay.quotaLinkUser": "Ви можете переглянути зведення обмежень на кількість запитів цього користувача на <ProfileLink>сторінці його профілю</ProfileLink>.",
"components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {запитів {type} не залишилося} other {залишилось <strong>#</strong> запиту(ів) {type}}}",
"components.RequestModal.QuotaDisplay.requiredquota": "Вам необхідно мати принаймні <strong>{seasons}</strong> {seasons, plural, one {запит на сезони} other {запиту(ів) на сезони}} для того , щоб надіслати запит на цей серіал.",
"components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {Запитів на {type} не залишилося} other {Залишилось <strong>#</strong> запити(ів) на {type}}}",
"components.RequestModal.QuotaDisplay.requiredquota": "Вам необхідно мати принаймні <strong>{seasons}</strong> {seasons, plural, one {запит на сезони} other {запиту(ів) на сезони}} для того, щоб надіслати запит на цей серіал.",
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Цьому користувачеві необхідно мати принаймні <strong>{seasons}</strong> {seasons, plural, one {запит на сезони} other {запиту(ів) на сезони}} для того, щоб надіслати запит на цей серіал.",
"components.RequestModal.QuotaDisplay.season": "сезон",
"components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {сезон} other {сезони}}",
"components.RequestModal.SearchByNameModal.nomatches": "Нам не вдалося знайти відповідність для цієї серії.",
"components.RequestModal.SearchByNameModal.notvdbiddescription": "Ми не змогли автоматично виконати ваш запит. Будь ласка, виберіть правильний збіг зі списку нижче.",
"components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {сезон} other {сезонів}}",
"components.RequestModal.SearchByNameModal.nomatches": "Нам не вдалося знайти відповідність для цього серіалу.",
"components.RequestModal.SearchByNameModal.notvdbiddescription": "Нам не вдалося автоматично знайти цей серіал. Будь ласка, виберіть правильний збіг зі списку нижче.",
"components.RequestModal.alreadyrequested": "Вже запрошений",
"components.RequestModal.approve": "Схвалити запит",
"components.RequestModal.autoapproval": "Автоматичне схвалення",
@@ -385,9 +385,9 @@
"components.RequestModal.errorediting": "Щось пішло не так під час редагування запиту.",
"components.RequestModal.extras": "Додатково",
"components.RequestModal.numberofepisodes": "# епізодів",
"components.RequestModal.pending4krequest": "",
"components.RequestModal.pending4krequest": "Очікуючий запит в 4К",
"components.RequestModal.pendingapproval": "Ваш запит чекає схвалення.",
"components.RequestModal.pendingrequest": "",
"components.RequestModal.pendingrequest": "Очікуючий запит",
"components.RequestModal.requestApproved": "Запит на <strong>{title}</strong> схвалений!",
"components.RequestModal.requestCancel": "Запит на <strong>{title}</strong> скасовано.",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> успішно запрошений!",
@@ -404,7 +404,7 @@
"components.RequestModal.requestmovietitle": "Запит на фільм",
"components.RequestModal.requestseasons": "Запросити {seasonCount} {seasonCount, plural, one {сезон} other {сезону(ів)}}",
"components.RequestModal.requestseasons4k": "Запит {seasonCount} {seasonCount, plural, one {сезону} other {сезонів}} у 4К",
"components.RequestModal.requestseries4ktitle": "Надіслати запит на серіал у 4K",
"components.RequestModal.requestseries4ktitle": "Запросити серіал у 4K",
"components.RequestModal.requestseriestitle": "Запит на серіал",
"components.RequestModal.season": "Сезон",
"components.RequestModal.seasonnumber": "Сезон {number}",
@@ -452,7 +452,7 @@
"components.Settings.Notifications.NotificationsPushbullet.accessToken": "Токен доступу",
"components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Створіть токен у <PushbulletSettingsLink>налаштуваннях облікового запису</PushbulletSettingsLink>",
"components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "Активувати службу",
"components.Settings.Notifications.NotificationsPushbullet.channelTag": "Channel Tag",
"components.Settings.Notifications.NotificationsPushbullet.channelTag": "Тег каналу",
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Не вдалося зберегти налаштування сповіщень Pushbullet.",
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Налаштування сповіщень Pushbullet успішно збережено!",
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Не вдалося надіслати тестове повідомлення до Pushbullet.",
@@ -609,7 +609,7 @@
"components.Settings.RadarrModal.validationRootFolderRequired": "Ви повинні вибрати кореневий каталог",
"components.Settings.SettingsAbout.Releases.currentversion": "Поточна",
"components.Settings.SettingsAbout.Releases.latestversion": "Остання",
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Дані про дозвіл в даний час недоступні.",
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Дані про реліз наразі недоступні.",
"components.Settings.SettingsAbout.Releases.releases": "Релізи",
"components.Settings.SettingsAbout.Releases.versionChangelog": "Зміни у версії {version}",
"components.Settings.SettingsAbout.Releases.viewchangelog": "Переглянути список змін",
@@ -626,7 +626,7 @@
"components.Settings.SettingsAbout.preferredmethod": "Переважний спосіб",
"components.Settings.SettingsAbout.runningDevelop": "Ви використовуєте гілку <code>develop</code> проекту Jellyseerr, яка рекомендується тільки для тих, хто робить внесок у розробку або допомагає в тестуванні.",
"components.Settings.SettingsAbout.supportoverseerr": "Підтримати Jellyseerr",
"components.Settings.SettingsAbout.timezone": "Годинний пояс",
"components.Settings.SettingsAbout.timezone": "Часовий пояс",
"components.Settings.SettingsAbout.totalmedia": "Усього мультимедіа",
"components.Settings.SettingsAbout.totalrequests": "Усього запитів",
"components.Settings.SettingsAbout.uptodate": "Актуальна",
@@ -645,7 +645,7 @@
"components.Settings.SettingsJobsCache.download-sync": "Синхронізувати завантаження",
"components.Settings.SettingsJobsCache.download-sync-reset": "Скинути синхронізацію завантажень",
"components.Settings.SettingsJobsCache.editJobSchedule": "Змінити завдання",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Current Frequency",
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Поточна частота",
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Частота",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Кожен {jobScheduleHours, plural, one {година} other {{jobScheduleHours} години(ів)}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Кожну {jobScheduleMinutes, plural, one {хвилину} other {{jobScheduleMinutes} хвилин(и)}}",
@@ -837,7 +837,7 @@
"components.Settings.sonarrsettings": "Налаштування Sonarr",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Почати сканування",
"components.Settings.tautulliApiKey": "API Key",
"components.Settings.tautulliApiKey": "Ключ API",
"components.Settings.tautulliSettings": "Tautulli Налаштування",
"components.Settings.tautulliSettingsDescription": "За бажанням налаштуйте параметри для вашого сервера Tautulli. Jellyseerr отримує дані історії переглядів для медіафайлів Plex від Tautulli.",
"components.Settings.toastApiKeyFailure": "Щось пішло не так при створенні нового ключа API.",
@@ -966,8 +966,8 @@
"components.UserList.user": "Користувач",
"components.UserList.usercreatedfailed": "Щось пішло не так при створенні користувача.",
"components.UserList.usercreatedfailedexisting": "Вказана адреса електронної пошти вже використовується іншим користувачем.",
"components.UserList.usercreatedsuccess": "Користувач успішно створено!",
"components.UserList.userdeleted": "Користувач успішно видалено!",
"components.UserList.usercreatedsuccess": "Користувача успішно створено!",
"components.UserList.userdeleted": "Користувача успішно видалено!",
"components.UserList.userdeleteerror": "Щось пішло не так при видаленні користувача.",
"components.UserList.userfail": "Щось пішло не так при збереженні дозволів користувача.",
"components.UserList.userlist": "Список користувачів",
@@ -996,9 +996,9 @@
"components.UserProfile.UserSettings.UserGeneralSettings.owner": "Власник",
"components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Користувач Plex",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Автоматичний запит фільмів",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Автоматично надсилайте запит на перегляд фільмів у своєму <PlexWatchlistSupportLink>списку спостереження Plex</PlexWatchlistSupportLink>",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Автоматично запитувати фільми з вашого <PlexWatchlistSupportLink>списку перегляду Plex</PlexWatchlistSupportLink>",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Автоматичний запит Серіалів",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Автоматично надсилайте запит на серіал у своєму <PlexWatchlistSupportLink>списку спостереження Plex</PlexWatchlistSupportLink>",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Автоматично запитувати серіали з вашого <PlexWatchlistSupportLink>списку перегляду Plex</PlexWatchlistSupportLink>",
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Регіон для пошуку фільмів та серіалів",
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Контент фільтрується за доступністю у вибраному регіоні",
"components.UserProfile.UserSettings.UserGeneralSettings.role": "Роль",
@@ -1031,7 +1031,7 @@
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Надсилати без звуку",
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Надсилати повідомлення без звуку",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "ID чату",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Почніть чат</TelegramBotLink>, додайте <GetIdBotLink>@get_id_bot</GetIdBotLink> і виконайте команду <code>/my_id</code>",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Почніть чат</TelegramBotLink>, додайте <GetIdBotLink>@get_id_bot</GetIdBotLink> і виконайте команду <code>/my_id</code>",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Не вдалося зберегти налаштування сповіщень Telegram.",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Налаштування сповіщень Telegram успішно збережено!",
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Ви повинні надати дійсний ID користувача",
@@ -1067,15 +1067,15 @@
"components.UserProfile.UserSettings.menuNotifications": "Сповіщення",
"components.UserProfile.UserSettings.menuPermissions": "Дозволи",
"components.UserProfile.UserSettings.unauthorizedDescription": "У вас немає дозволу на зміну налаштувань цього користувача.",
"components.UserProfile.emptywatchlist": "Тут з’являться медіафайли, додані до вашого <PlexWatchlistSupportLink>списку спостереження Plex</PlexWatchlistSupportLink>.",
"components.UserProfile.emptywatchlist": "Тут з’являться медіафайли, додані до вашого <PlexWatchlistSupportLink>списку перегляду Plex</PlexWatchlistSupportLink>.",
"components.UserProfile.limit": "{remaining} з {limit}",
"components.UserProfile.movierequests": "Запитів фільмів",
"components.UserProfile.pastdays": "{type} (за {days} день(ів))",
"components.UserProfile.plexwatchlist": "Список спостереження Plex",
"components.UserProfile.pastdays": "{type} (на {days} дні(в)",
"components.UserProfile.plexwatchlist": "Список перегляду Plex",
"components.UserProfile.recentlywatched": "Нещодавно переглянуто",
"components.UserProfile.recentrequests": "Останні запити",
"components.UserProfile.requestsperdays": "залишилось {limit}",
"components.UserProfile.seriesrequest": "Запитів серіалів",
"components.UserProfile.seriesrequest": "Запитів сезонів",
"components.UserProfile.totalrequests": "Усього запитів",
"components.UserProfile.unlimited": "Необмежено",
"i18n.advanced": "Для просунутих користувачів",
@@ -1114,7 +1114,7 @@
"i18n.requested": "Запрошений",
"i18n.requesting": "Запит…",
"i18n.resolved": "Вирішені",
"i18n.restartRequired": "Restart Required",
"i18n.restartRequired": "Потрібне перезавантаження",
"i18n.resultsperpage": "Відобразити {pageSize} результатів на сторінці",
"i18n.retry": "Повторити",
"i18n.retrying": "Повтор…",
@@ -1140,144 +1140,211 @@
"Components.PermissionEdit.requestMovies": "Запити фільмів",
"Components.PermissionEdit.autoapprove4kMovies": "Автоматичне схвалення 4К фільмів",
"Components.PermissionEdit.autoapproveMovies": "Автоматичне схвалення фільмів",
"components.Discover.FilterSlideover.studio": "Студія",
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Нещодавно додані",
"components.Discover.FilterSlideover.keywords": "Ключові слова",
"components.Discover.FilterSlideover.ratingText": "Оцінки від {minValue} до {maxValue}",
"components.Discover.FilterSlideover.tmdbuserscore": "Оцінка користувачів TMDB",
"components.Discover.DiscoverTv.discovertv": "Серіали",
"components.Discover.FilterSlideover.runtime": "Тривалість",
"components.Discover.FilterSlideover.from": "Від",
"components.Discover.studios": "Студії",
"components.Discover.FilterSlideover.to": "До",
"components.Discover.FilterSlideover.filters": "Фільтри",
"components.Discover.CreateSlider.providetmdbsearch": "Введіть пошуковий запит",
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Фільми {keywordTitle}",
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Ваш список перегляду Plex",
"components.Discover.FilterSlideover.genres": "Жанри",
"components.Discover.FilterSlideover.originalLanguage": "Мова оригіналу",
"components.Discover.CreateSlider.nooptions": "Немає результатів.",
"components.Discover.FilterSlideover.tmdbuservotecount": "Кількість голосів користувачів TMDB",
"components.Discover.DiscoverMovies.discovermovies": "Фільми",
"components.Discover.FilterSlideover.clearfilters": "Очистити активні фільтри",
"components.Discover.CreateSlider.searchKeywords": "Ключові слова пошуку…",
"components.Discover.CreateSlider.searchStudios": "Пошук студій…",
"components.Discover.CreateSlider.starttyping": "Починайте писати для пошуку.",
"components.Discover.CreateSlider.validationTitlerequired": "Ви повинні вказати назву.",
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Активний фільтр} other {# Активні фільтри}}",
"components.Discover.DiscoverMovies.discovermovies": "Фільми",
"components.Discover.DiscoverSliderEdit.enable": "Змінити видимість",
"components.Discover.DiscoverSliderEdit.remove": "Видалити",
"components.Discover.DiscoverTv.discovertv": "Серіали",
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Активний фільтр} other {# Активні фільтри}}",
"components.Discover.FilterSlideover.filters": "Фільтри",
"components.Discover.FilterSlideover.from": "Від",
"components.Discover.FilterSlideover.genres": "Жанри",
"components.Discover.FilterSlideover.keywords": "Ключові слова",
"components.Discover.FilterSlideover.originalLanguage": "Оригінальна мова",
"components.Discover.FilterSlideover.ratingText": "Оцінки від {minValue} до {maxValue}",
"components.Discover.FilterSlideover.releaseDate": "Дата релізу",
"components.Discover.FilterSlideover.runtime": "Тривалість",
"components.Discover.FilterSlideover.studio": "Студія",
"components.Discover.FilterSlideover.tmdbuserscore": "Оцінка користувачів TMDB",
"components.Discover.FilterSlideover.tmdbuservotecount": "Кількість голосів від користувачів TMDB",
"components.Discover.FilterSlideover.to": "До",
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Медіа додано до вашого <PlexWatchlistSupportLink>списку перегляду Plex</PlexWatchlistSupportLink>.",
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Ваш список перегляду Plex",
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Нещодавно додані",
"components.Discover.studios": "Студії",
"components.Discover.tmdbmoviegenre": "Жанр фільму TMDB",
"components.Discover.tmdbmoviekeyword": "Ключове слово фільму TMDB",
"components.Discover.tmdbmoviestreamingservices": "Сервіси потокової передачі фільмів TMDB",
"components.Discover.tmdbnetwork": "Телеканал TMDB",
"components.Discover.tmdbstudio": "Студія TMDB",
"components.Discover.tmdbtvgenre": "Жанр серіалу TMDB",
"components.Discover.tmdbtvkeyword": "Ключове слово серіалу TMDB",
"components.Discover.tvgenres": "Жанри серіалів",
"components.Layout.UserWarnings.passwordRequired": "Потрібно вказати пароль.",
"components.Login.host": "{mediaServerName} URL",
"components.Login.initialsignin": "Підключитися",
"components.Login.initialsigningin": "Підключення…",
"components.Login.save": "Додати",
"components.Login.signinwithjellyfin": "Використовуйте свій {mediaServerName} обліковий запис",
"components.Login.title": "Додати email",
"components.Login.username": "Ім'я користувача",
"components.ManageSlideOver.removearr4k": "Видалити з 4K {arr}",
"components.MovieDetails.downloadstatus": "Статус завантаження",
"components.MovieDetails.imdbuserscore": "Оцінка користувачів IMDB",
"components.MovieDetails.openradarr4k": "Відкрити фільм у 4К Radarr",
"components.Selector.searchKeywords": "Ключові слова пошуку…",
"components.Selector.searchStudios": "Пошук студій…",
"components.Selector.starttyping": "Початок введення для пошуку.",
"components.Settings.Notifications.NotificationsPushover.sound": "Звук сповіщення",
"components.Settings.SettingsMain.general": "Загальні",
"components.Settings.SettingsMain.generalsettings": "Загальні налаштування",
"components.Settings.SettingsMain.applicationTitle": "Назва програми",
"components.Settings.SettingsMain.applicationurl": "URL програми",
"components.Settings.SettingsMain.cacheImages": "Увімкнути кешування зображень",
"components.Settings.SettingsMain.csrfProtection": "Увімкнути CSRF захист",
"components.Settings.SettingsMain.csrfProtectionHoverTip": "НЕ вмикайте цей параметр, якщо ви не розумієте, що робите!",
"components.Settings.SettingsMain.hideAvailable": "Приховати доступні медіа",
"components.Settings.SettingsMain.locale": "Мова програми",
"components.Settings.SettingsMain.originallanguage": "Мови для пошуку фільмів та серіалів",
"components.Settings.SettingsMain.partialRequestsEnabled": "Дозволити запитувати серіали частково",
"components.Settings.SettingsMain.region": "Регіон для пошуку фільмів та серіалів",
"components.Settings.SettingsMain.regionTip": "Фільтрувати вміст за регіональною доступністю",
"components.Settings.SettingsMain.toastApiKeySuccess": "Новий ключ API успішно згенеровано!",
"components.Settings.SettingsMain.toastSettingsFailure": "Під час збереження налаштувань сталася помилка.",
"components.Settings.SettingsMain.toastSettingsSuccess": "Налаштування успішно збережено!",
"components.Settings.SettingsMain.trustProxyTip": "Дозволити Jellyseerr правильно реєструвати IP-адреси клієнтів за проксі-сервером",
"components.Settings.SettingsMain.validationApplicationUrl": "Ви повинні надати дійсну URL-адресу",
"components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL-адреса не має закінчуватися косою рискою",
"components.Settings.SonarrModal.animeSeriesType": "Тип аніме",
"components.Settings.jellyfinSettings": "Налаштування {mediaServerName}",
"components.Settings.jellyfinSettingsSuccess": "Налаштування {mediaServerName} успішно збережено!",
"components.Settings.jellyfinlibraries": "Бібліотеки {mediaServerName}",
"components.Settings.jellyfinsettings": "Налаштування {mediaServerName}",
"components.Setup.signinWithPlex": "Використовуйте свій обліковий запис Plex",
"components.TvDetails.play": "Відтворити в {mediaServerName}",
"components.UserList.mediaServerUser": "Користувач {mediaServerName}",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Електронна пошта",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Збереження…",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Звук сповіщення",
"i18n.collection": "Колекція",
"components.Discover.CreateSlider.needresults": "Ви повинні мати принаймні 1 результат.",
"components.Discover.CreateSlider.searchGenres": "Пошук жанрів…",
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Фільми {keywordTitle}",
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Активний фільтр} other {# Активні фільтри}}",
"components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Серіали",
"components.Discover.FilterSlideover.clearfilters": "Очистити всі активні фільтри",
"components.Discover.FilterSlideover.runtimeText": "тривалість {minValue}-{maxValue} хвилин",
"components.Discover.FilterSlideover.voteCount": "Кількість голосів від {minValue} до {maxValue}",
"components.Discover.DiscoverSliderEdit.remove": "Видалити",
"components.Layout.Sidebar.browsemovies": "Фільми",
"components.MovieDetails.imdbuserscore": "Оцінка користувачів IMDB",
"components.Layout.Sidebar.browsetv": "Серіали",
"components.Discover.DiscoverTv.sortPopularityDesc": "Популярність за спаданням",
"components.Discover.moviegenres": "Жанри фільмів",
"components.Discover.resetwarning": "Скинути всі повзунки до стандартних. Це також видалить будь-які спеціальні повзунки!",
"components.Discover.stopediting": "Зупинити редагування",
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Активний фільтр} other {# Активні фільтри}}",
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Активний фільтр} other {# Активні фільтри}}",
"components.Discover.FilterSlideover.streamingservices": "Сервіси потокового передавання",
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Активний фільтр} other {# Активні фільтри}}",
"components.Discover.CreateSlider.addSlider": "Додати повзунок",
"components.Discover.tvgenres": "Жанри серіалів",
"components.Discover.tmdbmoviekeyword": "Ключове слово фільму TMDB",
"components.Discover.tmdbtvkeyword": "Ключове слово серіала TMDB",
"components.Discover.tmdbnetwork": "Телеканал TMDB",
"components.Discover.networks": "Телеканали",
"components.Discover.tmdbtvgenre": "Жанр серіала TMDB",
"components.Discover.tmdbstudio": "Студія TMDB",
"components.Discover.tmdbtvstreamingservices": "Сервіси потокового передавання серіалів TMDB",
"components.Discover.tmdbmoviestreamingservices": "Сервіси потокової передачі фільмів TMDB",
"components.Discover.resetfailed": "Щось пішло не так під час скидання налаштувань Discover.",
"components.Discover.tmdbsearch": "Пошук TMDB",
"components.Discover.CreateSlider.searchKeywords": "Ключові слова пошуку…",
"components.Discover.tmdbmoviegenre": "Жанр фільму TMDB",
"components.Discover.updatesuccess": "Оновлено параметри налаштування Discover.",
"components.Discover.resetsuccess": "Успішно скинуто параметри налаштування.",
"components.Discover.updatefailed": "Під час оновлення налаштувань Discover сталася помилка.",
"components.Selector.showmore": "Показати більше",
"components.Selector.searchGenres": "Виберіть жанри…",
"components.Selector.searchStudios": "Пошук студій…",
"components.Discover.CreateSlider.addcustomslider": "Створити власний повзунок",
"components.Selector.showless": "Показати менше",
"components.Selector.starttyping": "Початок введення для пошуку.",
"components.Selector.searchKeywords": "Пошук за ключовими словами…",
"components.Selector.nooptions": "Немає результатів.",
"components.Discover.resettodefault": "Скинути за замовчуванням",
"components.Settings.SettingsJobsCache.availability-sync": "Синхронізація доступності медіа",
"components.Discover.tmdbsearch": "Пошук TMDB",
"components.Discover.tmdbtvstreamingservices": "Сервіси потокового передавання серіалів TMDB",
"components.Layout.Sidebar.browsemovies": "Фільми",
"components.Layout.Sidebar.browsetv": "Серіали",
"components.Layout.UserWarnings.emailInvalid": "Адреса електронної пошти недійсна.",
"components.Login.saving": "Додавання…",
"components.Login.validationhostformat": "Потрібна дійсна URL-адреса",
"components.Login.validationusernamerequired": "Потрібно ім'я користувача",
"components.ManageSlideOver.removearr": "Видалити з {arr}",
"components.MovieDetails.openradarr": "Відкрити фільм у Radarr",
"components.Selector.nooptions": "Немає результатів.",
"components.Selector.searchGenres": "Виберіть жанри…",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Пристрій за замовчуванням",
"components.Settings.Notifications.userEmailRequired": "Потрібен email користувача",
"components.Settings.RadarrModal.tagRequestsInfo": "Автоматично додавати додатковий тег з ID та іменем користувача, який запитує",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Кожну {jobScheduleSeconds, plural, one {секунду} other {{jobScheduleSeconds} секунд}}",
"components.Settings.RadarrModal.tagRequests": "Теги запитів",
"components.Settings.SettingsMain.hideAvailable": "Приховати доступні медіа",
"components.Settings.SettingsMain.regionTip": "Фільтрувати вміст за регіональною доступністю",
"components.Settings.SettingsMain.region": "Регіон для пошуку фільмів та серіалів",
"components.Settings.SettingsMain.trustProxy": "Увімкнути підтримку проксі",
"components.Settings.SettingsMain.toastSettingsSuccess": "Налаштування успішно збережено!",
"components.Settings.SettingsMain.locale": "Мова інтерфейсу",
"components.Settings.SettingsMain.applicationTitle": "Назва програми",
"components.Settings.SettingsMain.originallanguage": "Мови для пошуку фільмів та серіалів",
"components.Settings.SettingsMain.csrfProtection": "Увімкнути захист CSRF",
"components.Settings.SettingsMain.toastApiKeyFailure": "Під час створення нового ключа API сталася помилка.",
"components.Settings.SettingsMain.originallanguageTip": "Фільтрувати вміст за мовою оригіналу",
"components.Settings.SettingsMain.cacheImagesTip": "Кешувати зображення із зовнішніх джерел (потрібний значний об'єм дискового простору)",
"components.Settings.SettingsMain.trustProxyTip": "Дозволити Overseerr правильно реєструвати IP-адреси клієнтів за проксі-сервером",
"components.Settings.SettingsMain.generalsettings": "Загальні налаштування",
"components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL-адреса не має закінчуватися скісною рискою",
"components.Settings.SettingsMain.apikey": "Ключ API",
"components.Settings.SettingsMain.generalsettingsDescription": "Налаштуйте глобальні параметри і параметри за замовчуванням для Overseerr.",
"components.Settings.SettingsMain.toastApiKeySuccess": "Новий ключ API успішно згенеровано!",
"components.Settings.SettingsMain.cacheImages": "Увімкнути кешування зображень",
"components.Settings.SettingsMain.applicationurl": "URL програми",
"components.Settings.SettingsMain.general": "Загальні",
"components.Settings.SettingsMain.csrfProtectionHoverTip": "НЕ вмикайте цей параметр, якщо ви не розумієте, що робите!",
"components.Settings.SettingsMain.partialRequestsEnabled": "Дозволити запитувати серіали частково",
"components.Settings.SettingsMain.toastSettingsFailure": "Під час збереження налаштувань сталася помилка.",
"components.Settings.SettingsMain.validationApplicationUrl": "Ви повинні вказати дійсну URL-адресу",
"components.Settings.SettingsMain.validationApplicationTitle": "Ви повинні вказати назву програми",
"components.Settings.SettingsAbout.supportjellyseerr": "Підтримайте Jellyseerr",
"components.Settings.SettingsMain.csrfProtectionTip": "Встановіть доступ до зовнішнього API лише для читання (потрібний HTTPS)",
"components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Серіали",
"components.Discover.CreateSlider.editsuccess": "Відредаговано повзунок і збережено налаштування Discover.",
"components.Discover.CreateSlider.slidernameplaceholder": "Назва повзунка",
"components.Settings.internalUrl": "Внутрішня URL-адреса",
"components.Settings.SettingsJobsCache.availability-sync": "Синхронізація доступності медіа",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Кожну {jobScheduleSeconds, plural, one {секунду} other {{jobScheduleSeconds} секунд}}",
"components.Settings.SettingsMain.apikey": "Ключ API",
"components.Settings.SettingsMain.toastApiKeyFailure": "Під час створення нового ключа API сталася помилка.",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Пристрій за замовчуванням",
"components.UserList.noJellyfinuserstoimport": "Немає користувачів {mediaServerName} для імпорту.",
"components.UserList.importfromJellyfinerror": "Під час імпорту користувачів з {mediaServerName} сталася помилка.",
"components.Settings.SettingsMain.cacheImagesTip": "Кешувати зображення із зовнішніх джерел (потрібний значний об'єм дискового простору)",
"components.Settings.SettingsMain.validationApplicationTitle": "Ви повинні вказати назву програми",
"components.Settings.SonarrModal.seriesType": "Тип серіалу",
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Користувач {mediaServerName}",
"components.TitleCard.watchlistError": "Щось пішло не так, повторіть спробу.",
"components.Settings.SettingsMain.generalsettingsDescription": "Налаштуйте глобальні параметри та параметри за замовчуванням для Jellyseerr.",
"components.Settings.SettingsMain.originallanguageTip": "Фільтрувати вміст за мовою оригіналу",
"components.Settings.SettingsMain.trustProxy": "Увімкнути підтримку проксі",
"components.Settings.jellyfinSettingsFailure": "Під час збереження налаштувань {mediaServerName} сталася помилка.",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Зберегти зміни",
"components.TvDetails.play4k": "Відтворити 4K в {mediaServerName}",
"components.Discover.CreateSlider.providetmdbsearch": "Введіть пошуковий запит",
"components.Discover.FilterSlideover.streamingservices": "Сервіси потокового передавання",
"components.Discover.moviegenres": "Жанри фільмів",
"components.MovieDetails.play": "Відтворити в {mediaServerName}",
"components.MovieDetails.play4k": "Відтворити в {mediaServerName} у 4К",
"components.Selector.showless": "Згорнути",
"components.Discover.CreateSlider.addSlider": "Додати повзунок",
"components.Discover.CreateSlider.addcustomslider": "Створити власний повзунок",
"components.Discover.CreateSlider.addfail": "Не вдалося створити новий повзунок.",
"components.Discover.CreateSlider.needresults": "Ви повинні мати принаймні 1 результат.",
"components.Discover.DiscoverSliderEdit.deletefail": "Не вдалося видалити повзунок.",
"components.Discover.DiscoverSliderEdit.deletesuccess": "Повзунок успішно видалено.",
"components.Discover.CreateSlider.addsuccess": "Створено новий повзунок і збережено параметри налаштування Discover.",
"components.Discover.CreateSlider.editSlider": "Редагувати повзунок",
"components.Discover.CreateSlider.editfail": "Не вдалося відредагувати повзунок.",
"components.Discover.CreateSlider.addsuccess": "Створено новий повзунок і збережено параметри налаштування Discover.",
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Медіа додано до вашого <PlexWatchlistSupportLink>списку перегляду Plex</PlexWatchlistSupportLink>.",
"components.Settings.Notifications.NotificationsPushover.sound": "Звук сповіщення",
"components.Discover.customizediscover": "Налаштувати Discover",
"components.Discover.createnewslider": "Створити новий повзунок",
"components.Discover.FilterSlideover.firstAirDate": "Дата виходу в ефір",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Пристрій за замовчуванням",
"components.Settings.SonarrModal.tagRequests": "Теги запитів",
"components.Settings.SonarrModal.tagRequestsInfo": "Автоматично додавати додатковий тег з ID та іменем користувача, який запитує",
"components.Settings.SonarrModal.animeSeriesType": "Тип аніме-серіалу",
"components.Settings.SonarrModal.seriesType": "Тип серіалу",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Пристрій за замовчуванням",
"i18n.collection": "Колекція",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Звук сповіщення",
"components.Discover.DiscoverMovies.sortPopularityDesc": "Популярність за спаданням",
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Рейтинг TMDB за зростанням",
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Дата випуску за спаданням",
"components.Discover.CreateSlider.providetmdbnetwork": "Введіть TMDB ID мережі",
"components.Discover.DiscoverMovies.sortPopularityAsc": "Популярність за зростанням",
"components.Discover.CreateSlider.validationDatarequired": "Ви повинні надати доступний для пошуку вміст.",
"components.Discover.CreateSlider.providetmdbstudio": "Введіть TMDB ID студії",
"components.Discover.DiscoverMovies.sortTitleDesc": "Назва (Я-А) за спаданням",
"components.Discover.CreateSlider.starttyping": "Початок введення для пошуку.",
"components.Discover.CreateSlider.providetmdbkeywordid": "Введіть TMDB ID ключового слова",
"components.Discover.CreateSlider.validationTitlerequired": "Ви повинні вказати назву.",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Дата випуску за зростанням",
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Рейтинг TMDB за спаданням",
"components.Discover.DiscoverMovies.sortTitleAsc": "Назва (А-Я) за зростанням",
"components.Discover.CreateSlider.editsuccess": "Відредаговано повзунок і збережено параметри налаштування Discover.",
"components.Discover.CreateSlider.providetmdbgenreid": "Введіть TMDB ID жанру",
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Рейтинг TMDB за зростанням",
"components.Discover.CreateSlider.providetmdbkeywordid": "Введіть TMDB ID ключового слова",
"components.Discover.CreateSlider.providetmdbnetwork": "Введіть TMDB ID мережі",
"components.Discover.CreateSlider.providetmdbstudio": "Введіть TMDB ID студії",
"components.Discover.CreateSlider.slidernameplaceholder": "Назва повзунка",
"components.Discover.CreateSlider.validationDatarequired": "Ви повинні надати доступний для пошуку вміст.",
"components.Discover.DiscoverMovies.sortPopularityAsc": "Популярність за зростанням",
"components.Discover.DiscoverMovies.sortPopularityDesc": "Популярність за спаданням",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Дата випуску за зростанням",
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Дата випуску за спаданням",
"components.Discover.DiscoverMovies.sortTitleAsc": "Назва (А-Я) за зростанням",
"components.Discover.DiscoverMovies.sortTitleDesc": "Назва (Я-А) за спаданням",
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Рейтинг TMDB за зростанням",
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Рейтинг TMDB за спаданням",
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Дата виходу в ефір за зростанням",
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "Рейтинг TMDB за спаданням",
"components.Discover.DiscoverTv.sortPopularityAsc": "Популярність за зростанням",
"components.Discover.DiscoverTv.sortTitleAsc": "Назва (А-Я) за зростанням",
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Дата виходу в ефір за спаданням",
"components.Discover.DiscoverSliderEdit.deletefail": "Не вдалося видалити повзунок.",
"components.Discover.DiscoverSliderEdit.enable": "Перемкнути видимість",
"components.Discover.DiscoverSliderEdit.deletesuccess": "Повзунок успішно видалено.",
"components.Discover.DiscoverTv.sortTitleDesc": "Назва (Я-А) за спаданням"
"components.Discover.DiscoverTv.sortPopularityAsc": "Популярність за зростанням",
"components.Discover.DiscoverTv.sortPopularityDesc": "Популярність за спаданням",
"components.Discover.DiscoverTv.sortTitleAsc": "Назва (А-Я) за зростанням",
"components.Discover.DiscoverTv.sortTitleDesc": "Назва (Я-А) за спаданням",
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Рейтинг TMDB за зростанням",
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "Рейтинг TMDB за спаданням",
"components.Discover.FilterSlideover.firstAirDate": "Дата виходу в ефір",
"components.Discover.createnewslider": "Створити новий повзунок",
"components.Discover.customizediscover": "Налаштувати Discover",
"components.Discover.resetfailed": "Щось пішло не так під час скидання налаштувань Discover.",
"components.Discover.resetsuccess": "Успішно скинуто параметри налаштування.",
"components.Discover.resetwarning": "Скинути всі повзунки до стандартних. Це також видалить будь-які спеціальні повзунки!",
"components.Discover.stopediting": "Зупинити редагування",
"components.Discover.updatefailed": "Під час оновлення налаштувань Discover сталася помилка.",
"components.Discover.updatesuccess": "Оновлено параметри налаштування Discover.",
"components.Login.credentialerror": "Ім'я користувача або пароль неправильні.",
"components.Login.description": "Оскільки ви вперше входите в {applicationName}, вам потрібно додати дійсну адресу електронної пошти.",
"components.Login.validationEmailFormat": "Невірний email",
"components.Login.validationEmailRequired": "Ви повинні вказати адресу електронної пошти",
"components.Login.validationemailformat": "Потрібен дійсний email",
"components.Layout.UserWarnings.emailRequired": "Потрібно вказати адресу електронної пошти.",
"components.Login.emailtooltip": "Адресу не потрібно пов’язувати з вашим {mediaServerName} сервером.",
"components.Login.validationhostrequired": "Необхідна URL-адреса {mediaServerName}",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* Це безповоротно видалить цей {mediaType} з {arr}, включаючи всі файли.",
"components.Selector.showmore": "Показати більше",
"components.Settings.RadarrModal.tagRequests": "Теги запитів",
"components.Settings.SonarrModal.tagRequests": "Теги запитів",
"components.Setup.configuremediaserver": "Налаштуйте медіасервер",
"components.Settings.jellyfinsettingsDescription": "Налаштуйте свій {mediaServerName} сервер. {mediaServerName} відсканує бібліотеки, щоб побачити, які бібліотеки доступні.",
"components.Settings.manualscanJellyfin": "Сканувати бібліотеки вручну",
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
"components.Settings.jellyfinlibrariesDescription": "Бібліотеки {mediaServerName} перевіряються на наявність заголовків. Натисніть нижче, якщо в списку не вистачає бібліотек.",
"components.Settings.saving": "Збереження…",
"components.Settings.syncJellyfin": "Синхронізувати бібліотеки",
"components.Settings.syncing": "Синхронізація",
"components.Setup.signin": "Увійти",
"components.Setup.signinWithJellyfin": "Використовуйте свій {mediaServerName} обліковий запис",
"components.TitleCard.addToWatchList": "Додати в список перегляду",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> успішно додано до списку перегляду!",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} успішно імпортовано!",
"components.UserList.importfromJellyfin": "Додати користувачів з {mediaServerName}",
"components.Settings.manualscanDescriptionJellyfin": "Зазвичай це запускається лише раз на 24 години. Jellyseerr перевірятиме нещодавно доданий сервер {mediaServerName} більш агресивно. Якщо ви вперше налаштовуєте Jellyseerr, рекомендується одноразове повне сканування бібліотеки вручну!",
"components.Settings.save": "Зберегти зміни",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> Успішно видалено зі списку перегляду!",
"components.UserList.newJellyfinsigninenabled": "Параметр <strong>Увімкнути новий вхід на {mediaServerName}</strong> наразі ввімкнено. Користувачам {mediaServerName} із доступом до бібліотеки не потрібно імпортувати, щоб увійти.",
"components.UserProfile.localWatchlist": "Список перегляду {username}",
"components.Settings.jellyfinSettingsDescription": "Додатково налаштуйте внутрішні та зовнішні кінцеві точки для вашого сервера {mediaServerName}. У більшості випадків зовнішня URL-адреса відрізняється від внутрішньої URL-адреси. Для входу в систему {mediaServerName} також можна встановити спеціальну URL-адресу скидання пароля, якщо ви хочете переспрямувати на іншу сторінку скидання пароля.",
"components.Settings.SonarrModal.tagRequestsInfo": "Автоматично додавати додатковий тег з ID та іменем користувача, який запитує"
}

View File

@@ -397,7 +397,7 @@
"components.UserProfile.UserSettings.UserPasswordChange.password": "密码设置",
"components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "你无权设置此用户的密码。",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "你的帐户目前没有设置密码。在下方配置密码,使你能够作为“本地用户”登录。",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "此用户帐户目前没有设置密码。在下方配置密码,使该帐户能够作为“本地用户”登录。",
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "此用户帐户目前没有设置密码。配置下面的密码以使此帐户能够作为“本地用户”登录。",
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "新密码",
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "当前的密码",
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "确认密码",
@@ -766,14 +766,14 @@
"components.RequestButton.viewrequest": "查看请求",
"components.RequestButton.requestmore4k": "再提交 4K 请求",
"components.RequestButton.requestmore": "提交更多季数的请求",
"components.RequestButton.declinerequests": "拒绝{requestCount, plural, one {请求} other {{requestCount} 个请求}}",
"components.RequestButton.declinerequests": "拒绝{requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.declinerequest4k": "拒绝 4K 请求",
"components.RequestButton.declinerequest": "拒绝请求",
"components.RequestButton.decline4krequests": "拒绝{requestCount, plural, one { 4K 请求} other { {requestCount} 4K 请求}}",
"components.RequestButton.approverequests": "批准{requestCount, plural, one {请求} other {{requestCount} 个请求}}",
"components.RequestButton.decline4krequests": "拒绝 {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
"components.RequestButton.approverequests": "批准 {requestCount, plural, one {Request} other {{requestCount} Requests}}",
"components.RequestButton.approverequest4k": "批准 4K 请求",
"components.RequestButton.approverequest": "批准请求",
"components.RequestButton.approve4krequests": "批准{requestCount, plural, one { 4K 请求} other { {requestCount} 4K 请求}}",
"components.RequestButton.approve4krequests": "批准 {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}",
"components.RequestBlock.server": "目標服务器",
"components.RequestBlock.seasons": "季数",
"components.RequestBlock.rootfolder": "根目录",
@@ -782,10 +782,10 @@
"components.RegionSelector.regionServerDefault": "默认设置({region}",
"components.RegionSelector.regionDefault": "所有地区",
"components.QuotaSelector.unlimited": "无限",
"components.QuotaSelector.tvRequests": "<quotaUnits>每 {quotaDays} {days} </quotaUnits>{quotaLimit}<quotaUnits> {seasons}</quotaUnits>",
"components.QuotaSelector.tvRequests": "{quotaLimit} <quotaUnits>{seasons} 每 {quotaDays} {days}</quotaUnits>",
"components.QuotaSelector.seasons": "季",
"components.QuotaSelector.movies": "部电影",
"components.QuotaSelector.movieRequests": "<quotaUnits>每 {quotaDays} {days} </quotaUnits>{quotaLimit}<quotaUnits> {movies}</quotaUnits>",
"components.QuotaSelector.movieRequests": "{quotaLimit} <quotaUnits>{movies} 每 {quotaDays} {days}</quotaUnits>",
"components.QuotaSelector.days": "天",
"components.PlexLoginButton.signinwithplex": "登入",
"components.PlexLoginButton.signingin": "登入中…",
@@ -900,7 +900,7 @@
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Gotify 测试通知发送失败。",
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Gotify测试通知发送中…",
"components.Settings.Notifications.enableMentions": "允许提及",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "每 {jobScheduleMinutes} 分钟",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "每 {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "应用 API 令牌",
"components.UserList.newplexsigninenabled": "<strong>允许新的 Plex 用户登录</strong> 设置目前已启用。还没有导入的Plex用户也能登录。",
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "Pushover 通知设置保存失败。",
@@ -909,7 +909,7 @@
"components.Settings.RadarrModal.inCinemas": "已上映",
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "从您的<PushbulletSettingsLink>账号设置</PushbulletSettingsLink>获取API令牌",
"components.Settings.SettingsAbout.appDataPath": "数据目录",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "每 {jobScheduleHours} 小时",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "每 {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
"components.Settings.tautulliSettings": "Tautulli 设置",
"components.Settings.tautulliSettingsDescription": "关于 Tautulli 服务器的设置。Jellyseerr 会从 Tautulli 获取 Plex 媒体的观看历史记录。",
"components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord 用户ID",
@@ -1063,8 +1063,8 @@
"components.Settings.restartrequiredTooltip": "必须重新启动 Jellyseerr 才能使更改的设置生效",
"components.TvDetails.manageseries": "管理电视节目",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "自动请求您的 <PlexWatchlistSupportLink>Plex 关注列表</PlexWatchlistSupportLink>的媒体",
"components.AirDateBadge.airedrelative": "播出{relativeTime}",
"components.AirDateBadge.airsrelative": "播出{relativeTime}",
"components.AirDateBadge.airedrelative": "{relativeTime}播出",
"components.AirDateBadge.airsrelative": "{relativeTime}播出",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "电影请求",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "电视节目请求",
"components.NotificationTypeSelector.mediaautorequestedDescription": "当 Plex 关注列表中的项目自动提交新媒体请求时,会收到通知。",
@@ -1203,7 +1203,7 @@
"components.Discover.PlexWatchlistSlider.emptywatchlist": "您的 <PlexWatchlistSupportLink>Plex 关注列表</PlexWatchlistSupportLink>中的媒体会显示在这里。",
"components.Selector.starttyping": "开始打字以进行搜索。",
"components.Discover.CreateSlider.starttyping": "开始打字以进行搜索。",
"components.Discover.CreateSlider.needresults": "需要至少有 1 个结果。",
"components.Discover.CreateSlider.needresults": "需要至少有 1 个结果。",
"components.Selector.showless": "显示更少",
"components.Discover.resetfailed": "重置探索媒体设置时出了点问题。",
"components.Settings.SettingsMain.validationApplicationTitle": "你必须提供一个应用程序标题",
@@ -1253,7 +1253,7 @@
"components.Settings.SettingsJobsCache.availability-sync": "同步媒体可用性",
"components.Discover.tmdbmoviestreamingservices": "TMDB 电影流媒体服务",
"components.Discover.tmdbtvstreamingservices": "TMDB 电视流媒体服务",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "每 {jobScheduleSeconds} 秒",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "每 {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
"components.Discover.FilterSlideover.voteCount": "在 {minValue} 和 {maxValue} 之间的评分数",
"components.Settings.RadarrModal.tagRequests": "标签请求",
"components.Settings.RadarrModal.tagRequestsInfo": "自动添加带有请求者的用户 ID 和显示名称的附加标签",
@@ -1261,11 +1261,83 @@
"i18n.collection": "合集",
"components.Discover.FilterSlideover.tmdbuservotecount": "TMDB 用户评分数",
"components.Settings.SonarrModal.tagRequestsInfo": "自动添加带有请求者的用户 ID 和显示名称的附加标签",
"components.MovieDetails.imdbuserscore": "IMDB 用户评分",
"components.Settings.Notifications.NotificationsPushover.sound": "通知提示音",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "默认设备",
"components.Settings.SonarrModal.animeSeriesType": "动漫剧集类型",
"components.Settings.SonarrModal.seriesType": "剧集类型",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "通知提示音",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "默认设备"
"components.Layout.UserWarnings.passwordRequired": "需要输入密码。",
"components.Login.emailtooltip": "地址不需要与{mediaServerName}实例相关联。",
"components.Login.host": "{mediaServerName} 的 URL",
"components.Login.initialsignin": "连接",
"components.Login.initialsigningin": "连接中……",
"components.Login.save": "添加",
"components.Login.saving": "添加中……",
"components.Login.signinwithjellyfin": "使用您的{mediaServerName}帐户",
"components.Login.title": "添加邮件",
"components.Login.username": "用户名",
"components.Login.validationEmailFormat": "无效的邮件地址",
"components.Login.validationEmailRequired": "你必须提供一个电子邮件",
"components.Login.validationemailformat": "需要有效的电子邮件",
"components.Login.validationhostformat": "需要有效的URL",
"components.Login.validationusernamerequired": "需要用户名",
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* 这将不可逆地从{arr}中删除{mediaType},包括所有文件。",
"components.ManageSlideOver.removearr4k": "移除4K {arr}",
"components.MovieDetails.downloadstatus": "下载状态",
"components.MovieDetails.imdbuserscore": "IMDB用户评分",
"components.MovieDetails.openradarr": "在Radarr中打开电影",
"components.MovieDetails.play": "播放{mediaServerName}",
"components.MovieDetails.play4k": "播放 4K {mediaServerName}",
"components.Settings.Notifications.NotificationsPushover.sound": "通知声音",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin全库扫描",
"components.Settings.SonarrModal.seriesType": "系列类型",
"components.Settings.jellyfinSettingsFailure": "保存{mediaServerName}设置时出错。",
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName}设置保存成功!",
"components.Settings.jellyfinlibraries": "{mediaServerName}库",
"components.Settings.jellyfinlibrariesDescription": "库{mediaServerName}用于扫描标题。如果没有列出库,请单击下面的按钮。",
"components.Settings.jellyfinsettings": "{mediaServerName}设置",
"components.Settings.manualscanJellyfin": "手动扫描库",
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
"components.Settings.saving": "保存中……",
"components.Settings.syncJellyfin": "同步库",
"components.Settings.syncing": "同步中",
"components.Settings.timeout": "超时",
"components.Setup.signin": "登录",
"components.Setup.signinWithJellyfin": "使用您的{mediaServerName}帐户",
"components.TitleCard.addToWatchList": "添加到监视列表",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong>从监视列表中删除成功!",
"components.TitleCard.watchlistError": "出了问题,再试一次。",
"components.TvDetails.play": "在 {mediaServerName} 播放",
"components.TvDetails.play4k": "mediaServerName} 播放 4K",
"components.UserList.importfrommediaserver": "导入{mediaServerName}用户",
"components.UserList.mediaServerUser": "{mediaServerName} 用户",
"components.UserList.noJellyfinuserstoimport": "在{mediaServerName}中没有用户要导入。",
"components.UserList.newJellyfinsigninenabled": "<strong>启用 {mediaServerName} 登录</strong> 设置当前已启用. {mediaServerName} 具有库访问权限的用户不需要导入即可登录。",
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "{mediaServerName} 用户",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "设备默认",
"components.Layout.UserWarnings.emailInvalid": "邮件地址无效。",
"components.Layout.UserWarnings.emailRequired": "需要填写电子邮件地址。",
"components.Login.credentialerror": "用户名或密码错误。",
"components.Login.description": "由于这是您第一次登录{applicationName},您需要添加一个有效的电子邮件地址。",
"components.Login.validationhostrequired": "{mediaServerName} URL是必需的",
"components.ManageSlideOver.removearr": "从{arr}中删除",
"components.MovieDetails.openradarr4k": "在 4K Radarr 中打开电影",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "设备默认",
"components.Settings.Notifications.userEmailRequired": "获取用户邮箱",
"components.Settings.SettingsAbout.supportjellyseerr": "支持Jellyseerr",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Jellyfin最近新增扫描",
"components.Settings.SonarrModal.animeSeriesType": "动漫系列",
"components.Settings.internalUrl": "内部URL",
"components.Settings.jellyfinSettings": "{mediaServerName}设置",
"components.Settings.jellyfinSettingsDescription": "可以为您的{mediaServerName}服务器配置内部和外部端点。在大多数情况下外部URL与内部URL不同。如果你想重定向到不同的密码重置页面也可以为{mediaServerName}登录设置自定义密码重置URL。",
"components.Settings.jellyfinsettingsDescription": "配置{mediaServerName}服务器的设置。{mediaServerName}扫描{mediaServerName}库以查看可用的内容。",
"components.Settings.manualscanDescriptionJellyfin": "正常情况下每24小时只会运行一次。Jellyseerr将更积极地检查您的{mediaServerName}服务器最近添加的内容。如果这是您第一次配置Jellyseerr建议您手动进行一次完整的库扫描!",
"components.Settings.save": "保存更改",
"components.Setup.configuremediaserver": "配置媒体服务器",
"components.Setup.signinWithPlex": "使用您的 Plex 帐户",
"components.TitleCard.watchlistCancel": "<strong>{title}</strong>的监视列表已取消。",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong>添加到监视列表成功!",
"components.UserList.importfromJellyfin": "导入{mediaServerName}用户",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "电子邮件",
"components.UserProfile.UserSettings.UserGeneralSettings.save": "保存更改",
"components.UserList.importfromJellyfinerror": "导入{mediaServerName}用户时出错。",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "通知声音",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "保存中……",
"components.UserProfile.localWatchlist": "{username}的监视列表",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} 导入成功!"
}

View File

@@ -0,0 +1,13 @@
import Blacklist from '@app/components/Blacklist';
import useRouteGuard from '@app/hooks/useRouteGuard';
import { Permission } from '@server/lib/permissions';
import type { NextPage } from 'next';
const BlacklistPage: NextPage = () => {
useRouteGuard([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
type: 'or',
});
return <Blacklist />;
};
export default BlacklistPage;

View File

@@ -8,7 +8,7 @@ const JellyfinSettingsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS);
return (
<SettingsLayout>
<SettingsJellyfin showAdvancedSettings={true} />
<SettingsJellyfin />
</SettingsLayout>
);
};

View File

@@ -83,6 +83,16 @@
background: #f19a30;
}
.server-type-button {
@apply rounded-md border border-gray-500 bg-gray-700 px-4 py-2 text-white transition duration-150 ease-in-out hover:bg-gray-500;
}
.jellyfin-server svg {
@apply h-6 w-6;
}
.emby-server svg {
@apply h-7 w-7;
}
ul.cards-vertical,
ul.cards-horizontal {
@apply grid gap-4;