Compare commits

..

4 Commits

Author SHA1 Message Date
dd060606
bcd2bb7c96 fix: lint issues 2022-09-28 15:55:56 +02:00
dd060606
5a72f5f86e Merge branch 'develop' into features/deleteMediaFile 2022-09-14 14:58:37 +02:00
dd060606
7d4455ba6b fix: hide remove button when default service is not configured 2022-08-14 12:07:12 +02:00
dd060606
2e7458457e feat: add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr 2022-07-22 17:58:33 +02:00
80 changed files with 634 additions and 1590 deletions

View File

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

7
.github/CODEOWNERS vendored
View File

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

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -213,6 +213,20 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
);
}
}
public removeMovie = async (movieId: number): Promise<void> => {
try {
const { id, title } = await this.getMovieByTmdbId(movieId);
await this.axios.delete(`/movie/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed movie ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
}
};
}
export default RadarrAPI;

View File

@@ -302,6 +302,20 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
return newSeasons;
}
public removeSerie = async (serieId: number): Promise<void> => {
try {
const { id, title } = await this.getSeriesByTvdbId(serieId);
await this.axios.delete(`/series/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed serie ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
}
};
}
export default SonarrAPI;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,39 +0,0 @@
import ImageProxy from '@server/lib/imageproxy';
import logger from '@server/logger';
import { Router } from 'express';
const router = Router();
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
/**
* Image Proxy
*/
router.get('/*', async (req, res) => {
const imagePath = req.path.replace('/image', '');
try {
const imageData = await tmdbImageProxy.getImage(imagePath);
res.writeHead(200, {
'Content-Type': `image/${imageData.meta.extension}`,
'Content-Length': imageData.imageBuffer.length,
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
'OS-Cache-Key': imageData.meta.cacheKey,
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
});
res.end(imageData.imageBuffer);
} catch (e) {
logger.error('Failed to proxy image', {
imagePath,
errorMessage: e.message,
});
res.status(500).send();
}
});
export default router;

View File

@@ -1,4 +1,7 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import TautulliAPI from '@server/api/tautulli';
import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
@@ -168,6 +171,100 @@ mediaRoutes.delete(
}
);
mediaRoutes.delete(
'/:id/file',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
try {
const settings = getSettings();
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOneOrFail({
where: { id: Number(req.params.id) },
});
const is4k = media.serviceUrl4k !== undefined;
const isMovie = media.mediaType === MediaType.MOVIE;
let serviceSettings;
if (isMovie) {
serviceSettings = settings.radarr.find(
(radarr) => radarr.isDefault && radarr.is4k === is4k
);
} else {
serviceSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && sonarr.is4k === is4k
);
}
if (
media.serviceId &&
media.serviceId >= 0 &&
serviceSettings?.id !== media.serviceId
) {
if (isMovie) {
serviceSettings = settings.radarr.find(
(radarr) => radarr.id === media.serviceId
);
} else {
serviceSettings = settings.sonarr.find(
(sonarr) => sonarr.id === media.serviceId
);
}
}
if (!serviceSettings) {
logger.warn(
`There is no default ${
is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr'
}/ server configured. Did you set any of your ${
is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr'
} servers as default?`,
{
label: 'Media Request',
mediaId: media.id,
}
);
return;
}
let service;
if (isMovie) {
service = new RadarrAPI({
apiKey: serviceSettings?.apiKey,
url: RadarrAPI.buildUrl(serviceSettings, '/api/v3'),
});
} else {
service = new SonarrAPI({
apiKey: serviceSettings?.apiKey,
url: SonarrAPI.buildUrl(serviceSettings, '/api/v3'),
});
}
if (isMovie) {
await (service as RadarrAPI).removeMovie(
parseInt(
is4k
? (media.externalServiceSlug4k as string)
: (media.externalServiceSlug as string)
)
);
} else {
const tmdb = new TheMovieDb();
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
if (!tvdbId) {
throw new Error('TVDB ID not found');
}
await (service as SonarrAPI).removeSerie(tvdbId);
}
return res.status(204).send();
} catch (e) {
logger.error('Something went wrong fetching media in delete request', {
label: 'Media',
message: e.message,
});
next({ status: 404, message: 'Media not found' });
}
}
);
mediaRoutes.get<{ id: string }, MediaWatchDataResponse>(
'/:id/watch_data',
isAuthenticated(Permission.ADMIN),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,15 +8,23 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { ServerIcon, ViewListIcon } from '@heroicons/react/outline';
import { CheckCircleIcon, DocumentRemoveIcon } from '@heroicons/react/solid';
import {
CheckCircleIcon,
DocumentRemoveIcon,
TrashIcon,
} from '@heroicons/react/solid';
import { IssueStatus } from '@server/constants/issue';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfaces';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import getConfig from 'next/config';
import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@@ -31,9 +39,13 @@ const messages = defineMessages({
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear Data',
manageModalClearMediaWarning:
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your {mediaServerName} library, the media information will be recreated during the next scan.',
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
manageModalRemoveMediaWarning:
'* This will irreversibly remove this {mediaType} from {arr}, including all files.',
openarr: 'Open in {arr}',
removearr: 'Remove from {arr}',
openarr4k: 'Open in 4K {arr}',
removearr4k: 'Remove from 4K {arr}',
downloadstatus: 'Downloads',
markavailable: 'Mark as Available',
mark4kavailable: 'Mark as Available in 4K',
@@ -80,7 +92,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 &&
@@ -88,6 +99,12 @@ const ManageSlideOver = ({
? `/api/v1/media/${data.mediaInfo.id}/watch_data`
: null
);
const { data: radarrData } = useSWR<RadarrSettings[]>(
'/api/v1/settings/radarr'
);
const { data: sonarrData } = useSWR<SonarrSettings[]>(
'/api/v1/settings/sonarr'
);
const deleteMedia = async () => {
if (data.mediaInfo) {
@@ -96,6 +113,35 @@ const ManageSlideOver = ({
}
};
const deleteMediaFile = async () => {
if (data.mediaInfo) {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate();
}
};
const isDefaultService = () => {
if (data.mediaInfo) {
if (data.mediaInfo.mediaType === MediaType.MOVIE) {
return (
radarrData?.find(
(radarr) =>
radarr.isDefault && radarr.id === data.mediaInfo?.serviceId
) !== undefined
);
} else {
return (
sonarrData?.find(
(sonarr) =>
sonarr.isDefault && sonarr.id === data.mediaInfo?.serviceId
) !== undefined
);
}
}
return false;
};
const markAvailable = async (is4k = false) => {
if (data.mediaInfo) {
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
@@ -328,6 +374,40 @@ const ManageSlideOver = ({
</Button>
</a>
)}
{hasPermission(Permission.ADMIN) &&
data?.mediaInfo?.serviceUrl &&
isDefaultService() && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalRemoveMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</div>
</div>
)}
@@ -433,21 +513,54 @@ const ManageSlideOver = ({
</div>
)}
{data?.mediaInfo?.serviceUrl4k && (
<a
href={data?.mediaInfo?.serviceUrl4k}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
<>
<a
href={data?.mediaInfo?.serviceUrl4k}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
{isDefaultService() && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalRemoveMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</>
)}
</div>
</div>
@@ -507,13 +620,6 @@ const ManageSlideOver = ({
mediaType: intl.formatMessage(
mediaType === 'movie' ? messages.movie : messages.tvshow
),
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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