mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-29 13:09:39 -05:00
Compare commits
4 Commits
preview-pr
...
v1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57e7d68092 | ||
|
|
d3622f7bb3 | ||
|
|
20c821e2eb | ||
|
|
7b82ced5e6 |
@@ -773,42 +773,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lunks",
|
||||
"name": "Pedro Nascimento",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/91118?v=4",
|
||||
"profile": "http://twitter.com/lunks/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "owenvoke",
|
||||
"name": "Owen Voke",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1899334?v=4",
|
||||
"profile": "https://voke.dev",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Nimelrian",
|
||||
"name": "Sebastian K",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8960836?v=4",
|
||||
"profile": "https://github.com/Nimelrian",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jariz",
|
||||
"name": "jariz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1415847?v=4",
|
||||
"profile": "https://github.com/jariz",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
name: Send Discord Notification
|
||||
needs: semantic-release
|
||||
if: always()
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -16,8 +16,5 @@
|
||||
}
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"files.associations": {
|
||||
"globals.css": "tailwindcss"
|
||||
}
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
||||
|
||||
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,3 +1,68 @@
|
||||
## [1.4.1](https://github.com/fallenbagel/jellyseerr/compare/v1.4.0...v1.4.1) (2023-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pass in library type when scanning recently added items ([#3287](https://github.com/fallenbagel/jellyseerr/issues/3287)) ([8942eb8](https://github.com/fallenbagel/jellyseerr/commit/8942eb8b7c4fa1d16aa2e72e8ba7120a653c9aa2))
|
||||
* **ui:** air date will use UTC for timezone ([#3297](https://github.com/fallenbagel/jellyseerr/issues/3297)) ([3e43586](https://github.com/fallenbagel/jellyseerr/commit/3e43586acc0804c3fff524509caa890a104e132b))
|
||||
* **ui:** correct range slider styling in chrome ([#3299](https://github.com/fallenbagel/jellyseerr/issues/3299)) ([d954328](https://github.com/fallenbagel/jellyseerr/commit/d9543289111d72245564d25d300a71b0ea3954ba))
|
||||
* **ui:** show 5 icons when possible on mobile menu ([#3298](https://github.com/fallenbagel/jellyseerr/issues/3298)) ([7040da1](https://github.com/fallenbagel/jellyseerr/commit/7040da1334f6d18e19a494c73caa17f7df552dfe))
|
||||
* **ui:** style range thumbs correctly for firefox ([#3294](https://github.com/fallenbagel/jellyseerr/issues/3294)) ([9d10e6a](https://github.com/fallenbagel/jellyseerr/commit/9d10e6a88c0996671f1d9d20792e1930dbc82329))
|
||||
|
||||
# [1.4.0](https://github.com/fallenbagel/jellyseerr/compare/v1.3.0...v1.4.0) (2023-01-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add bg-opacity to in-progress status badges ([#3190](https://github.com/fallenbagel/jellyseerr/issues/3190)) ([68223f4](https://github.com/fallenbagel/jellyseerr/commit/68223f4b1e98b01825516dcba39cbb2d3df31a70))
|
||||
* added download status and title to request card/item error components ([#3186](https://github.com/fallenbagel/jellyseerr/issues/3186)) ([3309f77](https://github.com/fallenbagel/jellyseerr/commit/3309f77aa4be1d70b27693531c119a8e26822518))
|
||||
* arrow icons were misplaced on mobile in slider edit ([#3260](https://github.com/fallenbagel/jellyseerr/issues/3260)) ([d328485](https://github.com/fallenbagel/jellyseerr/commit/d328485161b9cae6a70ef0713b4878207bc6015e))
|
||||
* **build:** update usage of publish snap action ([#3272](https://github.com/fallenbagel/jellyseerr/issues/3272)) ([51b05cd](https://github.com/fallenbagel/jellyseerr/commit/51b05cd8fbb5d332807d8c00b2ffb7b10c3d0179))
|
||||
* changed overflow scroll to only if necessary ([#3184](https://github.com/fallenbagel/jellyseerr/issues/3184)) ([27feeea](https://github.com/fallenbagel/jellyseerr/commit/27feeea69121336557deda1f32b65a5daa146f82))
|
||||
* convert genre/studio to string in create slider ([#3201](https://github.com/fallenbagel/jellyseerr/issues/3201)) ([93afead](https://github.com/fallenbagel/jellyseerr/commit/93afead92e497f2e5bce67a34fffdaa08d20c7f2))
|
||||
* correct checkbox position (again) for slider edits ([#3227](https://github.com/fallenbagel/jellyseerr/issues/3227)) ([3ba6df1](https://github.com/fallenbagel/jellyseerr/commit/3ba6df1a41c084c4a6a90354338047623abef521))
|
||||
* correct grid sizing for webkit on streaming services ([#3248](https://github.com/fallenbagel/jellyseerr/issues/3248)) ([6fd11cf](https://github.com/fallenbagel/jellyseerr/commit/6fd11cf4254e1a19310592bec78a6de52bc073a8))
|
||||
* correct issue detail bottom padding on mobile displays ([#3268](https://github.com/fallenbagel/jellyseerr/issues/3268)) ([3db010b](https://github.com/fallenbagel/jellyseerr/commit/3db010b9eaec62aa08d973a61caf1801471bbf3e))
|
||||
* correct link to correct keyword results for series ([#3208](https://github.com/fallenbagel/jellyseerr/issues/3208)) ([4e9be7a](https://github.com/fallenbagel/jellyseerr/commit/4e9be7a3f7304ee7be5ee6fd34b1ea8f6c0cf399))
|
||||
* correct spacing between sliders ([#3225](https://github.com/fallenbagel/jellyseerr/issues/3225)) ([62e2de7](https://github.com/fallenbagel/jellyseerr/commit/62e2de70bf37b72d5f63370b662d4103a642775b))
|
||||
* correctly check mobile menu permissions ([#3271](https://github.com/fallenbagel/jellyseerr/issues/3271)) ([f4a22dc](https://github.com/fallenbagel/jellyseerr/commit/f4a22dc437404558f301ccfc195cf0a300dd1ff2))
|
||||
* correctly restore selected streaming service filters ([#3249](https://github.com/fallenbagel/jellyseerr/issues/3249)) ([154f3e7](https://github.com/fallenbagel/jellyseerr/commit/154f3e72efbf0b663358b3029156f54516f01a2f))
|
||||
* create shared class to add bottom spacing ([#3269](https://github.com/fallenbagel/jellyseerr/issues/3269)) ([5d1c6f7](https://github.com/fallenbagel/jellyseerr/commit/5d1c6f706555613d97ed9e61d8b665543c2f239b))
|
||||
* **deps:** pin dependency @headlessui/react to 1.7.7 ([#3194](https://github.com/fallenbagel/jellyseerr/issues/3194)) [skip ci] ([c4b16ab](https://github.com/fallenbagel/jellyseerr/commit/c4b16abc62647c74215155942a4230a31a238677))
|
||||
* **deps:** update dependency @heroicons/react to v2 ([#2970](https://github.com/fallenbagel/jellyseerr/issues/2970)) ([dd48d59](https://github.com/fallenbagel/jellyseerr/commit/dd48d59b20e2d1800ea30912116f4a4f1bb7928f))
|
||||
* **deps:** update dependency axios to v1 ([#3202](https://github.com/fallenbagel/jellyseerr/issues/3202)) ([421029e](https://github.com/fallenbagel/jellyseerr/commit/421029ebab66c9a6622ba47e56d7f6473524cce4))
|
||||
* **deps:** update dependency swr to v2 ([#3212](https://github.com/fallenbagel/jellyseerr/issues/3212)) ([7b6db50](https://github.com/fallenbagel/jellyseerr/commit/7b6db50ae55b1fc60d19a5cff62dd46bb989fa51))
|
||||
* **experimental:** use new RT API (sorta) ([#3179](https://github.com/fallenbagel/jellyseerr/issues/3179)) ([357cab8](https://github.com/fallenbagel/jellyseerr/commit/357cab87ac7752b8e119b51c938b343c661d83c2))
|
||||
* improve small screen layout for discover editing ([#3221](https://github.com/fallenbagel/jellyseerr/issues/3221)) ([d23b213](https://github.com/fallenbagel/jellyseerr/commit/d23b2132de05f072f7f9daad83d81421d747cf99))
|
||||
* include new package calendar css in build ([#3235](https://github.com/fallenbagel/jellyseerr/issues/3235)) ([c2a1a20](https://github.com/fallenbagel/jellyseerr/commit/c2a1a20a3bb20039a1936c7fe0ecb9e8311a0aea))
|
||||
* issues with issues ([#3267](https://github.com/fallenbagel/jellyseerr/issues/3267)) ([fd21971](https://github.com/fallenbagel/jellyseerr/commit/fd219717c01c558814d7a80de6304272b5a7944e))
|
||||
* multiple genre filtering now works ([#3282](https://github.com/fallenbagel/jellyseerr/issues/3282)) ([5076938](https://github.com/fallenbagel/jellyseerr/commit/507693881b939819413f0959df5ef6b7a357eb5c))
|
||||
* prevent double encode if we are on /search endpoint ([#3238](https://github.com/fallenbagel/jellyseerr/issues/3238)) ([a343f8a](https://github.com/fallenbagel/jellyseerr/commit/a343f8ad915491a9c81512c7e541a1dac8906025))
|
||||
* **request:** approve request when retrying request ([#3234](https://github.com/fallenbagel/jellyseerr/issues/3234)) ([b515701](https://github.com/fallenbagel/jellyseerr/commit/b5157010c46cd9083993d5ee0172007b83d631da))
|
||||
* **request:** mark request as approved if media is already available when retrying failed request ([#3244](https://github.com/fallenbagel/jellyseerr/issues/3244)) ([cb65074](https://github.com/fallenbagel/jellyseerr/commit/cb650745f6a33e69391a633e6d272831f314e098))
|
||||
* restore border to ghost button and fix discover slider visibility toggle position ([#3226](https://github.com/fallenbagel/jellyseerr/issues/3226)) ([2eebb7f](https://github.com/fallenbagel/jellyseerr/commit/2eebb7fd3941b34fe9472aaf9d28265df8cce311))
|
||||
* restore status badges on titles on actors page when hide available media enabled ([#3206](https://github.com/fallenbagel/jellyseerr/issues/3206)) ([9d3446d](https://github.com/fallenbagel/jellyseerr/commit/9d3446d370499c3251159393e5c791b01225e05c))
|
||||
* screen would zoom on mobile if date picker input was selected ([#3241](https://github.com/fallenbagel/jellyseerr/issues/3241)) ([3aefddd](https://github.com/fallenbagel/jellyseerr/commit/3aefddd48834d86150d5f5cceb2d08af3a78847b))
|
||||
* series displayed an empty season with series list/request modal ([#3147](https://github.com/fallenbagel/jellyseerr/issues/3147)) ([2179637](https://github.com/fallenbagel/jellyseerr/commit/2179637d437999290eaa4152f6f37c71fc3d8ba3))
|
||||
* tooltip shows properly if not in progress ([#3185](https://github.com/fallenbagel/jellyseerr/issues/3185)) ([6face8c](https://github.com/fallenbagel/jellyseerr/commit/6face8cc4564b978fb98af32659b326d8c5cede8))
|
||||
* **ui:** series first air date sorting ([#3283](https://github.com/fallenbagel/jellyseerr/issues/3283)) ([374c78c](https://github.com/fallenbagel/jellyseerr/commit/374c78c989cc86bb144a954a91d5d183c4b591c0))
|
||||
* update StatusBadgeMini to shrink on title cards (and remove ring) ([#3210](https://github.com/fallenbagel/jellyseerr/issues/3210)) ([042a1a9](https://github.com/fallenbagel/jellyseerr/commit/042a1a950fdd4d4a61edf4bc19657f9b7a526da8))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add discover customization ([#3182](https://github.com/fallenbagel/jellyseerr/issues/3182)) ([cd35748](https://github.com/fallenbagel/jellyseerr/commit/cd3574851a12517cbfadc109e6412a7a9e44c114))
|
||||
* add keywords to movie/series detail pages ([#3204](https://github.com/fallenbagel/jellyseerr/issues/3204)) ([e084649](https://github.com/fallenbagel/jellyseerr/commit/e084649878a58c296786141d12dd69a69a27ee85))
|
||||
* add streaming services filter ([#3247](https://github.com/fallenbagel/jellyseerr/issues/3247)) ([1154156](https://github.com/fallenbagel/jellyseerr/commit/1154156459403494e8daf0c89a3ba356aeea1d97))
|
||||
* discover inline customization ([#3220](https://github.com/fallenbagel/jellyseerr/issues/3220)) ([8bd10b5](https://github.com/fallenbagel/jellyseerr/commit/8bd10b5bf3d1b8069872b616c7c8596caeb4937e))
|
||||
* discover overhaul (filters!) ([#3232](https://github.com/fallenbagel/jellyseerr/issues/3232)) ([dd00e48](https://github.com/fallenbagel/jellyseerr/commit/dd00e48f59054b44bef6b32a2c169e59f6175051))
|
||||
* discover slider edit arrow buttons for reordering ([#3259](https://github.com/fallenbagel/jellyseerr/issues/3259)) ([da00d45](https://github.com/fallenbagel/jellyseerr/commit/da00d454e17e8b00d04f6e26f6dd5153ed6ced81))
|
||||
* **lang:** translations update from Hosted Weblate ([#3030](https://github.com/fallenbagel/jellyseerr/issues/3030)) ([0d8b390](https://github.com/fallenbagel/jellyseerr/commit/0d8b390b678731e76bd1f0f8a0a4952c11e77f4d))
|
||||
* new mobile menu ([#3251](https://github.com/fallenbagel/jellyseerr/issues/3251)) ([fcbca17](https://github.com/fallenbagel/jellyseerr/commit/fcbca1722f31f32633a57bc5048f46c9da057d87))
|
||||
* translations update from Hosted Weblate ([#3218](https://github.com/fallenbagel/jellyseerr/issues/3218)) ([5940ff7](https://github.com/fallenbagel/jellyseerr/commit/5940ff7f5f62eed9ac5aa6f02803418aaa09813a))
|
||||
* **ui:** add episode number to front of episode name in season details ([#3086](https://github.com/fallenbagel/jellyseerr/issues/3086)) ([a672b32](https://github.com/fallenbagel/jellyseerr/commit/a672b324ec391a20f6f3a1daed82a8d276a52c2c))
|
||||
* **ui:** request card progress bar ([#3123](https://github.com/fallenbagel/jellyseerr/issues/3123)) ([03853a1](https://github.com/fallenbagel/jellyseerr/commit/03853a1b9155c8a2153c8885022a74619af1bc15))
|
||||
|
||||
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
21
README.md
21
README.md
@@ -2,23 +2,9 @@
|
||||
<img src="./public/logo_full.svg" alt="Jellyseerr" style="margin: 20px 0;">
|
||||
</p>
|
||||
<p align="center">
|
||||
<<<<<<< HEAD
|
||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
=======
|
||||
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" />
|
||||
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20CI/badge.svg" alt="Overseerr CI">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/overseerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||
<a href="https://hub.docker.com/r/sctx/overseerr"><img src="https://img.shields.io/docker/pulls/sctx/overseerr" alt="Docker pulls"></a>
|
||||
<a href="https://hosted.weblate.org/engage/overseerr/"><img src="https://hosted.weblate.org/widgets/overseerr/-/overseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-88-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
>>>>>>> upstream/develop
|
||||
</p>
|
||||
|
||||
**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 Jellyfin & Emby media servers!
|
||||
@@ -154,11 +140,4 @@ Our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/COD
|
||||
|
||||
## Contributing
|
||||
|
||||
<<<<<<< HEAD
|
||||
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.
|
||||
=======
|
||||
You can help improve Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to all wonderful people who contributed directly to Jellyseerr and Overseerr.
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('Discover Customization', () => {
|
||||
.should('be.disabled');
|
||||
|
||||
cy.get('#data').clear();
|
||||
cy.get('#data').type('christmas{enter}', { delay: 100 });
|
||||
cy.get('#data').type('time travel{enter}', { delay: 100 });
|
||||
|
||||
// Confirming we have some results
|
||||
cy.contains('.slider-header', sliderTitle)
|
||||
|
||||
@@ -23,6 +23,5 @@ module.exports = {
|
||||
},
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
largePageDataBytes: 256000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3868,7 +3868,7 @@ paths:
|
||||
$ref: '#/components/schemas/User'
|
||||
/user/{userId}/requests:
|
||||
get:
|
||||
summary: Get requests for a specific user
|
||||
summary: Get user by ID
|
||||
description: |
|
||||
Retrieves a user's requests in a JSON object.
|
||||
tags:
|
||||
@@ -3964,7 +3964,7 @@ paths:
|
||||
example: false
|
||||
/user/{userId}/watchlist:
|
||||
get:
|
||||
summary: Get the Plex watchlist for a specific user
|
||||
summary: Get user by ID
|
||||
description: |
|
||||
Retrieves a user's Plex Watchlist in a JSON object.
|
||||
tags:
|
||||
@@ -5876,23 +5876,6 @@ 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyseerr",
|
||||
"version": "0.1.0",
|
||||
"version": "1.4.1",
|
||||
"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",
|
||||
|
||||
@@ -213,20 +213,6 @@ 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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logger from '@server/logger';
|
||||
import ServarrBase from './base';
|
||||
|
||||
export interface SonarrSeason {
|
||||
interface SonarrSeason {
|
||||
seasonNumber: number;
|
||||
monitored: boolean;
|
||||
statistics?: {
|
||||
@@ -321,20 +321,6 @@ class SonarrAPI extends ServarrBase<{
|
||||
|
||||
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;
|
||||
|
||||
@@ -115,29 +115,29 @@ class Media {
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
public mediaAddedAt: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'int' })
|
||||
public serviceId?: number | null;
|
||||
@Column({ nullable: true })
|
||||
public serviceId?: number;
|
||||
|
||||
@Column({ nullable: true, type: 'int' })
|
||||
public serviceId4k?: number | null;
|
||||
@Column({ nullable: true })
|
||||
public serviceId4k?: number;
|
||||
|
||||
@Column({ nullable: true, type: 'int' })
|
||||
public externalServiceId?: number | null;
|
||||
@Column({ nullable: true })
|
||||
public externalServiceId?: number;
|
||||
|
||||
@Column({ nullable: true, type: 'int' })
|
||||
public externalServiceId4k?: number | null;
|
||||
@Column({ nullable: true })
|
||||
public externalServiceId4k?: number;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
public externalServiceSlug?: string | null;
|
||||
@Column({ nullable: true })
|
||||
public externalServiceSlug?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
public externalServiceSlug4k?: string | null;
|
||||
@Column({ nullable: true })
|
||||
public externalServiceSlug4k?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
public ratingKey?: string | null;
|
||||
@Column({ nullable: true })
|
||||
public ratingKey?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
public ratingKey4k?: string | null;
|
||||
@Column({ nullable: true })
|
||||
public ratingKey4k?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinMediaId?: string;
|
||||
@@ -288,9 +288,7 @@ class Media {
|
||||
if (this.mediaType === MediaType.MOVIE) {
|
||||
if (
|
||||
this.externalServiceId !== undefined &&
|
||||
this.externalServiceId !== null &&
|
||||
this.serviceId !== undefined &&
|
||||
this.serviceId !== null
|
||||
this.serviceId !== undefined
|
||||
) {
|
||||
this.downloadStatus = downloadTracker.getMovieProgress(
|
||||
this.serviceId,
|
||||
@@ -300,9 +298,7 @@ class Media {
|
||||
|
||||
if (
|
||||
this.externalServiceId4k !== undefined &&
|
||||
this.externalServiceId4k !== null &&
|
||||
this.serviceId4k !== undefined &&
|
||||
this.serviceId4k !== null
|
||||
this.serviceId4k !== undefined
|
||||
) {
|
||||
this.downloadStatus4k = downloadTracker.getMovieProgress(
|
||||
this.serviceId4k,
|
||||
@@ -314,9 +310,7 @@ class Media {
|
||||
if (this.mediaType === MediaType.TV) {
|
||||
if (
|
||||
this.externalServiceId !== undefined &&
|
||||
this.externalServiceId !== null &&
|
||||
this.serviceId !== undefined &&
|
||||
this.serviceId !== null
|
||||
this.serviceId !== undefined
|
||||
) {
|
||||
this.downloadStatus = downloadTracker.getSeriesProgress(
|
||||
this.serviceId,
|
||||
@@ -326,9 +320,7 @@ class Media {
|
||||
|
||||
if (
|
||||
this.externalServiceId4k !== undefined &&
|
||||
this.externalServiceId4k !== null &&
|
||||
this.serviceId4k !== undefined &&
|
||||
this.serviceId4k !== null
|
||||
this.serviceId4k !== undefined
|
||||
) {
|
||||
this.downloadStatus4k = downloadTracker.getSeriesProgress(
|
||||
this.serviceId4k,
|
||||
|
||||
@@ -1187,5 +1187,3 @@ export class MediaRequest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MediaRequest;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import {
|
||||
AfterRemove,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@@ -36,18 +34,6 @@ class SeasonRequest {
|
||||
constructor(init?: Partial<SeasonRequest>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterRemove()
|
||||
public async handleRemoveParent(): Promise<void> {
|
||||
const mediaRequestRepository = getRepository(MediaRequest);
|
||||
const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({
|
||||
where: { id: this.request.id },
|
||||
});
|
||||
|
||||
if (requestToBeDeleted.seasons.length === 0) {
|
||||
await mediaRequestRepository.delete({ id: this.request.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SeasonRequest;
|
||||
|
||||
@@ -17,7 +17,6 @@ import WebhookAgent from '@server/lib/notifications/agents/webhook';
|
||||
import WebPushAgent from '@server/lib/notifications/agents/webpush';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import clearCookies from '@server/middleware/clearcookies';
|
||||
import routes from '@server/routes';
|
||||
import imageproxy from '@server/routes/imageproxy';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
@@ -193,8 +192,7 @@ app
|
||||
});
|
||||
server.use('/api/v1', routes);
|
||||
|
||||
// Do not set cookies so CDNs can cache them
|
||||
server.use('/imageproxy', clearCookies, imageproxy);
|
||||
server.use('/imageproxy', imageproxy);
|
||||
|
||||
server.get('*', (req, res) => handle(req, res));
|
||||
server.use(
|
||||
|
||||
@@ -278,11 +278,11 @@ class JobJellyfinSync {
|
||||
ExtendedEpisodeData.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
if (MediaStream.Type === 'Video') {
|
||||
if ((MediaStream.Width ?? 0) >= 2000) {
|
||||
total4k += episodeCount;
|
||||
} else {
|
||||
if (MediaStream.Width ?? 0 < 2000) {
|
||||
totalStandard += episodeCount;
|
||||
}
|
||||
} else {
|
||||
total4k += episodeCount;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
||||
@@ -17,7 +16,7 @@ interface ScheduledJob {
|
||||
job: schedule.Job;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
|
||||
interval: 'short' | 'long' | 'fixed';
|
||||
cronSchedule: string;
|
||||
running?: () => boolean;
|
||||
cancelFn?: () => void;
|
||||
@@ -35,7 +34,7 @@ export const startJobs = (): void => {
|
||||
id: 'plex-recently-added-scan',
|
||||
name: 'Plex Recently Added Scan',
|
||||
type: 'process',
|
||||
interval: 'minutes',
|
||||
interval: 'short',
|
||||
cronSchedule: jobs['plex-recently-added-scan'].schedule,
|
||||
job: schedule.scheduleJob(
|
||||
jobs['plex-recently-added-scan'].schedule,
|
||||
@@ -55,7 +54,7 @@ export const startJobs = (): void => {
|
||||
id: 'plex-full-scan',
|
||||
name: 'Plex Full Library Scan',
|
||||
type: 'process',
|
||||
interval: 'hours',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['plex-full-scan'].schedule,
|
||||
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Plex Full Library Scan', {
|
||||
@@ -75,7 +74,7 @@ export const startJobs = (): void => {
|
||||
id: 'jellyfin-recently-added-sync',
|
||||
name: 'Jellyfin Recently Added Sync',
|
||||
type: 'process',
|
||||
interval: 'minutes',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['jellyfin-recently-added-sync'].schedule,
|
||||
job: schedule.scheduleJob(
|
||||
jobs['jellyfin-recently-added-sync'].schedule,
|
||||
@@ -95,7 +94,7 @@ export const startJobs = (): void => {
|
||||
id: 'jellyfin-full-sync',
|
||||
name: 'Jellyfin Full Library Sync',
|
||||
type: 'process',
|
||||
interval: 'hours',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['jellyfin-full-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['jellyfin-full-sync'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Jellyfin Full Sync', {
|
||||
@@ -113,7 +112,7 @@ export const startJobs = (): void => {
|
||||
id: 'plex-watchlist-sync',
|
||||
name: 'Plex Watchlist Sync',
|
||||
type: 'process',
|
||||
interval: 'minutes',
|
||||
interval: 'short',
|
||||
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||
@@ -128,7 +127,7 @@ export const startJobs = (): void => {
|
||||
id: 'radarr-scan',
|
||||
name: 'Radarr Scan',
|
||||
type: 'process',
|
||||
interval: 'hours',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['radarr-scan'].schedule,
|
||||
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
|
||||
@@ -143,7 +142,7 @@ export const startJobs = (): void => {
|
||||
id: 'sonarr-scan',
|
||||
name: 'Sonarr Scan',
|
||||
type: 'process',
|
||||
interval: 'hours',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['sonarr-scan'].schedule,
|
||||
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
|
||||
@@ -153,29 +152,12 @@ export const startJobs = (): void => {
|
||||
cancelFn: () => sonarrScanner.cancel(),
|
||||
});
|
||||
|
||||
// Checks if media is still available in plex/sonarr/radarr libs
|
||||
scheduledJobs.push({
|
||||
id: 'availability-sync',
|
||||
name: 'Media Availability Sync',
|
||||
type: 'process',
|
||||
interval: 'hours',
|
||||
cronSchedule: jobs['availability-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['availability-sync'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Media Availability Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
availabilitySync.run();
|
||||
}),
|
||||
running: () => availabilitySync.running,
|
||||
cancelFn: () => availabilitySync.cancel(),
|
||||
});
|
||||
|
||||
// Run download sync every minute
|
||||
scheduledJobs.push({
|
||||
id: 'download-sync',
|
||||
name: 'Download Sync',
|
||||
type: 'command',
|
||||
interval: 'seconds',
|
||||
interval: 'fixed',
|
||||
cronSchedule: jobs['download-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
|
||||
logger.debug('Starting scheduled job: Download Sync', {
|
||||
@@ -190,7 +172,7 @@ export const startJobs = (): void => {
|
||||
id: 'download-sync-reset',
|
||||
name: 'Download Sync Reset',
|
||||
type: 'command',
|
||||
interval: 'hours',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['download-sync-reset'].schedule,
|
||||
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Download Sync Reset', {
|
||||
@@ -200,12 +182,12 @@ export const startJobs = (): void => {
|
||||
}),
|
||||
});
|
||||
|
||||
// Run image cache cleanup every 24 hours
|
||||
// Run image cache cleanup every 5 minutes
|
||||
scheduledJobs.push({
|
||||
id: 'image-cache-cleanup',
|
||||
name: 'Image Cache Cleanup',
|
||||
type: 'process',
|
||||
interval: 'hours',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['image-cache-cleanup'].schedule,
|
||||
job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Image Cache Cleanup', {
|
||||
|
||||
@@ -1,718 +0,0 @@
|
||||
import type { PlexMetadata } from '@server/api/plexapi';
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import type { SonarrSeason } from '@server/api/servarr/sonarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import MediaRequest from '@server/entity/MediaRequest';
|
||||
import Season from '@server/entity/Season';
|
||||
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
|
||||
class AvailabilitySync {
|
||||
public running = false;
|
||||
private plexClient: PlexAPI;
|
||||
private plexSeasonsCache: Record<string, PlexMetadata[]> = {};
|
||||
private sonarrSeasonsCache: Record<string, SonarrSeason[]> = {};
|
||||
private radarrServers: RadarrSettings[];
|
||||
private sonarrServers: SonarrSettings[];
|
||||
|
||||
async run() {
|
||||
const settings = getSettings();
|
||||
this.running = true;
|
||||
this.plexSeasonsCache = {};
|
||||
this.sonarrSeasonsCache = {};
|
||||
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
|
||||
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
|
||||
await this.initPlexClient();
|
||||
|
||||
if (!this.plexClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Starting availability sync...`, {
|
||||
label: 'AvailabilitySync',
|
||||
});
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const seasonRepository = getRepository(Season);
|
||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||
|
||||
const pageSize = 50;
|
||||
|
||||
try {
|
||||
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
|
||||
try {
|
||||
if (!this.running) {
|
||||
throw new Error('Job aborted');
|
||||
}
|
||||
|
||||
const mediaExists = await this.mediaExists(media);
|
||||
|
||||
//We can not delete media so if both versions do not exist, we will change both columns to unknown or null
|
||||
if (!mediaExists) {
|
||||
if (
|
||||
media.status !== MediaStatus.UNKNOWN ||
|
||||
media.status4k !== MediaStatus.UNKNOWN
|
||||
) {
|
||||
const request = await requestRepository.find({
|
||||
relations: {
|
||||
media: true,
|
||||
},
|
||||
where: { media: { id: media.id } },
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`${
|
||||
media.mediaType === 'tv' ? media.tvdbId : media.tmdbId
|
||||
} does not exist in any of your media instances. We will change its status to unknown.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
|
||||
await mediaRepository.update(media.id, {
|
||||
status: MediaStatus.UNKNOWN,
|
||||
status4k: MediaStatus.UNKNOWN,
|
||||
serviceId: null,
|
||||
serviceId4k: null,
|
||||
externalServiceId: null,
|
||||
externalServiceId4k: null,
|
||||
externalServiceSlug: null,
|
||||
externalServiceSlug4k: null,
|
||||
ratingKey: null,
|
||||
ratingKey4k: null,
|
||||
});
|
||||
|
||||
await requestRepository.remove(request);
|
||||
}
|
||||
}
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
// ok, the show itself exists, but do all it's seasons?
|
||||
const seasons = await seasonRepository.find({
|
||||
where: [
|
||||
{ status: MediaStatus.AVAILABLE, media: { id: media.id } },
|
||||
{
|
||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||
media: { id: media.id },
|
||||
},
|
||||
{ status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
|
||||
{
|
||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||
media: { id: media.id },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let didDeleteSeasons = false;
|
||||
for (const season of seasons) {
|
||||
if (
|
||||
!mediaExists &&
|
||||
(season.status !== MediaStatus.UNKNOWN ||
|
||||
season.status4k !== MediaStatus.UNKNOWN)
|
||||
) {
|
||||
await seasonRepository.update(
|
||||
{ id: season.id },
|
||||
{
|
||||
status: MediaStatus.UNKNOWN,
|
||||
status4k: MediaStatus.UNKNOWN,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const seasonExists = await this.seasonExists(media, season);
|
||||
|
||||
if (!seasonExists) {
|
||||
logger.info(
|
||||
`Removing season ${season.seasonNumber}, media id: ${media.tvdbId} because it does not exist in any of your media instances.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
|
||||
if (
|
||||
season.status !== MediaStatus.UNKNOWN ||
|
||||
season.status4k !== MediaStatus.UNKNOWN
|
||||
) {
|
||||
await seasonRepository.update(
|
||||
{ id: season.id },
|
||||
{
|
||||
status: MediaStatus.UNKNOWN,
|
||||
status4k: MediaStatus.UNKNOWN,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const seasonToBeDeleted =
|
||||
await seasonRequestRepository.findOne({
|
||||
relations: {
|
||||
request: {
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
request: {
|
||||
media: {
|
||||
id: media.id,
|
||||
},
|
||||
},
|
||||
seasonNumber: season.seasonNumber,
|
||||
},
|
||||
});
|
||||
|
||||
if (seasonToBeDeleted) {
|
||||
await seasonRequestRepository.remove(seasonToBeDeleted);
|
||||
}
|
||||
|
||||
didDeleteSeasons = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didDeleteSeasons) {
|
||||
if (
|
||||
media.status === MediaStatus.AVAILABLE ||
|
||||
media.status4k === MediaStatus.AVAILABLE
|
||||
) {
|
||||
logger.info(
|
||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted some of its seasons.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
|
||||
if (media.status === MediaStatus.AVAILABLE) {
|
||||
await mediaRepository.update(media.id, {
|
||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||
});
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||
await mediaRepository.update(media.id, {
|
||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.error('Failure with media.', {
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.error('Failed to complete availability sync.', {
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
});
|
||||
} finally {
|
||||
logger.info(`Availability sync complete.`, {
|
||||
label: 'AvailabilitySync',
|
||||
});
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
private async *loadAvailableMediaPaginated(pageSize: number) {
|
||||
let offset = 0;
|
||||
const mediaRepository = getRepository(Media);
|
||||
const whereOptions = [
|
||||
{ status: MediaStatus.AVAILABLE },
|
||||
{ status: MediaStatus.PARTIALLY_AVAILABLE },
|
||||
{ status4k: MediaStatus.AVAILABLE },
|
||||
{ status4k: MediaStatus.PARTIALLY_AVAILABLE },
|
||||
];
|
||||
|
||||
let mediaPage: Media[];
|
||||
|
||||
do {
|
||||
yield* (mediaPage = await mediaRepository.find({
|
||||
where: whereOptions,
|
||||
skip: offset,
|
||||
take: pageSize,
|
||||
}));
|
||||
offset += pageSize;
|
||||
} while (mediaPage.length > 0);
|
||||
}
|
||||
|
||||
private async mediaUpdater(media: Media, is4k: boolean): Promise<void> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
const isTVType = media.mediaType === 'tv';
|
||||
|
||||
const request = await requestRepository.findOne({
|
||||
relations: {
|
||||
media: true,
|
||||
},
|
||||
where: { media: { id: media.id }, is4k: is4k ? true : false },
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`${media.tmdbId} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
|
||||
isTVType ? 'sonarr' : 'radarr'
|
||||
} and plex instance. We will change its status to unknown.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
|
||||
await mediaRepository.update(
|
||||
media.id,
|
||||
is4k
|
||||
? {
|
||||
status4k: MediaStatus.UNKNOWN,
|
||||
serviceId4k: null,
|
||||
externalServiceId4k: null,
|
||||
externalServiceSlug4k: null,
|
||||
ratingKey4k: null,
|
||||
}
|
||||
: {
|
||||
status: MediaStatus.UNKNOWN,
|
||||
serviceId: null,
|
||||
externalServiceId: null,
|
||||
externalServiceSlug: null,
|
||||
ratingKey: null,
|
||||
}
|
||||
);
|
||||
|
||||
if (isTVType) {
|
||||
const seasonRepository = getRepository(Season);
|
||||
|
||||
await seasonRepository?.update(
|
||||
{ media: { id: media.id } },
|
||||
is4k
|
||||
? { status4k: MediaStatus.UNKNOWN }
|
||||
: { status: MediaStatus.UNKNOWN }
|
||||
);
|
||||
}
|
||||
|
||||
await requestRepository.delete({ id: request?.id });
|
||||
}
|
||||
|
||||
private async mediaExistsInRadarr(
|
||||
media: Media,
|
||||
existsInPlex: boolean,
|
||||
existsInPlex4k: boolean
|
||||
): Promise<boolean> {
|
||||
let existsInRadarr = true;
|
||||
let existsInRadarr4k = true;
|
||||
|
||||
for (const server of this.radarrServers) {
|
||||
const api = new RadarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
||||
});
|
||||
const meta = await api.getMovieByTmdbId(media.tmdbId);
|
||||
|
||||
//check if both exist or if a single non-4k or 4k exists
|
||||
//if both do not exist we will return false
|
||||
if (!server.is4k && !meta.id) {
|
||||
existsInRadarr = false;
|
||||
}
|
||||
|
||||
if (server.is4k && !meta.id) {
|
||||
existsInRadarr4k = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInRadarr && existsInRadarr4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!existsInRadarr && existsInPlex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!existsInRadarr4k && existsInPlex4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//if only a single non-4k or 4k exists, then change entity columns accordingly
|
||||
//related media request will then be deleted
|
||||
if (!existsInRadarr && existsInRadarr4k && !existsInPlex) {
|
||||
if (media.status !== MediaStatus.UNKNOWN) {
|
||||
this.mediaUpdater(media, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInRadarr && !existsInRadarr4k && !existsInPlex4k) {
|
||||
if (media.status4k !== MediaStatus.UNKNOWN) {
|
||||
this.mediaUpdater(media, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInRadarr || existsInRadarr4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async mediaExistsInSonarr(
|
||||
media: Media,
|
||||
existsInPlex: boolean,
|
||||
existsInPlex4k: boolean
|
||||
): Promise<boolean> {
|
||||
if (!media.tvdbId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let existsInSonarr = true;
|
||||
let existsInSonarr4k = true;
|
||||
|
||||
for (const server of this.sonarrServers) {
|
||||
const api = new SonarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||
});
|
||||
|
||||
const meta = await api.getSeriesByTvdbId(media.tvdbId);
|
||||
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = meta.seasons;
|
||||
|
||||
//check if both exist or if a single non-4k or 4k exists
|
||||
//if both do not exist we will return false
|
||||
if (!server.is4k && !meta.id) {
|
||||
existsInSonarr = false;
|
||||
}
|
||||
|
||||
if (server.is4k && !meta.id) {
|
||||
existsInSonarr4k = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInSonarr && existsInSonarr4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!existsInSonarr && existsInPlex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!existsInSonarr4k && existsInPlex4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//if only a single non-4k or 4k exists, then change entity columns accordingly
|
||||
//related media request will then be deleted
|
||||
if (!existsInSonarr && existsInSonarr4k && !existsInPlex) {
|
||||
if (media.status !== MediaStatus.UNKNOWN) {
|
||||
this.mediaUpdater(media, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInSonarr && !existsInSonarr4k && !existsInPlex4k) {
|
||||
if (media.status4k !== MediaStatus.UNKNOWN) {
|
||||
this.mediaUpdater(media, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInSonarr || existsInSonarr4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async seasonExistsInSonarr(
|
||||
media: Media,
|
||||
season: Season,
|
||||
seasonExistsInPlex: boolean,
|
||||
seasonExistsInPlex4k: boolean
|
||||
): Promise<boolean> {
|
||||
if (!media.tvdbId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let seasonExistsInSonarr = true;
|
||||
let seasonExistsInSonarr4k = true;
|
||||
|
||||
const mediaRepository = getRepository(Media);
|
||||
const seasonRepository = getRepository(Season);
|
||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||
|
||||
for (const server of this.sonarrServers) {
|
||||
const api = new SonarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||
});
|
||||
|
||||
const seasons =
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] ??
|
||||
(await api.getSeriesByTvdbId(media.tvdbId)).seasons;
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = seasons;
|
||||
|
||||
const hasMonitoredSeason = seasons.find(
|
||||
({ monitored, seasonNumber }) =>
|
||||
monitored && season.seasonNumber === seasonNumber
|
||||
);
|
||||
|
||||
if (!server.is4k && !hasMonitoredSeason) {
|
||||
seasonExistsInSonarr = false;
|
||||
}
|
||||
|
||||
if (server.is4k && !hasMonitoredSeason) {
|
||||
seasonExistsInSonarr4k = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (seasonExistsInSonarr && seasonExistsInSonarr4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!seasonExistsInSonarr && seasonExistsInPlex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!seasonExistsInSonarr4k && seasonExistsInPlex4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const seasonToBeDeleted = await seasonRequestRepository.findOne({
|
||||
relations: {
|
||||
request: {
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
request: {
|
||||
is4k: seasonExistsInSonarr ? true : false,
|
||||
media: {
|
||||
id: media.id,
|
||||
},
|
||||
},
|
||||
seasonNumber: season.seasonNumber,
|
||||
},
|
||||
});
|
||||
|
||||
//if season does not exist, we will change status to unknown and delete related season request
|
||||
//if parent media request is empty(all related seasons have been removed), parent is automatically deleted
|
||||
if (
|
||||
!seasonExistsInSonarr &&
|
||||
seasonExistsInSonarr4k &&
|
||||
!seasonExistsInPlex
|
||||
) {
|
||||
if (season.status !== MediaStatus.UNKNOWN) {
|
||||
logger.info(
|
||||
`${media.tvdbId}, season: ${season.seasonNumber} does not exist in your non-4k sonarr and plex instance. We will change its status to unknown.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
await seasonRepository.update(season.id, {
|
||||
status: MediaStatus.UNKNOWN,
|
||||
});
|
||||
|
||||
if (seasonToBeDeleted) {
|
||||
await seasonRequestRepository.remove(seasonToBeDeleted);
|
||||
}
|
||||
|
||||
if (media.status === MediaStatus.AVAILABLE) {
|
||||
logger.info(
|
||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
await mediaRepository.update(media.id, {
|
||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
seasonExistsInSonarr &&
|
||||
!seasonExistsInSonarr4k &&
|
||||
!seasonExistsInPlex4k
|
||||
) {
|
||||
if (season.status4k !== MediaStatus.UNKNOWN) {
|
||||
logger.info(
|
||||
`${media.tvdbId}, season: ${season.seasonNumber} does not exist in your 4k sonarr and plex instance. We will change its status to unknown.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
await seasonRepository.update(season.id, {
|
||||
status4k: MediaStatus.UNKNOWN,
|
||||
});
|
||||
|
||||
if (seasonToBeDeleted) {
|
||||
await seasonRequestRepository.remove(seasonToBeDeleted);
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||
logger.info(
|
||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
await mediaRepository.update(media.id, {
|
||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seasonExistsInSonarr || seasonExistsInSonarr4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async mediaExists(media: Media): Promise<boolean> {
|
||||
const ratingKey = media.ratingKey;
|
||||
const ratingKey4k = media.ratingKey4k;
|
||||
|
||||
let existsInPlex = false;
|
||||
let existsInPlex4k = false;
|
||||
|
||||
//check each plex instance to see if media exists
|
||||
try {
|
||||
if (ratingKey) {
|
||||
const meta = await this.plexClient?.getMetadata(ratingKey);
|
||||
if (meta) {
|
||||
existsInPlex = true;
|
||||
}
|
||||
}
|
||||
if (ratingKey4k) {
|
||||
const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
|
||||
if (meta4k) {
|
||||
existsInPlex4k = true;
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
// TODO: oof, not the nicest way of handling this, but plex-api does not leave us with any other options...
|
||||
if (!ex.message.includes('response code: 404')) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
//base case for if both media versions exist in plex
|
||||
if (existsInPlex && existsInPlex4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//we then check radarr or sonarr has that specific media. If not, then we will move to delete
|
||||
//if a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
|
||||
if (media.mediaType === 'movie') {
|
||||
const existsInRadarr = await this.mediaExistsInRadarr(
|
||||
media,
|
||||
existsInPlex,
|
||||
existsInPlex4k
|
||||
);
|
||||
|
||||
//if true, media exists in at least one radarr or plex instance.
|
||||
if (existsInRadarr) {
|
||||
logger.warn(
|
||||
`${media.tmdbId} exists in at least one radarr or plex instance. Media will be updated if set to available.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
const existsInSonarr = await this.mediaExistsInSonarr(
|
||||
media,
|
||||
existsInPlex,
|
||||
existsInPlex4k
|
||||
);
|
||||
|
||||
//if true, media exists in at least one sonarr or plex instance.
|
||||
if (existsInSonarr) {
|
||||
logger.warn(
|
||||
`${media.tvdbId} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async seasonExists(media: Media, season: Season) {
|
||||
const ratingKey = media.ratingKey;
|
||||
const ratingKey4k = media.ratingKey4k;
|
||||
|
||||
let seasonExistsInPlex = false;
|
||||
let seasonExistsInPlex4k = false;
|
||||
|
||||
if (ratingKey) {
|
||||
const children =
|
||||
this.plexSeasonsCache[ratingKey] ??
|
||||
(await this.plexClient?.getChildrenMetadata(ratingKey)) ??
|
||||
[];
|
||||
this.plexSeasonsCache[ratingKey] = children;
|
||||
const seasonMeta = children?.find(
|
||||
(child) => child.index === season.seasonNumber
|
||||
);
|
||||
|
||||
if (seasonMeta) {
|
||||
seasonExistsInPlex = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ratingKey4k) {
|
||||
const children4k =
|
||||
this.plexSeasonsCache[ratingKey4k] ??
|
||||
(await this.plexClient?.getChildrenMetadata(ratingKey4k)) ??
|
||||
[];
|
||||
this.plexSeasonsCache[ratingKey4k] = children4k;
|
||||
const seasonMeta4k = children4k?.find(
|
||||
(child) => child.index === season.seasonNumber
|
||||
);
|
||||
|
||||
if (seasonMeta4k) {
|
||||
seasonExistsInPlex4k = true;
|
||||
}
|
||||
}
|
||||
|
||||
//base case for if both season versions exist in plex
|
||||
if (seasonExistsInPlex && seasonExistsInPlex4k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const existsInSonarr = await this.seasonExistsInSonarr(
|
||||
media,
|
||||
season,
|
||||
seasonExistsInPlex,
|
||||
seasonExistsInPlex4k
|
||||
);
|
||||
|
||||
if (existsInSonarr) {
|
||||
logger.warn(
|
||||
`${media.tvdbId}, season: ${season.seasonNumber} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async initPlexClient() {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
logger.warning('No admin configured. Availability sync skipped.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
||||
}
|
||||
}
|
||||
|
||||
const availabilitySync = new AvailabilitySync();
|
||||
export default availabilitySync;
|
||||
@@ -18,14 +18,14 @@ type ImageResponse = {
|
||||
imageBuffer: Buffer;
|
||||
};
|
||||
|
||||
const baseCacheDirectory = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/cache/images`
|
||||
: path.join(__dirname, '../../config/cache/images');
|
||||
|
||||
class ImageProxy {
|
||||
public static async clearCache(key: string) {
|
||||
let deletedImages = 0;
|
||||
const cacheDirectory = path.join(baseCacheDirectory, key);
|
||||
const cacheDirectory = path.join(
|
||||
__dirname,
|
||||
'../../config/cache/images/',
|
||||
key
|
||||
);
|
||||
|
||||
const files = await promises.readdir(cacheDirectory);
|
||||
|
||||
@@ -57,7 +57,11 @@ class ImageProxy {
|
||||
public static async getImageStats(
|
||||
key: string
|
||||
): Promise<{ size: number; imageCount: number }> {
|
||||
const cacheDirectory = path.join(baseCacheDirectory, key);
|
||||
const cacheDirectory = path.join(
|
||||
__dirname,
|
||||
'../../config/cache/images/',
|
||||
key
|
||||
);
|
||||
|
||||
const imageTotalSize = await ImageProxy.getDirectorySize(cacheDirectory);
|
||||
const imageCount = await ImageProxy.getImageCount(cacheDirectory);
|
||||
@@ -259,7 +263,7 @@ class ImageProxy {
|
||||
}
|
||||
|
||||
private getCacheDirectory() {
|
||||
return path.join(baseCacheDirectory, this.key);
|
||||
return path.join(__dirname, '../../config/cache/images/', this.key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -264,8 +264,7 @@ export type JobId =
|
||||
| 'download-sync-reset'
|
||||
| 'jellyfin-recently-added-sync'
|
||||
| 'jellyfin-full-sync'
|
||||
| 'image-cache-cleanup'
|
||||
| 'availability-sync';
|
||||
| 'image-cache-cleanup';
|
||||
|
||||
interface AllSettings {
|
||||
clientId: string;
|
||||
@@ -436,9 +435,6 @@ class Settings {
|
||||
'sonarr-scan': {
|
||||
schedule: '0 30 4 * * *',
|
||||
},
|
||||
'availability-sync': {
|
||||
schedule: '0 0 5 * * *',
|
||||
},
|
||||
'download-sync': {
|
||||
schedule: '0 * * * * *',
|
||||
},
|
||||
@@ -594,7 +590,7 @@ class Settings {
|
||||
}
|
||||
|
||||
private generateApiKey(): string {
|
||||
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
|
||||
return Buffer.from(`${Date.now()}${randomUUID()})`).toString('base64');
|
||||
}
|
||||
|
||||
private generateVapidKeys(force = false): void {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
const clearCookies: Middleware = (_req, res, next) => {
|
||||
res.removeHeader('Set-Cookie');
|
||||
next();
|
||||
};
|
||||
|
||||
export default clearCookies;
|
||||
@@ -800,12 +800,12 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
|
||||
discoverRoutes.get<{ page?: number }, WatchlistResponse>(
|
||||
'/watchlist',
|
||||
async (req, res) => {
|
||||
const userRepository = getRepository(User);
|
||||
const itemsPerPage = 20;
|
||||
const page = Number(req.query.page) ?? 1;
|
||||
const page = req.params.page ?? 1;
|
||||
const offset = (page - 1) * itemsPerPage;
|
||||
|
||||
const activeUser = await userRepository.findOne({
|
||||
@@ -829,8 +829,8 @@ discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
|
||||
|
||||
return res.json({
|
||||
page,
|
||||
totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
|
||||
totalResults: watchlist.totalSize,
|
||||
totalPages: Math.ceil(watchlist.size / itemsPerPage),
|
||||
totalResults: watchlist.size,
|
||||
results: watchlist.items.map((item) => ({
|
||||
ratingKey: item.ratingKey,
|
||||
title: item.title,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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';
|
||||
@@ -171,100 +168,6 @@ 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),
|
||||
|
||||
@@ -685,7 +685,7 @@ router.get<{ id: string }, UserWatchDataResponse>(
|
||||
}
|
||||
);
|
||||
|
||||
router.get<{ id: string }, WatchlistResponse>(
|
||||
router.get<{ id: string; page?: number }, WatchlistResponse>(
|
||||
'/:id/watchlist',
|
||||
async (req, res, next) => {
|
||||
if (
|
||||
@@ -705,7 +705,7 @@ router.get<{ id: string }, WatchlistResponse>(
|
||||
}
|
||||
|
||||
const itemsPerPage = 20;
|
||||
const page = Number(req.query.page) ?? 1;
|
||||
const page = req.params.page ?? 1;
|
||||
const offset = (page - 1) * itemsPerPage;
|
||||
|
||||
const user = await getRepository(User).findOneOrFail({
|
||||
@@ -729,8 +729,8 @@ router.get<{ id: string }, WatchlistResponse>(
|
||||
|
||||
return res.json({
|
||||
page,
|
||||
totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
|
||||
totalResults: watchlist.totalSize,
|
||||
totalPages: Math.ceil(watchlist.size / itemsPerPage),
|
||||
totalResults: watchlist.size,
|
||||
results: watchlist.items.map((item) => ({
|
||||
ratingKey: item.ratingKey,
|
||||
title: item.title,
|
||||
|
||||
@@ -10,7 +10,6 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import type { Collection } from '@server/models/Collection';
|
||||
@@ -40,19 +39,6 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
||||
const [requestModal, setRequestModal] = useState(false);
|
||||
const [is4k, setIs4k] = useState(false);
|
||||
|
||||
const returnCollectionDownloadItems = (data: Collection | undefined) => {
|
||||
const [downloadStatus, downloadStatus4k] = [
|
||||
data?.parts.flatMap((item) =>
|
||||
item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : []
|
||||
),
|
||||
data?.parts.flatMap((item) =>
|
||||
item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : []
|
||||
),
|
||||
];
|
||||
|
||||
return { downloadStatus, downloadStatus4k };
|
||||
};
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
@@ -60,19 +46,21 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
||||
} = useSWR<Collection>(`/api/v1/collection/${router.query.collectionId}`, {
|
||||
fallbackData: collection,
|
||||
revalidateOnMount: true,
|
||||
refreshInterval: refreshIntervalHelper(
|
||||
returnCollectionDownloadItems(collection),
|
||||
15000
|
||||
),
|
||||
});
|
||||
|
||||
const { data: genres } =
|
||||
useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
|
||||
|
||||
const [downloadStatus, downloadStatus4k] = useMemo(() => {
|
||||
const downloadItems = returnCollectionDownloadItems(data);
|
||||
return [downloadItems.downloadStatus, downloadItems.downloadStatus4k];
|
||||
}, [data]);
|
||||
return [
|
||||
data?.parts.flatMap((item) =>
|
||||
item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : []
|
||||
),
|
||||
data?.parts.flatMap((item) =>
|
||||
item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : []
|
||||
),
|
||||
];
|
||||
}, [data?.parts]);
|
||||
|
||||
const [titles, titles4k] = useMemo(() => {
|
||||
return [
|
||||
|
||||
@@ -101,12 +101,12 @@ const ButtonWithDropdown = ({
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={isOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
enter="transition ease-out duration-100 opacity-0"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75 opacity-100"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div className="absolute right-0 z-40 mt-2 -mr-1 w-56 origin-top-right rounded-md shadow-lg">
|
||||
<div
|
||||
|
||||
@@ -78,10 +78,10 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
appear
|
||||
as="div"
|
||||
className="fixed top-0 bottom-0 left-0 right-0 z-50 flex h-full w-full items-center justify-center bg-gray-800 bg-opacity-70"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
ref={parentRef}
|
||||
@@ -89,10 +89,10 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
<Transition
|
||||
appear
|
||||
as={Fragment}
|
||||
enter="transition duration-300"
|
||||
enter="transition opacity-0 duration-300 transform scale-75"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={loading}
|
||||
@@ -102,7 +102,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition
|
||||
className="hide-scrollbar relative inline-block w-full overflow-auto bg-gray-800 px-4 pt-4 pb-4 text-left align-bottom shadow-xl ring-1 ring-gray-700 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle"
|
||||
className="hide-scrollbar relative inline-block w-full transform overflow-auto bg-gray-800 px-4 pt-4 pb-4 text-left align-bottom shadow-xl ring-1 ring-gray-700 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline"
|
||||
@@ -111,10 +111,10 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
}}
|
||||
appear
|
||||
as="div"
|
||||
enter="transition duration-300"
|
||||
enter="transition opacity-0 duration-300 transform scale-75"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={!loading}
|
||||
|
||||
@@ -29,7 +29,7 @@ const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => {
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
checked ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -37,10 +37,10 @@ const SlideOver = ({
|
||||
as={Fragment}
|
||||
show={show}
|
||||
appear
|
||||
enter="transition-opacity ease-in-out duration-300"
|
||||
enter="opacity-0 transition ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in-out duration-300"
|
||||
leave="opacity-100 transition ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
@@ -58,16 +58,16 @@ const SlideOver = ({
|
||||
<section className="absolute inset-y-0 right-0 flex max-w-full">
|
||||
<Transition.Child
|
||||
appear
|
||||
enter="transition-transform ease-in-out duration-500 sm:duration-700"
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform ease-in-out duration-500 sm:duration-700"
|
||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className="slideover relative h-full w-screen max-w-md p-2 sm:p-3"
|
||||
className="slideover relative h-full w-screen max-w-md p-2 sm:p-4"
|
||||
ref={slideoverRef}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
@@ -165,10 +165,10 @@ const Discover = () => {
|
||||
</Transition>
|
||||
<Transition
|
||||
show={isEditing}
|
||||
enter="transition duration-300"
|
||||
enter="transition transform duration-300"
|
||||
enterFrom="opacity-0 translate-y-6"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition duration-300"
|
||||
leave="transition duration-300 transform"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-6"
|
||||
className="safe-shift-edit-menu fixed right-0 left-0 z-50 flex flex-col items-center justify-end space-x-0 space-y-2 border-t border-gray-700 bg-gray-800 bg-opacity-80 p-4 backdrop-blur sm:bottom-0 sm:flex-row sm:space-y-0 sm:space-x-3"
|
||||
|
||||
@@ -65,10 +65,10 @@ const IssueComment = ({
|
||||
>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-300"
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={showDeleteModal}
|
||||
@@ -115,11 +115,11 @@ const IssueComment = ({
|
||||
as={Fragment}
|
||||
show={open}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
@@ -164,7 +164,7 @@ const IssueComment = ({
|
||||
</Menu>
|
||||
)}
|
||||
<div
|
||||
className={`absolute top-3 z-10 h-3 w-3 rotate-45 bg-gray-800 shadow ring-1 ring-gray-500 ${
|
||||
className={`absolute top-3 z-10 h-3 w-3 rotate-45 transform bg-gray-800 shadow ring-1 ring-gray-500 ${
|
||||
isReversed ? '-left-1' : '-right-1'
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -57,11 +57,11 @@ const IssueDescription = ({
|
||||
show={open}
|
||||
as="div"
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
|
||||
@@ -187,10 +187,10 @@ const IssueDetails = () => {
|
||||
<PageTitle title={[intl.formatMessage(messages.issuepagetitle), title]} />
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={showDeleteModal}
|
||||
|
||||
@@ -12,10 +12,10 @@ interface IssueModalProps {
|
||||
const IssueModal = ({ show, mediaType, onCancel, tmdbId }: IssueModalProps) => (
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
|
||||
@@ -34,12 +34,12 @@ const LanguagePicker = () => {
|
||||
<Transition
|
||||
as="div"
|
||||
show={isDropdownOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
enter="transition ease-out duration-100 opacity-0"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75 opacity-100"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className="absolute right-0 mt-2 w-56 origin-top-right rounded-md shadow-lg"
|
||||
|
||||
@@ -131,13 +131,13 @@ const MobileMenu = () => {
|
||||
show={isOpen}
|
||||
as="div"
|
||||
ref={ref}
|
||||
enter="transition duration-500"
|
||||
enter="transition transform duration-500"
|
||||
enterFrom="opacity-0 translate-y-0"
|
||||
enterTo="opacity-100 -translate-y-full"
|
||||
leave="transition duration-500"
|
||||
leave="transition duration-500 transform"
|
||||
leaveFrom="opacity-100 -translate-y-full"
|
||||
leaveTo="opacity-0 translate-y-0"
|
||||
className="absolute top-0 left-0 right-0 flex w-full -translate-y-full flex-col space-y-6 border-t border-gray-600 bg-gray-900 bg-opacity-90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
|
||||
className="absolute top-0 left-0 right-0 flex w-full -translate-y-full transform flex-col space-y-6 border-t border-gray-600 bg-gray-900 bg-opacity-90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
|
||||
>
|
||||
{filteredLinks.map((link) => {
|
||||
const isActive = router.pathname.match(link.activeRegExp);
|
||||
|
||||
@@ -128,10 +128,10 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as="div"
|
||||
enter="transition-transform ease-in-out duration-300"
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform ease-in-out duration-300"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
|
||||
@@ -63,11 +63,11 @@ const UserDropdown = () => {
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
appear
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-72 origin-top-right rounded-md shadow-lg">
|
||||
|
||||
@@ -100,10 +100,10 @@ const Login = () => {
|
||||
<Transition
|
||||
as="div"
|
||||
show={!!error}
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||
import SlideOver from '@app/components/Common/SlideOver';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import DownloadBlock from '@app/components/DownloadBlock';
|
||||
import IssueBlock from '@app/components/IssueBlock';
|
||||
import RequestBlock from '@app/components/RequestBlock';
|
||||
@@ -9,20 +8,11 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { Bars4Icon, ServerIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
DocumentMinusIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { CheckCircleIcon, DocumentMinusIcon } from '@heroicons/react/24/solid';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { MediaRequestStatus, MediaStatus } 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';
|
||||
@@ -42,12 +32,8 @@ const messages = defineMessages({
|
||||
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.',
|
||||
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',
|
||||
@@ -102,12 +88,6 @@ 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) {
|
||||
@@ -116,35 +96,6 @@ 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`, {
|
||||
@@ -198,24 +149,20 @@ const ManageSlideOver = ({
|
||||
<div className="overflow-hidden rounded-md border border-gray-700 shadow">
|
||||
<ul>
|
||||
{data.mediaInfo?.downloadStatus?.map((status, index) => (
|
||||
<Tooltip
|
||||
<li
|
||||
key={`dl-status-${status.externalId}-${index}`}
|
||||
content={status.title}
|
||||
className="border-b border-gray-700 last:border-b-0"
|
||||
>
|
||||
<li className="border-b border-gray-700 last:border-b-0">
|
||||
<DownloadBlock downloadItem={status} />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<DownloadBlock downloadItem={status} />
|
||||
</li>
|
||||
))}
|
||||
{data.mediaInfo?.downloadStatus4k?.map((status, index) => (
|
||||
<Tooltip
|
||||
<li
|
||||
key={`dl-status-${status.externalId}-${index}`}
|
||||
content={status.title}
|
||||
className="border-b border-gray-700 last:border-b-0"
|
||||
>
|
||||
<li className="border-b border-gray-700 last:border-b-0">
|
||||
<DownloadBlock downloadItem={status} is4k />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<DownloadBlock downloadItem={status} is4k />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -381,40 +328,6 @@ 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>
|
||||
)}
|
||||
@@ -520,54 +433,21 @@ 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>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,6 @@ import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
CloudIcon,
|
||||
@@ -117,13 +116,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
mutate: revalidate,
|
||||
} = useSWR<MovieDetailsType>(`/api/v1/movie/${router.query.movieId}`, {
|
||||
fallbackData: movie,
|
||||
refreshInterval: refreshIntervalHelper(
|
||||
{
|
||||
downloadStatus: movie?.mediaInfo?.downloadStatus,
|
||||
downloadStatus4k: movie?.mediaInfo?.downloadStatus4k,
|
||||
},
|
||||
15000
|
||||
),
|
||||
});
|
||||
|
||||
const { data: ratingData } = useSWR<RTRating>(
|
||||
|
||||
@@ -122,7 +122,7 @@ const RegionSelector = ({
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
leave="transition-opacity ease-in duration-100"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
className="absolute mt-1 w-full rounded-md bg-gray-800 shadow-lg"
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { withProperties } from '@app/utils/typeHelpers';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
@@ -221,7 +220,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
request.type === 'movie'
|
||||
? `/api/v1/movie/${request.media.tmdbId}`
|
||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||
inView ? `${url}` : null
|
||||
);
|
||||
@@ -231,13 +229,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
mutate: revalidate,
|
||||
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||
fallbackData: request,
|
||||
refreshInterval: refreshIntervalHelper(
|
||||
{
|
||||
downloadStatus: request.media.downloadStatus,
|
||||
downloadStatus4k: request.media.downloadStatus4k,
|
||||
},
|
||||
15000
|
||||
),
|
||||
});
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
CheckIcon,
|
||||
@@ -294,13 +293,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
`/api/v1/request/${request.id}`,
|
||||
{
|
||||
fallbackData: request,
|
||||
refreshInterval: refreshIntervalHelper(
|
||||
{
|
||||
downloadStatus: request.media.downloadStatus,
|
||||
downloadStatus4k: request.media.downloadStatus4k,
|
||||
},
|
||||
15000
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -582,10 +582,10 @@ const AdvancedRequester = ({
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition-opacity ease-in duration-300"
|
||||
enter="transition ease-in duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in duration-100"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
className="mt-1 w-full rounded-md border border-gray-700 bg-gray-800 shadow-lg"
|
||||
|
||||
@@ -324,7 +324,7 @@ const CollectionRequestModal = ({
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllParts() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
@@ -389,7 +389,7 @@ const CollectionRequestModal = ({
|
||||
isSelectedPart(part.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -540,7 +540,7 @@ const TvRequestModal = ({
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
@@ -631,7 +631,7 @@ const TvRequestModal = ({
|
||||
isSelectedSeason(season.seasonNumber)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -29,10 +29,10 @@ const RequestModal = ({
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
|
||||
@@ -32,7 +32,7 @@ const LibraryItem = ({ isEnabled, name, onToggle }: LibraryItemProps) => {
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
} relative inline-block h-5 w-5 rounded-full bg-white shadow transition duration-200 ease-in-out`}
|
||||
} relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
|
||||
@@ -214,10 +214,10 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
||||
as="div"
|
||||
appear
|
||||
show
|
||||
enter="transition-opacity ease-in-out duration-300"
|
||||
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in-out duration-300"
|
||||
enterTo="opacuty-100"
|
||||
leave="transition ease-in-out duration-300 transform opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
|
||||
@@ -63,10 +63,10 @@ const Release = ({ currentVersion, release, isLatest }: ReleaseProps) => {
|
||||
<div className="flex w-full flex-col space-y-3 rounded-md bg-gray-800 px-4 py-2 shadow-md ring-1 ring-gray-700 sm:flex-row sm:space-y-0 sm:space-x-3">
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={isModalOpen}
|
||||
|
||||
@@ -57,7 +57,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'plex-watchlist-sync': 'Plex Watchlist Sync',
|
||||
'jellyfin-recently-added-sync': 'Jellyfin Recently Added Scan',
|
||||
'jellyfin-full-sync': 'Jellyfin Full Library Scan',
|
||||
'availability-sync': 'Media Availability Sync',
|
||||
'radarr-scan': 'Radarr Scan',
|
||||
'sonarr-scan': 'Sonarr Scan',
|
||||
'download-sync': 'Download Sync',
|
||||
@@ -72,8 +71,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
||||
editJobScheduleSelectorMinutes:
|
||||
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
|
||||
editJobScheduleSelectorSeconds:
|
||||
'Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}',
|
||||
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>.',
|
||||
@@ -85,7 +82,7 @@ interface Job {
|
||||
id: JobId;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
|
||||
interval: 'short' | 'long' | 'fixed';
|
||||
cronSchedule: string;
|
||||
nextExecutionTime: string;
|
||||
running: boolean;
|
||||
@@ -96,11 +93,10 @@ type JobModalState = {
|
||||
job?: Job;
|
||||
scheduleHours: number;
|
||||
scheduleMinutes: number;
|
||||
scheduleSeconds: number;
|
||||
};
|
||||
|
||||
type JobModalAction =
|
||||
| { type: 'set'; hours?: number; minutes?: number; seconds?: number }
|
||||
| { type: 'set'; hours?: number; minutes?: number }
|
||||
| {
|
||||
type: 'close';
|
||||
}
|
||||
@@ -123,7 +119,6 @@ const jobModalReducer = (
|
||||
job: action.job,
|
||||
scheduleHours: 1,
|
||||
scheduleMinutes: 5,
|
||||
scheduleSeconds: 30,
|
||||
};
|
||||
|
||||
case 'set':
|
||||
@@ -131,7 +126,6 @@ const jobModalReducer = (
|
||||
...state,
|
||||
scheduleHours: action.hours ?? state.scheduleHours,
|
||||
scheduleMinutes: action.minutes ?? state.scheduleMinutes,
|
||||
scheduleSeconds: action.seconds ?? state.scheduleSeconds,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -159,7 +153,6 @@ const SettingsJobs = () => {
|
||||
isOpen: false,
|
||||
scheduleHours: 1,
|
||||
scheduleMinutes: 5,
|
||||
scheduleSeconds: 30,
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const settings = useSettings();
|
||||
@@ -212,11 +205,9 @@ const SettingsJobs = () => {
|
||||
const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
|
||||
|
||||
try {
|
||||
if (jobModalState.job?.interval === 'seconds') {
|
||||
jobScheduleCron.splice(0, 2, `*/${jobModalState.scheduleSeconds}`, '*');
|
||||
} else if (jobModalState.job?.interval === 'minutes') {
|
||||
if (jobModalState.job?.interval === 'short') {
|
||||
jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`;
|
||||
} else if (jobModalState.job?.interval === 'hours') {
|
||||
} else if (jobModalState.job?.interval === 'long') {
|
||||
jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`;
|
||||
} else {
|
||||
// jobs with interval: fixed should not be editable
|
||||
@@ -258,10 +249,10 @@ const SettingsJobs = () => {
|
||||
/>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={jobModalState.isOpen}
|
||||
@@ -300,30 +291,7 @@ const SettingsJobs = () => {
|
||||
{intl.formatMessage(messages.editJobSchedulePrompt)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
{jobModalState.job?.interval === 'seconds' ? (
|
||||
<select
|
||||
name="jobScheduleSeconds"
|
||||
className="inline"
|
||||
value={jobModalState.scheduleSeconds}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: 'set',
|
||||
seconds: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
>
|
||||
{[30, 45, 60].map((v) => (
|
||||
<option value={v} key={`jobScheduleSeconds-${v}`}>
|
||||
{intl.formatMessage(
|
||||
messages.editJobScheduleSelectorSeconds,
|
||||
{
|
||||
jobScheduleSeconds: v,
|
||||
}
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : jobModalState.job?.interval === 'minutes' ? (
|
||||
{jobModalState.job?.interval === 'short' ? (
|
||||
<select
|
||||
name="jobScheduleMinutes"
|
||||
className="inline"
|
||||
|
||||
@@ -143,10 +143,10 @@ const SettingsLogs = () => {
|
||||
/>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
appear
|
||||
|
||||
@@ -247,10 +247,10 @@ const SettingsServices = () => {
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={deleteServerModal.open}
|
||||
enter="transition-opacity ease-in-out duration-300"
|
||||
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in-out duration-300"
|
||||
enterTo="opacuty-100"
|
||||
leave="transition ease-in-out duration-300 transform opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
|
||||
@@ -223,10 +223,10 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
as="div"
|
||||
appear
|
||||
show
|
||||
enter="transition-opacity ease-in-out duration-300"
|
||||
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in-out duration-300"
|
||||
enterTo="opacuty-100"
|
||||
leave="transition ease-in-out duration-300 transform opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
|
||||
@@ -44,10 +44,10 @@ const StatusChecker = () => {
|
||||
return (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
appear
|
||||
|
||||
@@ -141,7 +141,7 @@ const TitleCard = ({
|
||||
: intl.formatMessage(globalMessages.tvshow)}
|
||||
</div>
|
||||
</div>
|
||||
{currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
|
||||
{currentStatus && (
|
||||
<div className="pointer-events-none z-40 flex items-center">
|
||||
<StatusBadgeMini
|
||||
status={currentStatus}
|
||||
@@ -154,10 +154,10 @@ const TitleCard = ({
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={isUpdating}
|
||||
enter="transition-opacity ease-in-out duration-300"
|
||||
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in-out duration-300"
|
||||
leave="transition ease-in-out duration-300 transform opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
@@ -169,10 +169,10 @@ const TitleCard = ({
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={!image || showDetail || showRequestModal}
|
||||
enter="transition-opacity"
|
||||
enter="transition transform opacity-0"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity"
|
||||
leave="transition transform opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
|
||||
@@ -30,7 +30,6 @@ import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { Disclosure, Transition } from '@headlessui/react';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
@@ -113,13 +112,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
mutate: revalidate,
|
||||
} = useSWR<TvDetailsType>(`/api/v1/tv/${router.query.tvId}`, {
|
||||
fallbackData: tv,
|
||||
refreshInterval: refreshIntervalHelper(
|
||||
{
|
||||
downloadStatus: tv?.mediaInfo?.downloadStatus,
|
||||
downloadStatus4k: tv?.mediaInfo?.downloadStatus4k,
|
||||
},
|
||||
15000
|
||||
),
|
||||
});
|
||||
|
||||
const { data: ratingData } = useSWR<RTRating>(
|
||||
@@ -767,18 +759,18 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : ''
|
||||
open ? 'rotate-180 transform' : ''
|
||||
} h-6 w-6 text-gray-500`}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition-opacity duration-100 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-75 ease-out"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
// Not sure why this transition is adding a margin without this here
|
||||
style={{ margin: '0px' }}
|
||||
>
|
||||
|
||||
@@ -155,7 +155,7 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
@@ -194,7 +194,7 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
|
||||
isSelectedUser(user.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -233,10 +233,10 @@ const UserList = () => {
|
||||
<PageTitle title={intl.formatMessage(messages.users)} />
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={deleteModal.isOpen}
|
||||
@@ -262,10 +262,10 @@ const UserList = () => {
|
||||
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={createModal.isOpen}
|
||||
@@ -445,10 +445,10 @@ const UserList = () => {
|
||||
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={showBulkEditModal}
|
||||
@@ -466,10 +466,10 @@ const UserList = () => {
|
||||
|
||||
<Transition
|
||||
as="div"
|
||||
enter="transition-opacity duration-300"
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-300"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={showImportModal}
|
||||
|
||||
@@ -24,7 +24,6 @@ export type AvailableLocale =
|
||||
| 'sq'
|
||||
| 'sr'
|
||||
| 'sv'
|
||||
| 'ua'
|
||||
| 'zh-CN'
|
||||
| 'zh-TW';
|
||||
|
||||
@@ -126,10 +125,6 @@ export const availableLanguages: AvailableLanguageObject = {
|
||||
code: 'ja',
|
||||
display: '日本語',
|
||||
},
|
||||
ua: {
|
||||
code: 'ua',
|
||||
display: 'українська',
|
||||
},
|
||||
'zh-TW': {
|
||||
code: 'zh-TW',
|
||||
display: '繁體中文',
|
||||
|
||||
@@ -631,7 +631,6 @@
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||
"components.Settings.SettingsAbout.uptodate": "Up to Date",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsJobsCache.availability-sync": "Media Availability Sync",
|
||||
"components.Settings.SettingsJobsCache.cache": "Cache",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} cache flushed.",
|
||||
@@ -650,7 +649,6 @@
|
||||
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "New Frequency",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
|
||||
"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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,995 +0,0 @@
|
||||
{
|
||||
"components.Discover.discovermovies": "Популярні фільми",
|
||||
"components.Discover.discovertv": "Популярні серіали",
|
||||
"components.Discover.popularmovies": "Популярні фільми",
|
||||
"components.Discover.populartv": "Популярні серіали",
|
||||
"components.Discover.recentlyAdded": "Нещодавно додані",
|
||||
"components.Discover.recentrequests": "Останні запити",
|
||||
"components.Discover.trending": "У трендах",
|
||||
"components.Discover.upcoming": "Майбутні фільми",
|
||||
"components.Discover.upcomingmovies": "Майбутні фільми",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Пошук фільмів та серіалів",
|
||||
"components.Layout.Sidebar.dashboard": "Знайти щось нове",
|
||||
"components.Layout.Sidebar.requests": "Запити",
|
||||
"components.Layout.Sidebar.settings": "Налаштування",
|
||||
"components.Layout.Sidebar.users": "Користувачі",
|
||||
"components.Layout.UserDropdown.signout": "Вихід",
|
||||
"components.MovieDetails.budget": "Бюджет",
|
||||
"components.MovieDetails.cast": "У ролях",
|
||||
"components.MovieDetails.originallanguage": "Мова оригіналу",
|
||||
"components.MovieDetails.overview": "Огляд",
|
||||
"components.MovieDetails.overviewunavailable": "Огляд недоступний.",
|
||||
"components.MovieDetails.recommendations": "Рекомендації",
|
||||
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Дата релізу} other {Дати релізу}}",
|
||||
"components.MovieDetails.revenue": "Дохід",
|
||||
"components.MovieDetails.runtime": "{minutes} хвилин",
|
||||
"components.MovieDetails.similar": "Схожі фільми",
|
||||
"components.PersonDetails.appearsin": "Появи у фільмах та серіалах",
|
||||
"components.PersonDetails.ascharacter": "в ролі {character}",
|
||||
"components.RequestBlock.seasons": "{seasonCount, plural, one {Сезон} other {Сезони}}",
|
||||
"components.RequestCard.seasons": "{seasonCount, plural, one {Сезон} other {Сезони}}",
|
||||
"components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Сезон} other {Сезони}}",
|
||||
"components.RequestList.requests": "Запити",
|
||||
"components.RequestModal.cancel": "Скасувати запит",
|
||||
"components.RequestModal.extras": "Додатково",
|
||||
"components.RequestModal.numberofepisodes": "# епізодів",
|
||||
"components.RequestModal.pendingrequest": "",
|
||||
"components.RequestModal.requestCancel": "Запит на <strong>{title}</strong> скасовано.",
|
||||
"components.RequestModal.requestSuccess": "<strong>{title}</strong> успішно запрошений!",
|
||||
"components.RequestModal.requestadmin": "Цей запит буде схвалено автоматично.",
|
||||
"components.RequestModal.requestfrom": "Запит користувача {username} очікує схвалення.",
|
||||
"components.RequestModal.requestseasons": "Запросити {seasonCount} {seasonCount, plural, one {сезон} other {сезону(ів)}}",
|
||||
"components.RequestModal.season": "Сезон",
|
||||
"components.RequestModal.seasonnumber": "Сезон {number}",
|
||||
"components.RequestModal.selectseason": "Виберіть сезон(и)",
|
||||
"components.Search.searchresults": "Результати пошуку",
|
||||
"components.Settings.Notifications.agentenabled": "Активувати службу",
|
||||
"components.Settings.Notifications.authPass": "Пароль SMTP",
|
||||
"components.Settings.Notifications.authUser": "Ім'я користувача SMTP",
|
||||
"components.Settings.Notifications.emailsender": "Адреса відправника",
|
||||
"components.Settings.Notifications.smtpHost": "SMTP-хост",
|
||||
"components.Settings.Notifications.smtpPort": "SMTP порт",
|
||||
"components.Settings.Notifications.validationSmtpHostRequired": "Ви повинні вказати дійсне ім'я хоста або IP-адресу",
|
||||
"components.Settings.Notifications.validationSmtpPortRequired": "Ви повинні вказати дійсний номер порту",
|
||||
"components.Settings.Notifications.webhookUrl": "URL веб-перехоплювача",
|
||||
"components.Settings.RadarrModal.add": "Додати сервер",
|
||||
"components.Settings.RadarrModal.apiKey": "Ключ API",
|
||||
"components.Settings.RadarrModal.baseUrl": "Базовий URL",
|
||||
"components.Settings.RadarrModal.createradarr": "Додати новий сервер Radarr",
|
||||
"components.Settings.RadarrModal.defaultserver": "Сервер за замовчуванням",
|
||||
"components.Settings.RadarrModal.editradarr": "Редагувати сервер Radarr",
|
||||
"components.Settings.RadarrModal.hostname": "Ім'я хоста або IP-адреса",
|
||||
"components.Settings.RadarrModal.minimumAvailability": "Мінімальна доступність",
|
||||
"components.Settings.RadarrModal.port": "Порт",
|
||||
"components.Settings.RadarrModal.qualityprofile": "Профіль якості",
|
||||
"components.Settings.RadarrModal.rootfolder": "Кореневий каталог",
|
||||
"components.Settings.RadarrModal.selectMinimumAvailability": "Виберіть мінімальну доступність",
|
||||
"components.Settings.RadarrModal.selectQualityProfile": "Виберіть профіль якості",
|
||||
"components.Settings.RadarrModal.selectRootFolder": "Виберіть кореневий каталог",
|
||||
"components.Settings.RadarrModal.server4k": "4К сервер",
|
||||
"components.Settings.RadarrModal.servername": "Назва сервера",
|
||||
"components.Settings.RadarrModal.ssl": "Використовувати SSL",
|
||||
"components.Settings.RadarrModal.toastRadarrTestFailure": "Не вдалося підключитися до Radarr.",
|
||||
"components.Settings.RadarrModal.toastRadarrTestSuccess": "З'єднання з Radarr встановлено успішно!",
|
||||
"components.Settings.RadarrModal.validationApiKeyRequired": "Ви повинні надати ключ API",
|
||||
"components.Settings.RadarrModal.validationHostnameRequired": "Ви повинні вказати дійсне ім'я хоста або IP-адресу",
|
||||
"components.Settings.RadarrModal.validationPortRequired": "Ви повинні вказати дійсний номер порту",
|
||||
"components.Settings.RadarrModal.validationProfileRequired": "Ви повинні вибрати профіль якості",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Ви повинні вибрати кореневий каталог",
|
||||
"components.Settings.SonarrModal.add": "Додати сервер",
|
||||
"components.Settings.SonarrModal.apiKey": "Ключ API",
|
||||
"components.Settings.SonarrModal.baseUrl": "Базовий URL",
|
||||
"components.Settings.SonarrModal.createsonarr": "Додати новий сервер Sonarr",
|
||||
"components.Settings.SonarrModal.defaultserver": "Сервер за замовчуванням",
|
||||
"components.Settings.SonarrModal.editsonarr": "Редагувати сервер Sonarr",
|
||||
"components.Settings.SonarrModal.hostname": "Ім'я хоста або IP-адреса",
|
||||
"components.Settings.SonarrModal.port": "Порт",
|
||||
"components.Settings.SonarrModal.qualityprofile": "Профіль якості",
|
||||
"components.Settings.SonarrModal.rootfolder": "Кореневий каталог",
|
||||
"components.Settings.SonarrModal.seasonfolders": "Папки для сезонів",
|
||||
"components.Settings.SonarrModal.selectQualityProfile": "Виберіть профіль якості",
|
||||
"components.Settings.SonarrModal.selectRootFolder": "Виберіть кореневий каталог",
|
||||
"components.Settings.SonarrModal.server4k": "4К сервер",
|
||||
"components.Settings.SonarrModal.servername": "Назва сервера",
|
||||
"components.Settings.SonarrModal.ssl": "Використовувати SSL",
|
||||
"components.Settings.SonarrModal.validationApiKeyRequired": "Ви повинні надати ключ API",
|
||||
"components.Settings.SonarrModal.validationHostnameRequired": "Ви повинні вказати дійсне ім'я хоста або IP-адресу",
|
||||
"components.Settings.SonarrModal.validationPortRequired": "Ви повинні вказати дійсний номер порту",
|
||||
"components.Settings.SonarrModal.validationProfileRequired": "Ви повинні вибрати профіль якості",
|
||||
"components.Settings.SonarrModal.validationRootFolderRequired": "Ви повинні вибрати кореневий каталог",
|
||||
"components.Settings.activeProfile": "Активний профіль",
|
||||
"components.Settings.addradarr": "Додати сервер Radarr",
|
||||
"components.Settings.address": "Адреса",
|
||||
"components.Settings.addsonarr": "Додати сервер Sonarr",
|
||||
"components.Settings.apikey": "Ключ API",
|
||||
"components.Settings.applicationurl": "URL-адреса програми",
|
||||
"components.Settings.cancelscan": "Скасувати сканування",
|
||||
"components.Settings.copied": "Ключ API скопійовано в буфер обміну.",
|
||||
"components.Settings.currentlibrary": "Поточна бібліотека: {name}",
|
||||
"components.Settings.default": "За замовчуванням",
|
||||
"components.Settings.default4k": "4К за замовчуванням",
|
||||
"components.Settings.deleteserverconfirm": "Ви впевнені, що хочете видалити цей сервер?",
|
||||
"components.Settings.generalsettings": "Загальні налаштування",
|
||||
"components.Settings.generalsettingsDescription": "Налаштуйте глобальні параметри та параметри за промовчанням для Jellyseerr.",
|
||||
"components.Settings.hostname": "Ім'я хоста або IP-адреса",
|
||||
"components.Settings.librariesRemaining": "Залишилось бібліотек: {count}",
|
||||
"components.Settings.manualscan": "Сканувати бібліотеки вручну",
|
||||
"components.Settings.manualscanDescription": "Зазвичай виконується раз на 24 години. Jellyseerr виконає більш агресивну перевірку вашого сервера Plex на предмет нещодавно доданих мультимедіа. Якщо ви вперше налаштовуєте Plex, рекомендується виконати одноразове повне сканування бібліотек вручну!",
|
||||
"components.Settings.menuAbout": "Про проект",
|
||||
"components.Settings.menuGeneralSettings": "Спільне",
|
||||
"components.Settings.menuJobs": "Завдання та кеш",
|
||||
"components.Settings.menuLogs": "Логи",
|
||||
"components.Settings.menuNotifications": "Сповіщення",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuServices": "Служби",
|
||||
"components.Settings.notificationsettings": "Налаштування повідомлень",
|
||||
"components.Settings.notrunning": "Не працює",
|
||||
"components.Settings.plexlibraries": "Бібліотеки Plex",
|
||||
"components.Settings.plexlibrariesDescription": "Бібліотеки, які Jellyseerr сканує на наявність мультимедіа. Налаштуйте та збережіть параметри підключення Plex, потім натисніть кнопку нижче, якщо список бібліотек порожній.",
|
||||
"components.Settings.plexsettings": "Налаштування Plex",
|
||||
"components.Settings.plexsettingsDescription": "Налаштуйте параметри вашого сервера Plex. Jellyseerr сканує ваші бібліотеки Plex, щоб визначити доступність контенту.",
|
||||
"components.Settings.port": "Порт",
|
||||
"components.Settings.radarrsettings": "Налаштування Radarr",
|
||||
"components.Settings.sonarrsettings": "Налаштування Sonarr",
|
||||
"components.Settings.ssl": "SSL",
|
||||
"components.Settings.startscan": "Почати сканування",
|
||||
"components.Setup.configureplex": "Налаштуйте Plex",
|
||||
"components.Setup.configureservices": "Налаштуйте служби",
|
||||
"components.Setup.continue": "Продовжити",
|
||||
"components.Setup.finish": "Завершити налаштування",
|
||||
"components.Setup.finishing": "Завершення…",
|
||||
"components.Setup.loginwithplex": "Увійти за допомогою Plex",
|
||||
"components.Setup.signinMessage": "Почніть з входу в систему за допомогою облікового запису Plex",
|
||||
"components.Setup.welcome": "Ласкаво просимо до Jellyseerr",
|
||||
"components.TvDetails.cast": "У ролях",
|
||||
"components.TvDetails.originallanguage": "Мова оригіналу",
|
||||
"components.TvDetails.overview": "Огляд",
|
||||
"components.TvDetails.overviewunavailable": "Огляд недоступний.",
|
||||
"components.TvDetails.recommendations": "Рекомендації",
|
||||
"components.TvDetails.similar": "Схожі серіали",
|
||||
"components.UserList.admin": "Адміністратор",
|
||||
"components.UserList.created": "Приєднався",
|
||||
"components.UserList.plexuser": "Користувач Plex",
|
||||
"components.UserList.role": "Роль",
|
||||
"components.UserList.totalrequests": "Запитів",
|
||||
"components.UserList.user": "Користувач",
|
||||
"components.UserList.userlist": "Список користувачів",
|
||||
"i18n.approve": "Схвалити",
|
||||
"i18n.approved": "Схвалений",
|
||||
"i18n.available": "Доступний",
|
||||
"i18n.cancel": "Скасувати",
|
||||
"i18n.decline": "Відхилити",
|
||||
"i18n.declined": "Відхилений",
|
||||
"i18n.delete": "Видалити",
|
||||
"i18n.movies": "Фільми",
|
||||
"i18n.partiallyavailable": "Доступний частково",
|
||||
"i18n.pending": "Чекаючи",
|
||||
"i18n.processing": "В обробці",
|
||||
"i18n.tvshows": "Серіали",
|
||||
"i18n.unavailable": "Недоступний",
|
||||
"pages.oops": "Упс",
|
||||
"pages.returnHome": "Повернутись додому",
|
||||
"components.CollectionDetails.overview": "Огляд",
|
||||
"components.CollectionDetails.numberofmovies": "{count} фільмів",
|
||||
"components.CollectionDetails.requestcollection": "Запросити Колекцію",
|
||||
"components.Login.email": "Адреса електронної пошти",
|
||||
"components.UserList.users": "Користувачі",
|
||||
"components.UserList.userdeleted": "Користувач успішно видалено!",
|
||||
"components.UserList.usercreatedsuccess": "Користувач успішно створено!",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Усього запитів",
|
||||
"components.UserList.sortRequests": "Кількість запитів",
|
||||
"components.UserList.sortCreated": "Дата приєднання",
|
||||
"components.Login.password": "Пароль",
|
||||
"components.UserList.password": "Пароль",
|
||||
"components.UserList.localuser": "Локальний користувач",
|
||||
"i18n.edit": "Редагувати",
|
||||
"components.UserList.deleteuser": "Видалити користувача",
|
||||
"components.UserList.creating": "Створення…",
|
||||
"components.UserList.createlocaluser": "Створити локального користувача",
|
||||
"components.UserList.create": "Створити",
|
||||
"components.TvDetails.network": "{networkCount, plural, one {Телеканал} other {Телеканали}}",
|
||||
"components.TvDetails.anime": "Аніме",
|
||||
"components.StatusChacker.newversionavailable": "Оновити програму",
|
||||
"components.Settings.toastSettingsSuccess": "Налаштування успішно збережено!",
|
||||
"components.Settings.serverpresetManualMessage": "Ручне налаштування",
|
||||
"components.Settings.serverpreset": "Сервер",
|
||||
"i18n.deleting": "Видалення…",
|
||||
"components.Settings.applicationTitle": "Назва програми",
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Остання",
|
||||
"components.Settings.SettingsAbout.Releases.currentversion": "Поточна",
|
||||
"components.Settings.SonarrModal.syncEnabled": "Увімкнути сканування",
|
||||
"components.Settings.RadarrModal.syncEnabled": "Увімкнути сканування",
|
||||
"components.Settings.Notifications.sendSilentlyTip": "Надсилати повідомлення без звуку",
|
||||
"components.Settings.Notifications.telegramsettingssaved": "Налаштування сповіщень Telegram успішно збережено!",
|
||||
"components.Settings.Notifications.senderName": "Ім'я відправника",
|
||||
"components.Settings.Notifications.botAPI": "Токен авторизації бота",
|
||||
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Активувати службу",
|
||||
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Активувати службу",
|
||||
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Активувати службу",
|
||||
"components.Search.search": "Пошук",
|
||||
"components.ResetPassword.resetpassword": "Скидання пароля",
|
||||
"components.ResetPassword.password": "Пароль",
|
||||
"components.ResetPassword.confirmpassword": "Підтвердити пароль",
|
||||
"components.RequestModal.requesterror": "Щось пішло не так під час надсилання запиту.",
|
||||
"components.RequestModal.requestedited": "Запит на <strong>{title}</strong> успішно відредаговано!",
|
||||
"components.RequestModal.requestcancelled": "Запит на <strong>{title}</strong> скасовано.",
|
||||
"components.RequestModal.errorediting": "Щось пішло не так під час редагування запиту.",
|
||||
"components.RequestModal.AdvancedRequester.rootfolder": "Кореневий каталог",
|
||||
"components.RequestModal.AdvancedRequester.requestas": "Запитати як",
|
||||
"components.RequestModal.AdvancedRequester.qualityprofile": "Профіль якості",
|
||||
"components.RequestModal.AdvancedRequester.default": "{name} (за замовчуванням)",
|
||||
"components.RequestModal.AdvancedRequester.advancedoptions": "Розширені налаштування",
|
||||
"components.RequestList.sortModified": "Остання зміна",
|
||||
"components.RequestList.sortAdded": "За датою",
|
||||
"components.RequestList.showallrequests": "Показати всі запити",
|
||||
"components.RequestButton.viewrequest": "Подивитися запит",
|
||||
"i18n.retry": "Повторити",
|
||||
"i18n.requested": "Запрошений",
|
||||
"components.PermissionEdit.request4k": "Запити 4K",
|
||||
"components.PermissionEdit.request": "Запити",
|
||||
"i18n.request": "Запросити",
|
||||
"i18n.failed": "Помилка",
|
||||
"i18n.experimental": "Експериментальний параметр",
|
||||
"i18n.close": "Закрити",
|
||||
"i18n.advanced": "Для просунутих користувачів",
|
||||
"components.Settings.SonarrModal.externalUrl": "Зовнішня URL-адреса",
|
||||
"components.Settings.RadarrModal.externalUrl": "Зовнішня URL-адреса",
|
||||
"components.Settings.Notifications.sendSilently": "Надсилати без звуку",
|
||||
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Скинути до стандартних налаштувань",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.validationApplicationUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.Notifications.validationUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.RadarrModal.validationApplicationUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.SonarrModal.validationApplicationUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.Notifications.NotificationsPushover.userToken": "Ключ користувача або групи",
|
||||
"components.UserList.email": "Адреса електронної пошти",
|
||||
"components.ResetPassword.email": "Адреса електронної пошти",
|
||||
"components.Settings.SonarrModal.languageprofile": "Мовний профіль",
|
||||
"components.RequestModal.AdvancedRequester.languageprofile": "Мовний профіль",
|
||||
"components.RequestModal.AdvancedRequester.animenote": "* Цей серіал - аніме.",
|
||||
"components.RequestList.RequestItem.requested": "Запрошений",
|
||||
"components.RequestBlock.rootfolder": "Кореневий каталог",
|
||||
"components.RegionSelector.regionServerDefault": "За замовчуванням ({region})",
|
||||
"components.RegionSelector.regionDefault": "Всі регіони",
|
||||
"components.PermissionEdit.viewrequests": "Перегляд запитів",
|
||||
"components.PermissionEdit.users": "Керування користувачами",
|
||||
"components.PermissionEdit.settings": "Керування налаштуваннями",
|
||||
"components.PermissionEdit.managerequests": "Керування запитами",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.admin": "Адміністратор",
|
||||
"components.PermissionEdit.admin": "Адміністратор",
|
||||
"components.RequestBlock.profilechanged": "Профіль якості",
|
||||
"components.UserProfile.recentrequests": "Останні запити",
|
||||
"components.UserProfile.UserSettings.menuPermissions": "Дозволи",
|
||||
"components.UserProfile.UserSettings.UserPermissions.permissions": "Дозволи",
|
||||
"components.UserProfile.UserSettings.menuNotifications": "Сповіщення",
|
||||
"components.UserProfile.UserSettings.menuGeneralSettings": "Спільне",
|
||||
"components.UserProfile.UserSettings.menuChangePass": "Пароль",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Локальний користувач",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "Загальні налаштування",
|
||||
"components.UserList.sortDisplayName": "Відображуване ім'я",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Відображуване ім'я",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "Ви повинні вказати свій поточний пароль",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "Ви повинні підтвердити новий пароль",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "Пароль успішно збережений!",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.password": "Пароль",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "Новий пароль",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Поточний пароль",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Підтвердіть пароль",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Налаштування успішно збережено!",
|
||||
"components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "Дозволи успішно збережені!",
|
||||
"components.Settings.toastSettingsFailure": "Щось пішло не так при збереженні налаштувань.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Щось пішло не так при збереженні налаштувань.",
|
||||
"components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Щось пішло не так при збереженні налаштувань.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Надсилати повідомлення без звуку",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Користувач Plex",
|
||||
"components.UserList.owner": "Власник",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.owner": "Власник",
|
||||
"components.MovieDetails.markavailable": "Позначити як доступний",
|
||||
"components.StatusChacker.reloadOverseerr": "Перезавантажити",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"pages.errormessagewithcode": "{statusCode} - {error}",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.accessToken": "Токен доступу",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.validationWebhookUrl": "Ви повинні вказати дійсну URL-адресу",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.profileName": "Назва профілю",
|
||||
"components.ResetPassword.resetpasswordsuccessmessage": "Пароль скинутий успішно!",
|
||||
"components.ResetPassword.passwordreset": "Скинути пароль",
|
||||
"components.RequestModal.edit": "Редагувати запит",
|
||||
"components.RequestModal.QuotaDisplay.movie": "фільм",
|
||||
"components.RequestModal.AdvancedRequester.tags": "Теги",
|
||||
"components.RequestModal.AdvancedRequester.selecttags": "Вибрати теги",
|
||||
"components.RequestModal.AdvancedRequester.notagoptions": "Тегов немає.",
|
||||
"components.RequestModal.AdvancedRequester.folder": "{path} ({space})",
|
||||
"components.RequestList.RequestItem.requesteddate": "Запрошений",
|
||||
"components.RequestList.RequestItem.modifieduserdate": "{date} користувачем {user}",
|
||||
"components.RequestList.RequestItem.modified": "Змінено",
|
||||
"components.RequestList.RequestItem.editrequest": "Редагувати запит",
|
||||
"components.RequestList.RequestItem.deleterequest": "Видалити запит",
|
||||
"components.RequestList.RequestItem.cancelRequest": "Скасувати запит",
|
||||
"components.RequestCard.deleterequest": "Видалити запит",
|
||||
"components.PlexLoginButton.signinwithplex": "Увійти",
|
||||
"components.PersonDetails.lifespan": "{birthdate} – {deathdate}",
|
||||
"components.PersonDetails.alsoknownas": "Також відомий(а) як: {names}",
|
||||
"components.NotificationTypeSelector.notificationTypes": "Типи повідомлень",
|
||||
"components.MovieDetails.watchtrailer": "Дивитись трейлер",
|
||||
"components.MovieDetails.originaltitle": "Назва оригіналу",
|
||||
"components.Login.signinwithplex": "Використовуйте ваш обліковий запис Plex",
|
||||
"components.Login.signinheader": "Увійдіть, щоб продовжити",
|
||||
"components.Login.signin": "Увійти",
|
||||
"components.Login.forgotpassword": "Забули пароль?",
|
||||
"components.Layout.UserDropdown.settings": "Налаштування",
|
||||
"components.Layout.UserDropdown.myprofile": "Профіль",
|
||||
"components.LanguageSelector.originalLanguageDefault": "Всі мови",
|
||||
"components.LanguageSelector.languageServerDefault": "За замовчуванням ({language})",
|
||||
"components.Discover.StudioSlider.studios": "Студії",
|
||||
"components.Discover.NetworkSlider.networks": "Телеканали",
|
||||
"components.Discover.DiscoverStudio.studioMovies": "Фільми {studio}",
|
||||
"components.Discover.DiscoverMovieLanguage.languageMovies": "Фільми мовою \"{language}\"",
|
||||
"components.Discover.DiscoverMovieGenre.genreMovies": "Фільми в жанрі \"{genre}\"",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Про Jellyseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Обговорення на GitHub",
|
||||
"components.Settings.enablessl": "Використовувати SSL",
|
||||
"components.Settings.is4k": "4К",
|
||||
"components.Settings.mediaTypeMovie": "фільм",
|
||||
"components.Settings.SonarrModal.tags": "Теги",
|
||||
"components.Settings.RadarrModal.tags": "Теги",
|
||||
"i18n.testing": "Тестування…",
|
||||
"i18n.test": "Протестувати",
|
||||
"i18n.status": "Статус",
|
||||
"i18n.saving": "Збереження…",
|
||||
"i18n.previous": "Попередня",
|
||||
"i18n.next": "Наступна",
|
||||
"i18n.movie": "Фільм",
|
||||
"i18n.canceling": "Скасувати…",
|
||||
"i18n.back": "Назад",
|
||||
"i18n.all": "Всі",
|
||||
"i18n.settings": "Налаштування",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Сповіщення",
|
||||
"components.Settings.plex": "Plex",
|
||||
"components.Settings.notifications": "Сповіщення",
|
||||
"components.Settings.SettingsUsers.users": "Користувачі",
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Відновити",
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Зупинити",
|
||||
"components.Settings.SettingsLogs.message": "Повідомлення",
|
||||
"components.Settings.SettingsLogs.label": "Мітка",
|
||||
"components.Settings.SettingsLogs.filterWarn": "Попередження",
|
||||
"components.Settings.SettingsLogs.filterInfo": "Інформаційні",
|
||||
"components.Settings.SettingsLogs.filterError": "Помилки",
|
||||
"components.Settings.menuUsers": "Користувачі",
|
||||
"components.Settings.scanning": "Синхронізація…",
|
||||
"i18n.loading": "Завантаження…",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.user": "Користувач",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.role": "Роль",
|
||||
"components.Settings.webhook": "Веб-перехоплювач",
|
||||
"components.Setup.setup": "Налаштування",
|
||||
"components.Settings.SettingsJobsCache.process": "Процес",
|
||||
"components.Settings.SettingsJobsCache.command": "Команда",
|
||||
"components.Settings.SettingsJobsCache.jobtype": "Тип",
|
||||
"components.Settings.SettingsJobsCache.cache": "Кеш",
|
||||
"components.Settings.SettingsAbout.documentation": "Документація",
|
||||
"components.PersonDetails.crewmember": "У складі знімальної групи",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Релізи",
|
||||
"components.Settings.SettingsAbout.version": "Версія",
|
||||
"components.UserProfile.ProfileHeader.profile": "Подивитися профіль",
|
||||
"components.Settings.SettingsJobsCache.cachename": "Назва кеша",
|
||||
"components.Settings.SettingsJobsCache.cacheksize": "Розмір ключів",
|
||||
"components.Settings.SettingsJobsCache.cachekeys": "Усього ключів",
|
||||
"components.UserList.bulkedit": "Масове редагування",
|
||||
"components.MediaSlider.ShowMoreCard.seemore": "Подивитися більше",
|
||||
"components.TvDetails.watchtrailer": "Дивитись трейлер",
|
||||
"components.Settings.SettingsAbout.timezone": "Годинний пояс",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Підтримати Jellyseerr",
|
||||
"components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Отримувати повідомлення, коли інші користувачі надсилають нові медіа-запити, які схвалюються автоматично.",
|
||||
"components.NotificationTypeSelector.mediarequestedDescription": "Надсилати повідомлення, коли користувачі надсилають нові медіа-запити, які вимагають схвалення.",
|
||||
"components.NotificationTypeSelector.mediarequested": "Запити медіафайлів",
|
||||
"components.NotificationTypeSelector.mediafailedDescription": "Відправляти повідомлення, коли медіа-запити не вдається додати до Radarr або Sonarr.",
|
||||
"components.NotificationTypeSelector.mediafailed": "Помилки при додаванні медіа-запитів",
|
||||
"components.NotificationTypeSelector.mediadeclinedDescription": "Надсилати повідомлення, коли медіа-запити відхиляються.",
|
||||
"components.NotificationTypeSelector.mediaAutoApprovedDescription": "Надсилати повідомлення, коли користувачі надсилають нові медіа-запити, які схвалюються автоматично.",
|
||||
"components.NotificationTypeSelector.mediaAutoApproved": "Автоматичне схвалення медіа-запитів",
|
||||
"components.NotificationTypeSelector.mediaapproved": "Схвалення медіа-запитів",
|
||||
"components.NotificationTypeSelector.mediaapprovedDescription": "Надсилати повідомлення, коли медіа-запити схвалюються вручну.",
|
||||
"components.NotificationTypeSelector.mediadeclined": "Відхилення медіа-запитів",
|
||||
"components.NotificationTypeSelector.mediaavailableDescription": "Надсилати повідомлення, коли запитані медіафайли стають доступними.",
|
||||
"components.NotificationTypeSelector.mediaavailable": "Доступні нові медіафайли",
|
||||
"components.MovieDetails.MovieCrew.fullcrew": "Повна знімальна група",
|
||||
"components.MovieDetails.viewfullcrew": "Подивитися повну знімальну групу",
|
||||
"components.MovieDetails.showmore": "Розгорнути",
|
||||
"components.MovieDetails.showless": "Згорнути",
|
||||
"components.MovieDetails.playonplex": "Відтворити в Plex",
|
||||
"components.MovieDetails.play4konplex": "Відтворити в Plex в 4К",
|
||||
"components.MovieDetails.mark4kavailable": "Позначити як доступний у 4К",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Повний акторський склад",
|
||||
"components.Login.validationpasswordrequired": "Ви повинні надати пароль",
|
||||
"components.Login.validationemailrequired": "Ви повинні вказати дійсну адресу електронної пошти",
|
||||
"components.Login.signinwithoverseerr": "Використовуйте ваш обліковий запис {applicationTitle}",
|
||||
"components.Login.signingin": "Виконується вхід...",
|
||||
"components.Login.loginerror": "Щось пішло не так при спробі виконати вхід.",
|
||||
"components.Layout.LanguagePicker.displaylanguage": "Мова інтерфейсу",
|
||||
"components.DownloadBlock.estimatedtime": "Приблизно {time}",
|
||||
"components.Discover.upcomingtv": "Майбутні серіали",
|
||||
"components.Discover.discover": "Знайти щось нове",
|
||||
"components.Discover.TvGenreSlider.tvgenres": "Серіали за жанрами",
|
||||
"components.Discover.TvGenreList.seriesgenres": "Серіали за жанрами",
|
||||
"components.Discover.MovieGenreSlider.moviegenres": "Фільми за жанрами",
|
||||
"components.Discover.MovieGenreList.moviegenres": "Фільми за жанрами",
|
||||
"components.Discover.DiscoverTvLanguage.languageSeries": "Серіали мовою \"{language}\"",
|
||||
"components.Discover.DiscoverTvGenre.genreSeries": "Серіали в жанрі \"{genre}\"",
|
||||
"components.Discover.DiscoverNetwork.networkSeries": "Серіали {network}",
|
||||
"components.CollectionDetails.requestcollection4k": "Запросити Колекцію в 4К",
|
||||
"components.QuotaSelector.movies": "{count, plural, one {фільм} other {фільми}}",
|
||||
"components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {фільм} other {фільми}}",
|
||||
"components.RequestModal.QuotaDisplay.allowedRequestsUser": "Цьому користувачеві дозволено запитувати <strong>{limit}</strong> {type} кожні <strong>{days}</strong> днів.",
|
||||
"components.RequestModal.QuotaDisplay.allowedRequests": "Вам дозволено запитувати <strong>{limit}</strong> {type} кожні <strong>{days}</strong> днів.",
|
||||
"components.Settings.SonarrModal.testFirstRootFolders": "Протестувати з'єднання для завантаження кореневих каталогів",
|
||||
"components.Settings.SonarrModal.loadingrootfolders": "Завантаження кореневих каталогів…",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Кореневий каталог для аніме",
|
||||
"components.Settings.RadarrModal.testFirstRootFolders": "Протестувати підключення для завантаження кореневих каталогів",
|
||||
"components.Settings.RadarrModal.loadingrootfolders": "Завантаження кореневих каталогів…",
|
||||
"components.RequestModal.AdvancedRequester.destinationserver": "Сервер-одержувач",
|
||||
"components.RequestList.RequestItem.mediaerror": "Назва, пов'язана з цим запитом, більше недоступна.",
|
||||
"components.RequestList.RequestItem.failedretry": "Щось пішло не так при спробі повторити запит.",
|
||||
"components.RequestCard.mediaerror": "Назва, пов'язана з цим запитом, більше недоступна.",
|
||||
"components.RequestCard.failedretry": "Щось пішло не так при спробі повторити запит.",
|
||||
"components.RequestButton.viewrequest4k": "Подивитися 4К запит",
|
||||
"components.RequestButton.requestmore4k": "Запросити більше у 4К",
|
||||
"components.RequestButton.requestmore": "Запросити більше",
|
||||
"components.RequestButton.declinerequests": "Відхилити {requestCount, plural, one {запит} other {{requestCount} запиту(ів)}}",
|
||||
"components.RequestButton.declinerequest4k": "Відхилити 4К запит",
|
||||
"components.RequestButton.declinerequest": "Відхилити запит",
|
||||
"components.RequestButton.approve4krequests": "Схвалити {requestCount, plural, one {4К запит} other {{requestCount} 4К запиту(ів)}}",
|
||||
"components.RequestButton.decline4krequests": "Відхилити {requestCount, plural, one {4К запит}} other {{requestCount} 4К запиту(ів)}}",
|
||||
"components.RequestButton.approverequests": "Схвалити {requestCount, plural, one {запит} other {{requestCount} запиту(ів)}}",
|
||||
"components.RequestButton.approverequest4k": "Схвалити 4К запит",
|
||||
"components.RequestButton.approverequest": "Схвалити запит",
|
||||
"components.RequestBlock.server": "Сервер-одержувач",
|
||||
"components.QuotaSelector.tvRequests": "{quotaLimit} <quotaUnits>{сезонів} за {quotaDays} {днів}</quotaUnits>",
|
||||
"components.QuotaSelector.seasons": "{count, plural, one {сезон} other {сезони}}",
|
||||
"components.RequestBlock.requestoverrides": "Перевизначення запиту",
|
||||
"components.QuotaSelector.unlimited": "Необмежено",
|
||||
"components.QuotaSelector.movieRequests": "{quotaLimit} <quotaUnits>{фільмів} за {quotaDays} {днів}</quotaUnits>",
|
||||
"components.QuotaSelector.days": "{count, plural, one {день} other {днів}}",
|
||||
"components.PlexLoginButton.signingin": "Виконується вхід...",
|
||||
"components.PersonDetails.birthdate": "Народжений {birthdate}",
|
||||
"components.PermissionEdit.viewrequestsDescription": "Надати дозвіл на перегляд медіа-запитів, надісланих іншими користувачами.",
|
||||
"components.PermissionEdit.usersDescription": "Надати дозвіл на керування користувачами. Користувачі з цим дозволом не можуть надавати права адміністратора та редагувати користувачів, які є адміністраторами.",
|
||||
"components.PermissionEdit.settingsDescription": "Надати дозвіл на зміну глобальних налаштувань. Користувач повинен мати цей дозвіл, щоб надати його іншим.",
|
||||
"components.PermissionEdit.requestTvDescription": "Надати дозвіл на надсилання запитів усіх серіалів, відмінних від 4К.",
|
||||
"components.PermissionEdit.requestTv": "Запити серіалів",
|
||||
"components.PermissionEdit.requestMoviesDescription": "Надати дозвіл на надсилання запитів усіх фільмів, відмінних від 4К.",
|
||||
"Components.PermissionEdit.requestMovies": "Запити фільмів",
|
||||
"components.PermissionEdit.requestDescription": "Надати дозвіл на надсилання запитів усіх медіафайлів, відмінних від 4К.",
|
||||
"components.PermissionEdit.request4kTvDescription": "Надати дозвіл на надсилання запитів серіалів у 4К.",
|
||||
"components.PermissionEdit.request4kTv": "Запити серіалів у 4К",
|
||||
"components.PermissionEdit.request4kMoviesDescription": "Надати дозвіл на надсилання запитів фільмів у 4К.",
|
||||
"components.PermissionEdit.request4kMovies": "Запити фільмів у 4К",
|
||||
"components.PermissionEdit.request4kDescription": "Надати дозвіл на надсилання запитів медіафайлів у 4К.",
|
||||
"components.PermissionEdit.managerequestsDescription": "Надати дозвіл на керування медіа-запитами. Всі запити користувача, що має цю роздільну здатність, будуть схвалюватися автоматично.",
|
||||
"components.PermissionEdit.autoapproveSeriesDescription": "Надати дозвіл на автоматичне схвалення всіх серіалів, відмінних від 4К.",
|
||||
"components.PermissionEdit.autoapproveSeries": "Автоматичне схвалення серіалів",
|
||||
"components.PermissionEdit.autoapprove4kMoviesDescription": "Надати дозвіл на автоматичне схвалення 4К фільмів.",
|
||||
"Components.PermissionEdit.autoapprove4kMovies": "Автоматичне схвалення 4К фільмів",
|
||||
"components.PermissionEdit.autoapprove4kSeries": "Автоматичне схвалення 4К серіалів",
|
||||
"components.PermissionEdit.autoapprove4kSeriesDescription": "Надати дозвіл на автоматичне схвалення 4К серіалів.",
|
||||
"components.PermissionEdit.autoapproveMoviesDescription": "Надати дозвіл на автоматичне схвалення всіх фільмів, відмінних від 4К.",
|
||||
"Components.PermissionEdit.autoapproveMovies": "Автоматичне схвалення фільмів",
|
||||
"components.PermissionEdit.autoapprove4kDescription": "Надати дозвіл на автоматичне схвалення всіх 4К медіа-запитів.",
|
||||
"components.PermissionEdit.autoapproveDescription": "Надати дозвіл на автоматичне схвалення всіх медіа-запитів, відмінних від 4К.",
|
||||
"components.PermissionEdit.autoapprove4k": "Автоматичне схвалення 4К",
|
||||
"components.PermissionEdit.autoapprove": "Автоматичне схвалення",
|
||||
"components.PermissionEdit.advancedrequestDescription": "Надати дозвіл на зміну додаткових параметрів запиту.",
|
||||
"components.PermissionEdit.advancedrequest": "Розширені запити",
|
||||
"components.PermissionEdit.adminDescription": "Адміністратор має повний доступ. Ігнорує всі інші налаштування дозволів.",
|
||||
"components.NotificationTypeSelector.usermediarequestedDescription": "Отримувати повідомлення, коли інші користувачі надсилають нові медіа-запити, які вимагають схвалення.",
|
||||
"components.NotificationTypeSelector.usermediafailedDescription": "Отримувати повідомлення, коли медіа-запити не вдається додати до Radarr або Sonarr.",
|
||||
"components.NotificationTypeSelector.usermediadeclinedDescription": "Отримувати повідомлення, коли медіа-запити відхиляються.",
|
||||
"components.NotificationTypeSelector.usermediaavailableDescription": "Отримувати сповіщення, коли медіафайли, які ви запросили, стають доступними.",
|
||||
"components.NotificationTypeSelector.usermediaapprovedDescription": "Отримувати повідомлення, коли ваші медіа-запити отримують схвалення.",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {комміт} other {коммітів}} позаду",
|
||||
"components.MovieDetails.studio": "{studioCount, plural, one {Студія} other {Студії}}",
|
||||
"components.Layout.VersionStatus.streamstable": "Стабільна версія Jellyseerr",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Версія Jellyseerr для розробки",
|
||||
"components.Layout.VersionStatus.outofdate": "Застаріла",
|
||||
"components.AppDataWarning.dockerVolumeMissingDescription": "Підключення тома <code>{appDataPath}</code> налаштовано неправильно. Всі дані будуть видалені при зупинці або перезапуску контейнера.",
|
||||
"components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {запитів {type} не залишилося} other {залишилось <strong>#</strong> запиту(ів) {type}}}",
|
||||
"components.RequestModal.QuotaDisplay.quotaLinkUser": "Ви можете переглянути зведення обмежень на кількість запитів цього користувача на <ProfileLink>сторінці його профілю</ProfileLink>.",
|
||||
"components.RequestModal.QuotaDisplay.quotaLink": "Ви можете переглянути зведення ваших обмежень на кількість запитів на <ProfileLink>сторінці вашого профілю</ProfileLink>.",
|
||||
"components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Залишилося недостатньо запитів на сезони",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Надсилання тестового повідомлення веб-перехоплювачу…",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "Не вдалося надіслати тестове повідомлення веб-перехоплювачу.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Допомога за змінними шаблону",
|
||||
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "Корисне навантаження JSON успішно скинуто до стандартних налаштувань!",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customJson": "Корисне навантаження JSON",
|
||||
"components.Settings.Notifications.NotificationsWebhook.authheader": "Заголовок авторизації",
|
||||
"components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "Налаштування веб-повідомлень успішно збережено!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "Не вдалося зберегти налаштування веб-повідомлень.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Тестове веб-повідомлення надіслано!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Надсилання тестового веб-push-сповіщення…",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Не вдалося надіслати тестове веб-push-сповіщення.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Щоб отримувати веб-push-сповіщення, Jellyseerr повинен обслуговуватися за протоколом HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Створіть інтеграцію <WebhookLink>вхідного веб-перехоплювача</WebhookLink>",
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL веб-перехоплювача",
|
||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Тестове повідомлення надіслано до Slack!",
|
||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "Надсилання тестового повідомлення у Slack…",
|
||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "Не вдалося надіслати тестове повідомлення до Slack.",
|
||||
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Налаштування повідомлень Slack успішно збережено!",
|
||||
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Не вдалося зберегти налаштування повідомлень Slack.",
|
||||
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "Ви повинні надати дійсний ключ користувача або групи",
|
||||
"components.Settings.Notifications.validationTypes": "Ви повинні вибрати хоча б один тип повідомлень",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationTypes": "Ви повинні вибрати хоча б один тип повідомлень",
|
||||
"components.Settings.Notifications.NotificationsSlack.validationTypes": "Ви повинні вибрати хоча б один тип повідомлень",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "Ви повинні вибрати хоча б один тип повідомлень",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.validationTypes": "Ви повинні вибрати хоча б один тип повідомлень",
|
||||
"components.Settings.Notifications.NotificationsPushover.validationTypes": "Ви повинні вибрати хоча б один тип повідомлень",
|
||||
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "Ви повинні надати дійсний токен програми",
|
||||
"components.Settings.Notifications.NotificationsPushover.userTokenTip": "Ваш тридцятизначний <UsersGroupsLink>ідентифікатор користувача або групи</UsersGroupsLink>",
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "Тестове повідомлення надіслано в Pushover!",
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "Надсилання тестового повідомлення Pushover…",
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "Не вдалося надіслати тестове повідомлення Pushover.",
|
||||
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Параметри сповіщень Pushover успішно збережені!",
|
||||
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Не вдалося зберегти налаштування сповіщень Pushover.",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Зареєструйте програму</ApplicationRegistrationLink> для використання з Jellyseerr",
|
||||
"i18n.view": "Подивитися",
|
||||
"i18n.notrequested": "Не запрошений",
|
||||
"i18n.noresults": "Результатів немає.",
|
||||
"i18n.delimitedlist": "{a}, {b}",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.webhookUrl": "URL веб-перехоплювача",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.webhookUrlTip": "<LunaSeaLink>URL веб-перехоплювача для повідомлень</LunaSeaLink> на основі користувача або пристрою",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessToken": "Токен API програми",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Ви повинні надати токен доступу",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Тестове повідомлення надіслано в Pushbullet!",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Надсилання тестового повідомлення в Pushbullet…",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Не вдалося надіслати тестове повідомлення до Pushbullet.",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Налаштування сповіщень Pushbullet успішно збережено!",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Не вдалося зберегти налаштування сповіщень Pushbullet.",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Створіть токен у <PushbulletSettingsLink>налаштуваннях облікового запису</PushbulletSettingsLink>",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSuccess": "Тестове повідомлення надіслано до LunaSea!",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSending": "Надсилання тестового повідомлення в LunaSea…",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestFailed": "Не вдалося надіслати тестове повідомлення до LunaSea.",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.settingsSaved": "Налаштування повідомлень LunaSea успішно збережено!",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.settingsFailed": "Не вдалося зберегти налаштування повідомлень LunaSea.",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.profileNameTip": "Потрібно лише в тому випадку, якщо не використовується профіль <code>default</code>",
|
||||
"components.Settings.Notifications.NotificationsWebPush.agentenabled": "Активувати службу",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "Активувати службу",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.agentenabled": "Активувати службу",
|
||||
"components.Settings.notificationAgentSettingsDescription": "Налаштуйте та активуйте служби сповіщень.",
|
||||
"components.ResetPassword.emailresetlink": "Надіслати посилання для відновлення електронною поштою",
|
||||
"pages.somethingwentwrong": "Щось пішло не так",
|
||||
"pages.serviceunavailable": "Сервіс недоступний",
|
||||
"pages.pagenotfound": "Сторінку не знайдено",
|
||||
"pages.internalservererror": "Внутрішня помилка сервера",
|
||||
"components.ResetPassword.validationpasswordrequired": "Ви повинні надати пароль",
|
||||
"components.ResetPassword.validationpasswordminchars": "Пароль занадто короткий: він повинен містити не менше 8 символів",
|
||||
"components.ResetPassword.validationpasswordmatch": "Паролі повинні збігатися",
|
||||
"components.ResetPassword.validationemailrequired": "Ви повинні вказати дійсну адресу електронної пошти",
|
||||
"components.ResetPassword.requestresetlinksuccessmessage": "Посилання для скидання пароля буде надіслано на вказану адресу електронної пошти, якщо вона пов'язана з дійсним користувачем.",
|
||||
"components.ResetPassword.gobacklogin": "Повернутися до сторінки входу",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Мови для пошуку фільмів та серіалів",
|
||||
"components.Settings.region": "Регіон для пошуку фільмів та серіалів",
|
||||
"components.Settings.originallanguage": "Мови для пошуку фільмів та серіалів",
|
||||
"components.TvDetails.seasons": "{seasonCount, plural, one {# сезон} other {# сезонів}}",
|
||||
"components.RequestModal.QuotaDisplay.requiredquotaUser": "Цьому користувачеві необхідно мати принаймні <strong>{seasons}</strong> {seasons, plural, one {запит на сезони} other {запиту(ів) на сезони}} для того, щоб надіслати запит на цей серіал.",
|
||||
"components.RequestModal.QuotaDisplay.requiredquota": "Вам необхідно мати принаймні <strong>{seasons}</strong> {seasons, plural, one {запит на сезони} other {запиту(ів) на сезони}} для того , щоб надіслати запит на цей серіал.",
|
||||
"components.RequestModal.pending4krequest": "",
|
||||
"components.RequestModal.autoapproval": "Автоматичне схвалення",
|
||||
"i18n.usersettings": "Налаштування користувача",
|
||||
"i18n.showingresults": "Показуються результати з <strong>{from}</strong> по <strong>{to}</strong> з <strong>{total}</strong>",
|
||||
"i18n.save": "Зберегти зміни",
|
||||
"i18n.retrying": "Повтор…",
|
||||
"i18n.resultsperpage": "Відобразити {pageSize} результатів на сторінці",
|
||||
"i18n.requesting": "Запит…",
|
||||
"i18n.request4k": "Запросити до 4К",
|
||||
"i18n.areyousure": "Ви впевнені?",
|
||||
"components.StatusChacker.newversionDescription": "Jellyseerr було оновлено! Будь ласка, натисніть кнопку нижче, щоб перезавантажити сторінку.",
|
||||
"components.RequestModal.alreadyrequested": "Вже запрошений",
|
||||
"components.RequestModal.SearchByNameModal.notvdbiddescription": "Ми не змогли автоматично виконати ваш запит. Будь ласка, виберіть правильний збіг зі списку нижче.",
|
||||
"components.TvDetails.originaltitle": "Назва оригіналу",
|
||||
"components.Settings.validationApplicationTitle": "Ви повинні вказати назву програми",
|
||||
"i18n.tvshow": "Серіал",
|
||||
"components.Settings.partialRequestsEnabled": "Дозволити часткові запити серіалів",
|
||||
"components.Settings.mediaTypeSeries": "серіал",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Регіон для пошуку фільмів та серіалів",
|
||||
"components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {сезон} other {сезони}}",
|
||||
"components.RequestModal.QuotaDisplay.season": "сезон",
|
||||
"components.RequestModal.pendingapproval": "Ваш запит чекає схвалення.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Налаштування повідомлень електронною поштою успішно збережено!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "Не вдалося зберегти налаштування повідомлень електронною поштою.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Мова інтерфейсу",
|
||||
"components.UserList.validationpasswordminchars": "Пароль занадто короткий: він повинен містити не менше 8 символів",
|
||||
"components.UserList.nouserstoimport": "Немає нових користувачів для імпорту з Plex.",
|
||||
"components.UserList.autogeneratepasswordTip": "Надіслати користувачеві пароль, згенерований сервером, електронною поштою",
|
||||
"components.TvDetails.viewfullcrew": "Подивитися повну знімальну групу",
|
||||
"components.TvDetails.showtype": "Тип серіалу",
|
||||
"components.TvDetails.TvCrew.fullseriescrew": "Повна знімальна група серіалу",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Повний акторський склад серіалу",
|
||||
"components.Settings.trustProxyTip": "Дозволяє Jellyseerr коректно реєструвати IP-адреси клієнтів за проксі-сервером (Jellyseerr необхідно перезавантажити, щоб зміни набули чинності)",
|
||||
"components.Settings.originallanguageTip": "Контент фільтрується за мовою оригіналу",
|
||||
"components.Settings.noDefaultNon4kServer": "Якщо ви використовуєте один сервер {serverType} для контенту, в тому числі і для 4К, або якщо ви завантажуєте лише контент 4K, ваш сервер {serverType} <strong>НЕ</strong> має бути позначений як 4К сервер.",
|
||||
"components.UserList.localLoginDisabled": "Параметр <strong>Увімкнути локальний вхід</strong> в даний час вимкнено.",
|
||||
"components.Settings.SettingsLogs.showall": "Показати всі логі",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Обмеження кількості запитів на серіали",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Контент фільтрується за доступністю у вибраному регіоні",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Обмеження кількості запитів на фільми",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Перевизначити глобальні обмеження",
|
||||
"components.Settings.noDefaultServer": "Принаймні один сервер {serverType} повинен бути позначений як сервер за промовчанням для обробки запитів на {mediaType}и.",
|
||||
"components.Settings.noDefault4kServer": "4K сервер {serverType} повинен бути позначений як сервер за промовчанням, щоб користувачі могли надсилати запити на 4K {mediaType}и.",
|
||||
"components.Settings.SonarrModal.validationLanguageProfileRequired": "Ви повинні вибрати мовний профіль",
|
||||
"components.Settings.validationApplicationUrlTrailingSlash": "URL-адреса не повинна закінчуватися косою межею",
|
||||
"components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "Базова URL-адреса повинна мати косу межу на початку",
|
||||
"components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "Базова URL-адреса повинна мати косу межу на початку",
|
||||
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Протестувати підключення для завантаження мовних профілів",
|
||||
"components.Settings.SonarrModal.loadingTags": "Завантаження тегів…",
|
||||
"components.Settings.SonarrModal.enableSearch": "Увімкнути автоматичний пошук",
|
||||
"components.Settings.SonarrModal.edit4ksonarr": "Редагувати 4К сервер Sonarr",
|
||||
"components.Settings.toastApiKeyFailure": "Щось пішло не так при створенні нового ключа API.",
|
||||
"components.Settings.csrfProtectionTip": "Встановлює доступ до API ззовні тільки для читання (потрібно HTTPS, для набрання чинності необхідно перезавантажити Jellyseerr)",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Профіль якості для аніме",
|
||||
"components.Settings.SonarrModal.animelanguageprofile": "Мовний профіль для аніме",
|
||||
"components.Settings.SonarrModal.animeTags": "Теги для аніме",
|
||||
"components.Settings.SettingsUsers.userSettings": "Налаштування користувачів",
|
||||
"components.Settings.SettingsUsers.tvRequestLimitLabel": "Загальне обмеження кількості запитів серіалів",
|
||||
"components.Settings.SettingsUsers.toastSettingsSuccess": "Налаштування користувачів успішно збережено!",
|
||||
"components.Settings.SettingsUsers.toastSettingsFailure": "Щось пішло не так при збереженні налаштувань.",
|
||||
"components.Settings.SettingsUsers.newPlexLoginTip": "Дозволити користувачам {mediaServerName} входити до системи без попереднього імпорту",
|
||||
"components.Settings.SettingsUsers.newPlexLogin": "Увімкнути вхід через {mediaServerName} для нових користувачів",
|
||||
"components.Settings.SettingsUsers.movieRequestLimitLabel": "Загальне обмеження кількості запитів фільмів",
|
||||
"components.Settings.SettingsUsers.localLoginTip": "Дозволити користувачам входити в систему, використовуючи свою адресу електронної пошти та пароль замість Plex OAuth",
|
||||
"components.Settings.SettingsUsers.localLogin": "Увімкнути локальний вхід",
|
||||
"components.Settings.SettingsUsers.defaultPermissionsTip": "Початкові дозволи, надані новим користувачам",
|
||||
"components.Settings.SettingsUsers.defaultPermissions": "Дозволи за замовчуванням",
|
||||
"components.Settings.SettingsLogs.time": "Час",
|
||||
"components.Settings.SettingsLogs.level": "Важливість",
|
||||
"components.Settings.SettingsLogs.filterDebug": "Налагоджувальні",
|
||||
"components.Settings.SettingsLogs.extraData": "Додаткова інформація",
|
||||
"components.Settings.SettingsLogs.copyToClipboard": "Скопіювати в буфер обміну",
|
||||
"components.Settings.serverpresetRefreshing": "Отримання списку серверів…",
|
||||
"components.Settings.SettingsJobsCache.jobstarted": "Завдання \"{jobname}\" запущено.",
|
||||
"components.Settings.cacheImagesTip": "Оптимізувати та зберігати всі зображення локально (споживає значний обсяг дискового простору)",
|
||||
"components.Settings.cacheImages": "Увімкнути кешування зображень",
|
||||
"components.Settings.SettingsJobsCache.unknownJob": "Невідоме завдання",
|
||||
"components.Settings.SettingsJobsCache.sonarr-scan": "Сканування Sonarr",
|
||||
"components.Settings.SettingsJobsCache.runnow": "Виконати зараз",
|
||||
"components.Settings.SettingsJobsCache.radarr-scan": "Сканування Radarr",
|
||||
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Сканування нещодавно доданих медіафайлів у Plex",
|
||||
"components.Settings.SettingsJobsCache.plex-full-scan": "Повне сканування бібліотек Plex",
|
||||
"components.Settings.SettingsJobsCache.nextexecution": "Наступне виконання",
|
||||
"components.Settings.SettingsJobsCache.jobsandcache": "Завдання та кеш",
|
||||
"components.Settings.SettingsJobsCache.jobs": "Завдання",
|
||||
"components.Settings.SettingsJobsCache.jobname": "Назва завдання",
|
||||
"components.Settings.SettingsJobsCache.jobcancelled": "Завдання \"{jobname}\" скасовано.",
|
||||
"components.Settings.SettingsJobsCache.canceljob": "Скасувати завдання",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Jellyseerr виконує певні завдання обслуговування як регулярно запланованих завдань, але вони також можуть бути запущені вручну нижче. Виконання завдання вручну не змінить його розклад.",
|
||||
"components.Settings.SettingsJobsCache.flushcache": "Очистити кеш",
|
||||
"components.Settings.SettingsJobsCache.download-sync-reset": "Скинути синхронізацію завантажень",
|
||||
"components.Settings.SettingsJobsCache.download-sync": "Синхронізувати завантаження",
|
||||
"components.Settings.SettingsJobsCache.cachehits": "Вдалих звернень",
|
||||
"components.Settings.SettingsJobsCache.cachemisses": "Невдалих звернень",
|
||||
"components.Settings.SettingsJobsCache.cachevsize": "Розмір значень",
|
||||
"components.Settings.SettingsAbout.uptodate": "Актуальна",
|
||||
"components.Settings.SettingsAbout.Releases.versionChangelog": "Зміни у версії {version}",
|
||||
"components.Settings.SettingsAbout.preferredmethod": "Переважний спосіб",
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} кеш скинутий.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr кешує запити до зовнішніх кінцевих точок API, щоб оптимізувати продуктивність і уникнути непотрібних викликів API.",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Усього мультимедіа",
|
||||
"components.Settings.SettingsAbout.outofdate": "Застаріла",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Допомога оплатити каву",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Отримати підтримку",
|
||||
"components.Settings.SettingsAbout.betawarning": "Це бета-версія програмного забезпечення. Деякі функції можуть не працювати або працювати нестабільно. Будь ласка, повідомляйте про будь-які проблеми на GitHub!",
|
||||
"components.Settings.SettingsAbout.about": "Про проект",
|
||||
"components.Settings.SettingsAbout.Releases.viewongithub": "Подивитися на GitHub",
|
||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "Переглянути список змін",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Дані про дозвіл в даний час недоступні.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "Ви повинні ввести новий пароль",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Ви повинні надати дійсний ID чату",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Ви повинні надати дійсний відкритий ключ PGP",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Ви повинні надати дійсний ID користувача",
|
||||
"components.UserList.validationEmail": "Ви повинні вказати дійсну адресу електронної пошти",
|
||||
"components.UserList.usercreatedfailedexisting": "Вказана адреса електронної пошти вже використовується іншим користувачем.",
|
||||
"components.TvDetails.streamingproviders": "Зараз транслюється",
|
||||
"components.Settings.validationPortRequired": "Ви повинні вказати дійсний номер порту",
|
||||
"components.Settings.validationHostnameRequired": "Ви повинні вказати дійсне ім'я хоста або IP-адресу",
|
||||
"components.Settings.SonarrModal.validationNameRequired": "Ви повинні вказати ім'я сервера",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Ви повинні вказати ім'я сервера",
|
||||
"components.Settings.Notifications.validationEmail": "Ви повинні вказати дійсну адресу електронної пошти",
|
||||
"components.MovieDetails.streamingproviders": "Зараз транслюється",
|
||||
"components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "Базова URL-адреса не повинна закінчуватися косою межею",
|
||||
"components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Ви повинні вибрати мінімальну доступність",
|
||||
"components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "Базова URL-адреса не повинна закінчуватися косою межею",
|
||||
"components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "URL-адреса не повинна закінчуватися косою межею",
|
||||
"components.Settings.RadarrModal.testFirstTags": "Протестувати підключення для завантаження тегів",
|
||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Протестувати підключення для завантаження профілів якості",
|
||||
"components.Settings.RadarrModal.selecttags": "Виберіть теги",
|
||||
"components.Settings.RadarrModal.notagoptions": "Тегов немає.",
|
||||
"components.Settings.RadarrModal.loadingprofiles": "Завантаження профілів якості…",
|
||||
"components.Settings.RadarrModal.loadingTags": "Завантаження тегів…",
|
||||
"components.Settings.RadarrModal.enableSearch": "Увімкнути автоматичний пошук",
|
||||
"components.Settings.RadarrModal.default4kserver": "4К сервер за промовчанням",
|
||||
"components.Settings.Notifications.webhookUrlTip": "Створіть <DiscordWebhookLink>інтеграцію веб-перехоплювача</DiscordWebhookLink> на своєму сервері",
|
||||
"components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "Налаштування повідомлень веб-перехоплювача успішно збережено!",
|
||||
"components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Не вдалося зберегти налаштування повідомлень веб-перехоплювача.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Тестове повідомлення веб-перехоплювачу надіслано!",
|
||||
"components.Settings.Notifications.NotificationsWebhook.webhookUrl": "URL веб-перехоплювача",
|
||||
"components.Settings.Notifications.validationPgpPrivateKey": "Ви повинні надати дійсний закритий ключ PGP",
|
||||
"components.Settings.Notifications.validationPgpPassword": "Ви повинні надати пароль PGP",
|
||||
"components.Settings.Notifications.validationChatIdRequired": "Ви повинні надати дійсний ID чату",
|
||||
"components.Settings.Notifications.validationBotAPIRequired": "Ви повинні надати токен авторизації бота",
|
||||
"components.Settings.Notifications.telegramsettingsfailed": "Не вдалося зберегти налаштування сповіщень Telegram.",
|
||||
"components.Settings.Notifications.pgpPrivateKeyTip": "Підписувати зашифровані повідомлення електронної пошти за допомогою <OpenPgpLink>OpenPGP</OpenPgpLink>",
|
||||
"components.Settings.Notifications.pgpPrivateKey": "Закритий ключ PGP",
|
||||
"components.Settings.Notifications.pgpPasswordTip": "Підписувати зашифровані повідомлення електронної пошти за допомогою <OpenPgpLink>OpenPGP</OpenPgpLink>",
|
||||
"components.Settings.Notifications.pgpPassword": "Пароль PGP",
|
||||
"components.Settings.Notifications.encryptionTip": "У більшості випадків неявний TLS використовує порт 465, а STARTTLS - порт 587",
|
||||
"components.Settings.Notifications.encryptionOpportunisticTls": "Завжди використовувати STARTTLS",
|
||||
"components.Settings.Notifications.encryptionNone": "Без шифрування",
|
||||
"components.Settings.Notifications.encryptionImplicitTls": "Використовувати неявний TLS",
|
||||
"components.Settings.Notifications.encryptionDefault": "Використовувати STARTTLS, якщо доступно",
|
||||
"components.Settings.Notifications.encryption": "Метод шифрування",
|
||||
"components.Settings.Notifications.emailsettingssaved": "Налаштування повідомлень електронною поштою успішно збережено!",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "Не вдалося зберегти налаштування повідомлень електронною поштою.",
|
||||
"components.Settings.Notifications.discordsettingssaved": "Налаштування повідомлень Discord успішно збережено!",
|
||||
"components.Settings.Notifications.discordsettingsfailed": "Не вдалося зберегти налаштування повідомлень Discord.",
|
||||
"components.Settings.Notifications.chatIdTip": "Почніть чат зі своїм ботом, додайте <GetIdBotLink>@get_id_bot</GetIdBotLink> і виконайте команду <code>/my_id</code>",
|
||||
"components.Settings.Notifications.chatId": "ID чату",
|
||||
"components.Settings.Notifications.botUsernameTip": "Дозволити користувачам починати чат з вашим ботом і налаштовувати власні повідомлення",
|
||||
"components.Settings.Notifications.botUsername": "Ім'я бота",
|
||||
"components.Settings.Notifications.botAvatarUrl": "URL аватара бота",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Створіть бота</CreateBotLink> для використання з Jellyseerr",
|
||||
"components.Settings.Notifications.allowselfsigned": "Дозволити самозавірені сертифікати",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Ви повинні надати допустиме корисне навантаження JSON",
|
||||
"components.Settings.Notifications.toastTelegramTestSuccess": "Тестове повідомлення надіслано до Telegram!",
|
||||
"components.Settings.Notifications.toastTelegramTestSending": "Надсилання тестового повідомлення в Telegram…",
|
||||
"components.Settings.Notifications.toastDiscordTestSuccess": "Тестове повідомлення надіслано до Discord!",
|
||||
"components.Settings.Notifications.toastDiscordTestSending": "Надсилання тестового повідомлення в Discord…",
|
||||
"components.Settings.Notifications.toastDiscordTestFailed": "Не вдалося надіслати тестове повідомлення до Discord.",
|
||||
"components.Settings.Notifications.toastTelegramTestFailed": "Не вдалося надіслати тестове повідомлення до Telegram.",
|
||||
"components.Settings.Notifications.toastEmailTestSuccess": "Тестове повідомлення надіслано електронною поштою!",
|
||||
"components.Settings.Notifications.toastEmailTestSending": "Надсилання тестового повідомлення електронною поштою…",
|
||||
"components.Settings.Notifications.toastEmailTestFailed": "Не вдалося надіслати тестове повідомлення електронною поштою.",
|
||||
"components.UserProfile.unlimited": "Необмежено",
|
||||
"components.UserProfile.totalrequests": "Усього запитів",
|
||||
"components.UserProfile.requestsperdays": "залишилось {limit}",
|
||||
"components.UserProfile.pastdays": "{type} (за {days} день(ів))",
|
||||
"components.UserProfile.seriesrequest": "Запитів серіалів",
|
||||
"components.UserProfile.movierequests": "Запитів фільмів",
|
||||
"components.UserProfile.limit": "{remaining} з {limit}",
|
||||
"components.UserProfile.UserSettings.unauthorizedDescription": "У вас немає дозволу на зміну налаштувань цього користувача.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "У вас немає дозволу на зміну пароля цього користувача.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "Паролі повинні збігатися",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "Щось пішло не так при збереженні пароля. Чи правильно введено ваш поточний пароль?",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "Щось пішло не так при збереженні пароля.",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "В даний час для цього облікового запису не встановлено пароль. Встановіть пароль нижче, щоб з цим обліковим записом можна було увійти в систему як \"локальний користувач\".",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "В даний час для вашого облікового запису не встановлено пароль. Встановіть пароль нижче, щоб мати можливість увійти в систему як \"локальний користувач\", використовуючи свою адресу електронної пошти.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingssaved": "Налаштування веб-повідомлень успішно збережено!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingsfailed": "Не вдалося зберегти налаштування веб-повідомлень.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Веб-push",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Налаштування сповіщень Telegram успішно збережено!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Не вдалося зберегти налаштування сповіщень Telegram.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Почніть чат</TelegramBotLink>, додайте <GetIdBotLink>@get_id_bot</GetIdBotLink> і виконайте команду <code>/my_id</code>",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "ID чату",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Надсилати без звуку",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Шифрувати повідомлення електронної пошти за допомогою <OpenPgpLink>OpenPGP</OpenPgpLink>",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "Відкритий ключ PGP",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Налаштування повідомлень",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.email": "Електронна пошта",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Налаштування повідомлень Discord успішно збережено!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Не вдалося зберегти налаштування повідомлень Discord.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "<FindDiscordIdLink>ID</FindDiscordIdLink> вашого облікового запису",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID користувача",
|
||||
"components.Settings.locale": "Мова інтерфейсу",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Тип облікового запису",
|
||||
"components.UserProfile.ProfileHeader.userid": "ID користувача: {userid}",
|
||||
"components.UserProfile.ProfileHeader.settings": "Редагувати налаштування",
|
||||
"components.UserProfile.ProfileHeader.joindate": "Приєднався {joindate}",
|
||||
"components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "Пароль занадто короткий: він повинен містити не менше 8 символів",
|
||||
"components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "Ви не можете змінювати власні дозволи.",
|
||||
"components.UserList.userssaved": "Дозволи користувача успішно збережені!",
|
||||
"components.UserList.userfail": "Щось пішло не так при збереженні дозволів користувача.",
|
||||
"components.UserList.userdeleteerror": "Щось пішло не так при видаленні користувача.",
|
||||
"components.UserList.usercreatedfailed": "Щось пішло не так при створенні користувача.",
|
||||
"components.UserList.passwordinfodescription": "Налаштуйте URL-адресу програми та увімкніть повідомлення електронною поштою, щоб забезпечити можливість автоматичної генерації пароля.",
|
||||
"components.UserList.importfromplexerror": "Щось пішло не так при імпорті користувачів з Plex.",
|
||||
"components.UserList.importfrommediaserver": "Імпортувати користувачів з {mediaServerName}",
|
||||
"components.UserList.importfromplex": "Імпортувати користувачів з Plex",
|
||||
"components.UserList.importedfromplex": "{userCount, plural, one {# новий користувач} other {# нових користувачів} успішно імпортовані з Plex!",
|
||||
"components.UserList.edituser": "Змінити дозволи користувача",
|
||||
"components.UserList.displayName": "Відображуване ім'я",
|
||||
"components.UserList.deleteconfirm": "Ви впевнені, що хочете видалити цього користувача? Усі дані про його запити будуть видалені без можливості відновлення.",
|
||||
"components.UserList.autogeneratepassword": "Згенерувати пароль автоматично",
|
||||
"components.UserList.accounttype": "Тип",
|
||||
"components.TvDetails.playonplex": "Відтворити в Plex",
|
||||
"components.TvDetails.play4konplex": "Відтворити в Plex в 4К",
|
||||
"components.TvDetails.nextAirDate": "Наступна дата виходу в ефір",
|
||||
"components.TvDetails.firstAirDate": "Дата першого ефіру",
|
||||
"components.TvDetails.episodeRuntimeMinutes": "{runtime} хвилин",
|
||||
"components.TvDetails.episodeRuntime": "Тривалість епізоду",
|
||||
"components.Setup.tip": "Підказка",
|
||||
"components.Setup.scanbackground": "Сканування буде виконуватися у фоновому режимі. А поки ви можете продовжити процес налаштування.",
|
||||
"components.Settings.webpush": "Веб-push",
|
||||
"components.Settings.webAppUrlTip": "При необхідності надсилайте користувачів у веб-додаток на вашому сервері замість розміщеного на plex.tv",
|
||||
"components.Settings.webAppUrl": "URL <WebAppLink>веб-програми</WebAppLink>",
|
||||
"components.Settings.trustProxy": "Увімкнути підтримку проксі",
|
||||
"components.Settings.toastPlexRefreshSuccess": "Список серверів Plex успішно отримано!",
|
||||
"components.Settings.toastPlexRefresh": "Отримання списку серверів Plex…",
|
||||
"components.Settings.toastPlexRefreshFailure": "Не вдалося отримати список серверів Plex.",
|
||||
"components.Settings.toastPlexConnecting": "Спроба підключення до Plex…",
|
||||
"components.Settings.settingUpPlexDescription": "Щоб налаштувати Plex, ви можете або ввести дані вручну, або вибрати сервер, отриманий зі сторінки <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Натисніть кнопку праворуч від випадаючого списку, щоб отримати список доступних серверів .",
|
||||
"components.Settings.services": "Служби",
|
||||
"components.Settings.serviceSettingsDescription": "Налаштуйте сервер(и) {serverType} нижче. Ви можете підключити кілька серверів {serverType}, але тільки два з них можуть бути позначені як сервери за промовчанням (один не 4К і один 4К). Адміністратори можуть перевизначити сервер для обробки нових запитів до їх схвалення.",
|
||||
"components.Settings.serverpresetLoad": "Натисніть кнопку, щоб завантажити список доступних серверів",
|
||||
"components.Settings.serverSecure": "захищений",
|
||||
"components.Settings.serverRemote": "віддалений",
|
||||
"components.Settings.serverLocal": "локальний",
|
||||
"components.Settings.scan": "Синхронізувати бібліотеки",
|
||||
"components.Settings.regionTip": "Контент фільтрується за доступністю у вибраному регіоні",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Ви також можете переглядати ці логи безпосередньо через <code>stdout</code> або в <code>{appDataPath}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logs": "Логи",
|
||||
"components.Settings.SettingsLogs.logDetails": "Докладні відомості про лог",
|
||||
"components.Settings.SettingsLogs.copiedLogMessage": "Повідомлення лога скопійовано в буфер обміну.",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Контент фільтрується за мовою оригіналу",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "За замовчуванням ({language})",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.general": "Загальне",
|
||||
"components.Settings.general": "Спільне",
|
||||
"components.Settings.hideAvailable": "Приховувати доступні медіа",
|
||||
"components.Settings.SettingsUsers.userSettingsDescription": "Налаштуйте глобальні параметри та параметри за промовчанням для користувачів.",
|
||||
"components.Settings.email": "Електронна пошта",
|
||||
"components.Settings.csrfProtectionHoverTip": "НЕ вмикайте цей параметр, якщо ви не розумієте, що робите!",
|
||||
"components.Settings.csrfProtection": "Увімкнути захист від CSRF",
|
||||
"components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "URL-адреса не повинна закінчуватися косою межею",
|
||||
"components.Settings.toastPlexConnectingSuccess": "З'єднання з Plex встановлено успішно!",
|
||||
"components.Settings.SonarrModal.toastSonarrTestSuccess": "З'єднання з Sonarr встановлено успішно!",
|
||||
"components.Settings.toastPlexConnectingFailure": "Не вдалося підключитися до Plex.",
|
||||
"components.Settings.SonarrModal.toastSonarrTestFailure": "Не вдалося підключитися до Sonarr.",
|
||||
"components.Settings.SonarrModal.testFirstTags": "Протестувати підключення для завантаження тегів",
|
||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Протестувати підключення для завантаження профілів якості",
|
||||
"components.Settings.SonarrModal.selecttags": "Виберіть теги",
|
||||
"components.Settings.SonarrModal.selectLanguageProfile": "Виберіть мовний профіль",
|
||||
"components.Settings.SonarrModal.notagoptions": "Тегов немає.",
|
||||
"components.Settings.SonarrModal.loadingprofiles": "Завантаження профілів якості…",
|
||||
"components.Settings.SonarrModal.loadinglanguageprofiles": "Завантаження мовних профілів…",
|
||||
"components.Settings.SonarrModal.create4ksonarr": "Додати новий 4К сервер Sonarr",
|
||||
"components.Settings.RadarrModal.edit4kradarr": "Редагувати 4К сервер Radarr",
|
||||
"components.Settings.RadarrModal.create4kradarr": "Додати новий 4К сервер Radarr",
|
||||
"components.Settings.SonarrModal.default4kserver": "4К сервер за промовчанням",
|
||||
"components.Settings.toastApiKeySuccess": "Новий ключ API успішно згенерований!",
|
||||
"components.Settings.SettingsJobsCache.editJobSchedule": "Змінити завдання",
|
||||
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Частота",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Завдання успішно відредаговано!",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "Ви використовуєте гілку <code>develop</code> проекту Jellyseerr, яка рекомендується тільки для тих, хто робить внесок у розробку або допомагає в тестуванні.",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Щось пішло не так при збереженні завдання.",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Кожен {jobScheduleHours, plural, one {година} other {{jobScheduleHours} години(ів)}}",
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Кожну {jobScheduleMinutes, plural, one {хвилину} other {{jobScheduleMinutes} хвилин(и)}}",
|
||||
"components.StatusBadge.status": "{status}",
|
||||
"components.IssueDetails.IssueComment.areyousuredelete": "Ви впевнені, що хочете видалити цей коментар?",
|
||||
"components.IssueDetails.IssueComment.delete": "Видалити коментар",
|
||||
"components.IssueDetails.IssueComment.edit": "Редагувати коментар",
|
||||
"components.IssueDetails.IssueComment.postedby": "Опубліковано {relativeTime} користувачем {username}",
|
||||
"components.IssueDetails.IssueComment.postedbyedited": "Опубліковано {relativeTime} користувачем {username} (змінено)",
|
||||
"components.IssueDetails.IssueComment.validationComment": "Ви повинні ввести повідомлення",
|
||||
"components.IssueDetails.IssueDescription.deleteissue": "Видалити проблему",
|
||||
"components.IssueDetails.IssueDescription.description": "Опис",
|
||||
"components.IssueDetails.IssueDescription.edit": "Редагувати опис",
|
||||
"components.IssueDetails.allseasons": "Всі сезони",
|
||||
"components.IssueDetails.allepisodes": "Всі епізоди",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Очистити дані",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Це призведе до незворотного видалення всіх даних для цього {mediaType}а, включаючи будь-які запити. Якщо цей елемент існує у вашій бібліотеці {mediaServerName}, мультимедійна інформація про нього буде відтворена під час наступного сканування. ",
|
||||
"components.IssueDetails.problemepisode": "Зачеплений епізод",
|
||||
"components.ManageSlideOver.manageModalRequests": "Запити",
|
||||
"components.IssueDetails.closeissue": "Закрити проблему",
|
||||
"components.IssueDetails.closeissueandcomment": "Закрити з коментарем",
|
||||
"components.IssueDetails.comments": "Коментарі",
|
||||
"components.IssueDetails.deleteissueconfirm": "Ви впевнені, що хочете видалити цю проблему?",
|
||||
"components.IssueDetails.episode": "Епізод {episodeNumber}",
|
||||
"components.IssueDetails.lastupdated": "Останнє оновлення",
|
||||
"components.IssueDetails.openinarr": "Відкрити в {arr}",
|
||||
"components.IssueDetails.toasteditdescriptionfailed": "Щось пішло не так при редагуванні опису проблеми.",
|
||||
"components.IssueDetails.toastissuedeletefailed": "Щось пішло не так при видаленні проблеми.",
|
||||
"components.IssueDetails.toaststatusupdatefailed": "Щось пішло не так при оновленні статусу проблеми.",
|
||||
"components.IssueDetails.unknownissuetype": "Невідомий",
|
||||
"components.IssueModal.CreateIssueModal.episode": "Епізод {episodeNumber}",
|
||||
"components.ManageSlideOver.mark4kavailable": "Позначити як доступний у 4К",
|
||||
"components.IssueModal.CreateIssueModal.extras": "Додатково",
|
||||
"components.IssueModal.CreateIssueModal.problemseason": "Зачеплений сезон",
|
||||
"components.ManageSlideOver.downloadstatus": "Завантаження",
|
||||
"components.ManageSlideOver.manageModalIssues": "Відкриті проблеми",
|
||||
"components.ManageSlideOver.manageModalNoRequests": "Запитів немає.",
|
||||
"components.ManageSlideOver.manageModalTitle": "Управління {mediaType}",
|
||||
"components.ManageSlideOver.markavailable": "Позначити як доступний",
|
||||
"components.ManageSlideOver.movie": "фільм",
|
||||
"components.ManageSlideOver.openarr": "Відкрити в {arr}",
|
||||
"components.ManageSlideOver.openarr4k": "Відкрити в 4К {arr}",
|
||||
"components.ManageSlideOver.tvshow": "серіал",
|
||||
"components.NotificationTypeSelector.userissueresolvedDescription": "Отримувати повідомлення, коли проблеми, про які ви повідомили, отримують рішення.",
|
||||
"components.NotificationTypeSelector.issuecomment": "Коментар до проблеми",
|
||||
"components.PermissionEdit.createissuesDescription": "Надати дозвіл на повідомлення про проблеми з медіафайлами.",
|
||||
"components.PermissionEdit.manageissuesDescription": "Надати дозвіл на керування проблемами з медіафайлами.",
|
||||
"components.NotificationTypeSelector.userissuecreatedDescription": "Отримувати повідомлення, коли інші користувачі повідомляють про проблеми.",
|
||||
"components.PermissionEdit.createissues": "Повідомлення про проблеми",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Налаштування сповіщень Pushover успішно збережено!",
|
||||
"components.PermissionEdit.viewissues": "Перегляд проблем",
|
||||
"components.PermissionEdit.viewissuesDescription": "Надати дозвіл на перегляд проблем з медіафайлами, про які повідомили інші користувачі.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Ви повинні надати дійсний ключ користувача або групи",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Токен доступу",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "Токен API програми",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Створіть токен на сторінці <PushbulletSettingsLink>налаштувань вашого облікового запису</PushbulletSettingsLink>",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Ви повинні надати дійсний токен програми",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "Ваш 30-значний <UsersGroupsLink>ідентифікатор користувача або групи</UsersGroupsLink>",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Не вдалося зберегти налаштування сповіщень Pushbullet.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Налаштування сповіщень Pushbullet успішно збережено!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "<ApplicationRegistrationLink>Зареєструйте програму</ApplicationRegistrationLink> для використання з {applicationTitle}",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Ключ користувача або групи",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "Не вдалося зберегти налаштування сповіщень Pushover.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "Ви повинні надати токен доступу",
|
||||
"i18n.resolved": "Вирішені",
|
||||
"components.IssueDetails.openedby": "#{issueId} відкрита {relativeTime} користувачем {username}",
|
||||
"components.IssueDetails.openin4karr": "Відкрити в 4К {arr}",
|
||||
"components.IssueDetails.play4konplex": "Відтворити в {mediaServerName} у 4К",
|
||||
"components.IssueDetails.problemseason": "Зачеплений сезон",
|
||||
"components.IssueDetails.reopenissue": "Знову відкрити проблему",
|
||||
"components.IssueDetails.season": "Сезон {seasonNumber}",
|
||||
"components.IssueDetails.toasteditdescriptionsuccess": "Опис проблеми успішно відредаговано!",
|
||||
"components.IssueDetails.toastissuedeleted": "Проблема успішно видалена!",
|
||||
"components.IssueDetails.toaststatusupdated": "Статус проблеми успішно оновлено!",
|
||||
"components.IssueList.IssueItem.issuetype": "Тип",
|
||||
"components.IssueModal.CreateIssueModal.allseasons": "Всі сезони",
|
||||
"components.IssueModal.CreateIssueModal.problemepisode": "Зачеплений епізод",
|
||||
"components.IssueModal.CreateIssueModal.toastFailedCreate": "Щось пішло не так під час надсилання проблеми.",
|
||||
"components.IssueModal.CreateIssueModal.toastSuccessCreate": "Звіт про проблему для <strong>{title}</strong> успішно надіслано!",
|
||||
"components.IssueModal.CreateIssueModal.toastviewissue": "Переглянути проблему",
|
||||
"components.IssueModal.CreateIssueModal.validationMessageRequired": "Ви повинні надати опис",
|
||||
"components.IssueModal.issueAudio": "Аудіо",
|
||||
"components.IssueModal.issueOther": "Інше",
|
||||
"components.IssueModal.issueSubtitles": "Субтитри",
|
||||
"components.IssueModal.issueVideo": "Відео",
|
||||
"components.IssueModal.CreateIssueModal.reportissue": "Повідомити про проблему",
|
||||
"components.NotificationTypeSelector.adminissuecommentDescription": "Отримувати повідомлення, коли інші користувачі надсилають коментарі до проблем.",
|
||||
"components.NotificationTypeSelector.userissuecommentDescription": "Отримувати повідомлення, коли до проблем, про які ви повідомили, з'являються нові коментарі.",
|
||||
"components.NotificationTypeSelector.issuecommentDescription": "Надсилати повідомлення, коли до проблем з'являються нові коментарі.",
|
||||
"components.NotificationTypeSelector.issuecreated": "Проблема опублікована",
|
||||
"components.NotificationTypeSelector.issueresolved": "Проблема вирішена",
|
||||
"components.NotificationTypeSelector.issueresolvedDescription": "Надсилати повідомлення, коли проблеми отримують рішення.",
|
||||
"components.NotificationTypeSelector.issuecreatedDescription": "Надсилати повідомлення, коли з'являються повідомлення про проблеми.",
|
||||
"components.PermissionEdit.manageissues": "Управління проблемами",
|
||||
"i18n.open": "Відкрити",
|
||||
"components.IssueList.IssueItem.problemepisode": "Зачеплений епізод",
|
||||
"components.IssueList.IssueItem.unknownissuetype": "Невідомий",
|
||||
"components.IssueList.issues": "Проблеми",
|
||||
"components.IssueList.IssueItem.opened": "Відкрито",
|
||||
"components.IssueDetails.nocomments": "Коментарів немає.",
|
||||
"components.IssueDetails.issuepagetitle": "Проблема",
|
||||
"components.IssueDetails.deleteissue": "Видалити проблему",
|
||||
"components.IssueDetails.issuetype": "Тип",
|
||||
"components.IssueDetails.leavecomment": "Коментар",
|
||||
"components.IssueDetails.playonplex": "Відтворити в {mediaServerName}",
|
||||
"components.IssueDetails.reopenissueandcomment": "Знову відкрити з коментарем",
|
||||
"components.IssueList.IssueItem.issuestatus": "Статус",
|
||||
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {Сезон} other {Сезони}}",
|
||||
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {Епізод} other {Епізоди}}",
|
||||
"components.IssueList.IssueItem.viewissue": "Переглянути проблему",
|
||||
"components.IssueList.showallissues": "Показати всі проблеми",
|
||||
"components.IssueList.sortModified": "За датою зміни",
|
||||
"components.IssueModal.CreateIssueModal.allepisodes": "Всі епізоди",
|
||||
"components.IssueList.IssueItem.openeduserdate": "{date} користувачем {user}",
|
||||
"components.IssueList.sortAdded": "За датою додавання",
|
||||
"components.IssueModal.CreateIssueModal.season": "Сезон {seasonNumber}",
|
||||
"components.IssueModal.CreateIssueModal.submitissue": "Надіслати проблему",
|
||||
"components.IssueModal.CreateIssueModal.providedetail": "Будь ласка, надайте детальний опис проблеми, з якою ви зіткнулися.",
|
||||
"components.IssueModal.CreateIssueModal.whatswrong": "Що не так?",
|
||||
"components.Layout.Sidebar.issues": "Проблеми",
|
||||
"components.NotificationTypeSelector.issuereopenedDescription": "Надсилати повідомлення, коли проблеми відкриті заново.",
|
||||
"components.NotificationTypeSelector.userissuereopenedDescription": "Отримувати повідомлення, коли проблеми, про які ви повідомили, будуть відкриті заново.",
|
||||
"components.NotificationTypeSelector.adminissuereopenedDescription": "Отримувати повідомлення, коли проблеми відкриті заново іншими користувачами.",
|
||||
"components.NotificationTypeSelector.issuereopened": "Проблема відкрита знову",
|
||||
"components.NotificationTypeSelector.adminissueresolvedDescription": "Отримувати повідомлення, коли проблеми вирішені іншими користувачами.",
|
||||
"components.RequestModal.requestseasons4k": "Запит {seasonCount} {seasonCount, plural, one {сезону} other {сезонів}} у 4К",
|
||||
"components.TvDetails.productioncountries": "{countryCount, plural, one {Країна} other {Країни}} виробництва",
|
||||
"components.IssueDetails.commentplaceholder": "Додати коментар…",
|
||||
"components.MovieDetails.productioncountries": "{countryCount, plural, one {Країна} other {Країни}} виробництва",
|
||||
"components.RequestModal.selectmovies": "Виберіть фільм(и)",
|
||||
"components.RequestModal.approve": "Схвалити запит",
|
||||
"components.RequestModal.requestmovies": "Запит {count} {count, plural, one {фільма} other {фільмів}}",
|
||||
"components.RequestModal.requestmovies4k": "Запит {count} {count, plural, one {фільма} other {фільмів}} у 4К",
|
||||
"components.Settings.RadarrModal.inCinemas": "У кіно",
|
||||
"components.Settings.RadarrModal.released": "Випущено",
|
||||
"components.RequestModal.requestApproved": "Запит на <strong>{title}</strong> схвалений!",
|
||||
"components.Settings.RadarrModal.announced": "Анонсовано"
|
||||
}
|
||||
@@ -71,8 +71,6 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
|
||||
return import('../i18n/locale/sr.json');
|
||||
case 'sv':
|
||||
return import('../i18n/locale/sv.json');
|
||||
case 'ua':
|
||||
return import('../i18n/locale/ua.json');
|
||||
case 'zh-CN':
|
||||
return import('../i18n/locale/zh_Hans.json');
|
||||
case 'zh-TW':
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
}
|
||||
|
||||
.slideover {
|
||||
padding-top: calc(0.75rem + env(safe-area-inset-top)) !important;
|
||||
padding-bottom: calc(0.75rem + env(safe-area-inset-top)) !important;
|
||||
padding-top: calc(1rem + env(safe-area-inset-top)) !important;
|
||||
padding-bottom: calc(1rem + env(safe-area-inset-top)) !important;
|
||||
}
|
||||
|
||||
.sidebar-close-button {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
|
||||
export const refreshIntervalHelper = (
|
||||
downloadItem: {
|
||||
downloadStatus: DownloadingItem[] | undefined;
|
||||
downloadStatus4k: DownloadingItem[] | undefined;
|
||||
},
|
||||
timer: number
|
||||
) => {
|
||||
if (
|
||||
(downloadItem.downloadStatus ?? []).length > 0 ||
|
||||
(downloadItem.downloadStatus4k ?? []).length > 0
|
||||
) {
|
||||
return timer;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user