Compare commits

...

11 Commits

Author SHA1 Message Date
fallenbagel
9e8000527e feat: jellyseerr makeover 2024-04-15 23:52:50 +05:00
Fallenbagel
0900a95532 fix: nullable type for jellyfinMediaId(4k) (#702)
The jellyfinMediaId(4k) properties were inferred as string | undefined, causing them to be set to
undefined when assigning null. This prevented the media from being saved correctly to the SQLite
database, as it doesn't accept undefined values. This resolves the availabilitySync job issue where
the "play on" button wasn't being removed for all media server types.

fix #668
2024-03-31 16:26:09 +05:00
Fallenbagel
0c86684bc2 refactor(i18n): change the user-facing identity of the application in i18n (#703) 2024-03-31 16:25:45 +05:00
Danish Humair
010df62776 feat: check if first jellyfin user is admin (#635)
* feat: merge check if first jellyfin user is admin

re #610

* refactor(i18n): extract admin error message into en locale

---------

Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
2024-03-30 05:53:14 +05:00
Fallenbagel
530be4272c fix(jellyfinscanner): conditionally assign the jellyfinMediaId and jellyfinMediaId4k (#686)
Previously `jellyfinMediaId4k` was being assigned even if 4k server was not setup or even if 4k
content were not present. This fixes it by conditionally assigning the jellyfinMediaId and
JellyfinMediaId4k

fix #681
2024-03-14 03:11:53 +05:00
Fallenbagel
c2e87714b4 fix(embyauth): remove the accidentally added mediaServerType change code from another PR (#684)
Accidentally added the mediaServerType change code from another feature branch/PR during the auth
logic refactor that broke emby logins.
2024-03-14 01:08:09 +05:00
Gauvino
eee9a025d2 fix: typos on readme (#655)
* Fix typo

* Apply suggestions

* Apply suggestions

---------

Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
2024-02-23 12:57:57 +05:00
allcontributors[bot]
aed011a557 docs: add trackmastersteve as a contributor for doc (#665)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-23 09:39:53 +05:00
Stephen Harris
ea47dd3571 Fixed a typo (#654)
Just a simple typo fix.
2024-02-23 09:38:45 +05:00
Fallenbagel
4c9013729e refactor: jellyfin authentication and add gravatar for missing avatars of jellyfin users (#664)
* refactor: jellyfin authentication

This refactor standardizes the authentication approach in Jellyfin to mirror the method employed in
Plex authentication for consistency

* feat: use gravatar for jellyfin users' with missing jellyfin avatars
2024-02-23 09:38:18 +05:00
Fallenbagel
3eb1bb3d8f feat(job): media availability support for jellyfin/emby (#522)
* feat(job): media availability support for jellyfin/emby

This refactors the media availability job to support jellyfin/emby for media removal automatically.
Needs further testing on 4k items (as I have not yet tested with 4k), however, non-4k items work as
intended.

fix #406, fix #193, fix #516, fix #362, fix #84

* fix(availabilitysync): use the correct 4k jellyfinMediaId

* fix: season mapping for plex

Fixes a bug introduced with this PR where media availability sync job removed the seasons from all
series even when those seasons existed
2024-02-23 07:42:59 +05:00
74 changed files with 1088 additions and 382 deletions

View File

@@ -313,6 +313,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "trackmastersteve",
"name": "Stephen Harris",
"avatar_url": "https://avatars.githubusercontent.com/u/16858514?v=4",
"profile": "https://arm0.red",
"contributions": [
"doc"
]
} }
] ]
} }

View File

@@ -11,11 +11,11 @@
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a> <a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a> <a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- 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-33-orange.svg"/></a> <a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-34-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
**Jellyseerr** is a free and open source software application for managing requests for your media library. **Jellyseerr** is a free and open source software application for managing requests for your media library.
It is a a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers! It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_ _The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
@@ -40,11 +40,11 @@ With more features on the way! Check out our [issue tracker](https://github.com/
#### Pre-requisite (Important) #### Pre-requisite (Important)
_*On Jellyfin/Emby, ensure the `settings > Home > Automatically group content from the following folders into views such as 'Movies', 'Music' and 'TV'` is turned off*_ _*On Jellyfin/Emby, ensure the `Settings > Home > Automatically group content from the following folders into views such as 'Movies', 'Music' and 'TV'` is turned off*_
### Launching Jellyseerr using Docker (Recommended) ### Launching Jellyseerr using Docker (Recommended)
Check out our dockerhub for instructions on how to install and run Jellyseerr: Check out our docker hub for instructions on how to install and run Jellyseerr:
https://hub.docker.com/r/fallenbagel/jellyseerr https://hub.docker.com/r/fallenbagel/jellyseerr
### Building from source (ADVANCED): ### Building from source (ADVANCED):
@@ -65,16 +65,16 @@ yarn run build
yarn start yarn start
``` ```
(you can use task scheduler to run a bat script with `@echo off` and `yarn start` to run jellyseerr in the background) (You can use task scheduler to run a bat script with `@echo off` and `yarn start` to run jellyseerr in the background)
_to set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` in the root directory of jellyseerr_ _To set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` in the root directory of jellyseerr_
#### Linux #### Linux
**Pre-requisites:** **Pre-requisites:**
- Nodejs [v18](https://nodejs.org/en/download/package-manager) - Nodejs [v18](https://nodejs.org/en/download/package-manager)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) (on debian based distros, the package manager provided `yarn` is different and is a package called cmdlet. You can remove that using `apt-remove cmdlet` then install yarn using `npm install -g yarn`) - [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) (on Debian based distros, the package manager provided `yarn` is different and is a package called cmdlet. You can remove that using `apt-remove cmdlet` then install yarn using `npm install -g yarn`)
- Git - Git
**Steps:** **Steps:**
@@ -85,7 +85,7 @@ _to set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` i
cd /opt cd /opt
``` ```
2. Then clone the follow commands to clone and checkout to the stable version 2. Then execute the following commands to clone and checkout to the stable version
```bash ```bash
git clone https://github.com/Fallenbagel/jellyseerr.git && cd jellyseerr git clone https://github.com/Fallenbagel/jellyseerr.git && cd jellyseerr
@@ -104,9 +104,9 @@ yarn run build
5. If you want to run jellyseerr as a _Systemd-service:_ 5. If you want to run jellyseerr as a _Systemd-service:_
- assuming jellyseerr was cloned to `/opt/` - assuming jellyseerr was cloned to `/opt/`
- first create the environmentfile at `/etc/jellyseerr/jellyseerr.conf` - first create the environment file at `/etc/jellyseerr/jellyseerr.conf`
Environmentfile: Environment file:
``` ```
# Jellyseerr's default port is 5055, if you want to use both, change this. # Jellyseerr's default port is 5055, if you want to use both, change this.
@@ -228,6 +228,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import availabilitySync from '@server/lib/availabilitySync';
import logger from '@server/logger'; import logger from '@server/logger';
import type { AxiosInstance } from 'axios'; import type { AxiosInstance } from 'axios';
import axios from 'axios'; import axios from 'axios';
@@ -8,6 +9,9 @@ export interface JellyfinUserResponse {
ServerId: string; ServerId: string;
ServerName: string; ServerName: string;
Id: string; Id: string;
Policy: {
IsAdministrator: boolean;
};
PrimaryImageTag?: string; PrimaryImageTag?: string;
} }
@@ -241,7 +245,9 @@ class JellyfinAPI {
} }
} }
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> { public async getItemData(
id: string
): Promise<JellyfinLibraryItemExtended | undefined> {
try { try {
const contents = await this.axios.get<any>( const contents = await this.axios.get<any>(
`/Users/${this.userId}/Items/${id}` `/Users/${this.userId}/Items/${id}`
@@ -249,6 +255,11 @@ class JellyfinAPI {
return contents.data; return contents.data;
} catch (e) { } catch (e) {
if (availabilitySync.running) {
if (e.response && e.response.status === 500) {
return undefined;
}
}
logger.error( logger.error(
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`, `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }

View File

@@ -151,11 +151,11 @@ class Media {
@Column({ nullable: true, type: 'varchar' }) @Column({ nullable: true, type: 'varchar' })
public ratingKey4k?: string | null; public ratingKey4k?: string | null;
@Column({ nullable: true }) @Column({ nullable: true, type: 'varchar' })
public jellyfinMediaId?: string; public jellyfinMediaId?: string | null;
@Column({ nullable: true }) @Column({ nullable: true, type: 'varchar' })
public jellyfinMediaId4k?: string; public jellyfinMediaId4k?: string | null;
public serviceUrl?: string; public serviceUrl?: string;
public serviceUrl4k?: string; public serviceUrl4k?: string;

View File

@@ -1,4 +1,5 @@
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import availabilitySync from '@server/lib/availabilitySync';
import downloadTracker from '@server/lib/downloadtracker'; import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy'; import ImageProxy from '@server/lib/imageproxy';
import { import {
@@ -167,7 +168,7 @@ export const startJobs = (): void => {
}); });
// Checks if media is still available in plex/sonarr/radarr libs // Checks if media is still available in plex/sonarr/radarr libs
/* scheduledJobs.push({ scheduledJobs.push({
id: 'availability-sync', id: 'availability-sync',
name: 'Media Availability Sync', name: 'Media Availability Sync',
type: 'process', type: 'process',
@@ -182,7 +183,6 @@ export const startJobs = (): void => {
running: () => availabilitySync.running, running: () => availabilitySync.running,
cancelFn: () => availabilitySync.cancel(), cancelFn: () => availabilitySync.cancel(),
}); });
*/
// Run download sync every minute // Run download sync every minute
scheduledJobs.push({ scheduledJobs.push({

View File

@@ -1,9 +1,12 @@
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
import JellyfinAPI from '@server/api/jellyfin';
import type { PlexMetadata } from '@server/api/plexapi'; import type { PlexMetadata } from '@server/api/plexapi';
import PlexAPI from '@server/api/plexapi'; import PlexAPI from '@server/api/plexapi';
import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr'; import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr'; import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import MediaRequest from '@server/entity/MediaRequest'; import MediaRequest from '@server/entity/MediaRequest';
@@ -18,14 +21,20 @@ class AvailabilitySync {
public running = false; public running = false;
private plexClient: PlexAPI; private plexClient: PlexAPI;
private plexSeasonsCache: Record<string, PlexMetadata[]>; private plexSeasonsCache: Record<string, PlexMetadata[]>;
private jellyfinClient: JellyfinAPI;
private jellyfinSeasonsCache: Record<string, JellyfinLibraryItem[]>;
private sonarrSeasonsCache: Record<string, SonarrSeason[]>; private sonarrSeasonsCache: Record<string, SonarrSeason[]>;
private radarrServers: RadarrSettings[]; private radarrServers: RadarrSettings[];
private sonarrServers: SonarrSettings[]; private sonarrServers: SonarrSettings[];
async run() { async run() {
const settings = getSettings(); const settings = getSettings();
const mediaServerType = getSettings().main.mediaServerType;
this.running = true; this.running = true;
this.plexSeasonsCache = {}; this.plexSeasonsCache = {};
this.jellyfinSeasonsCache = {};
this.sonarrSeasonsCache = {}; this.sonarrSeasonsCache = {};
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled); this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled); this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
@@ -37,13 +46,53 @@ class AvailabilitySync {
const pageSize = 50; const pageSize = 50;
const userRepository = getRepository(User); const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: { id: true, plexToken: true },
where: { id: 1 },
});
if (admin) { // If it is plex admin is selected using plexToken if jellyfin admin is selected using jellyfinUserID
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
let admin = null;
if (mediaServerType === MediaServerType.PLEX) {
admin = await userRepository.findOne({
select: { id: true, plexToken: true },
where: { id: 1 },
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
admin = await userRepository.findOne({
where: { id: 1 },
select: [
'id',
'jellyfinAuthToken',
'jellyfinUserId',
'jellyfinDeviceId',
],
order: { id: 'ASC' },
});
}
if (mediaServerType === MediaServerType.PLEX) {
if (admin && admin.plexToken) {
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
} else {
logger.error('Plex admin is not configured.');
}
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
if (admin) {
this.jellyfinClient = new JellyfinAPI(
settings.jellyfin.hostname ?? '',
admin.jellyfinAuthToken,
admin.jellyfinDeviceId
);
this.jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
} else {
logger.error('Jellyfin admin is not configured.');
}
} else { } else {
logger.error('An admin is not configured.'); logger.error('An admin is not configured.');
} }
@@ -60,41 +109,84 @@ class AvailabilitySync {
let movieExists = false; let movieExists = false;
let movieExists4k = false; let movieExists4k = false;
const { existsInPlex } = await this.mediaExistsInPlex(media, false); // if (mediaServerType === MediaServerType.PLEX) {
const { existsInPlex: existsInPlex4k } = await this.mediaExistsInPlex( // await this.mediaExistsInPlex(media, false);
media, // } else if (
true // mediaServerType === MediaServerType.JELLYFIN ||
); // mediaServerType === MediaServerType.EMBY
// ) {
// await this.mediaExistsInJellyfin(media, false);
// }
const existsInRadarr = await this.mediaExistsInRadarr(media, false); const existsInRadarr = await this.mediaExistsInRadarr(media, false);
const existsInRadarr4k = await this.mediaExistsInRadarr(media, true); const existsInRadarr4k = await this.mediaExistsInRadarr(media, true);
if (existsInPlex || existsInRadarr) { // plex
movieExists = true; if (mediaServerType === MediaServerType.PLEX) {
logger.info( const { existsInPlex } = await this.mediaExistsInPlex(media, false);
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, const { existsInPlex: existsInPlex4k } =
{ await this.mediaExistsInPlex(media, true);
label: 'AvailabilitySync',
} if (existsInPlex || existsInRadarr) {
); movieExists = true;
logger.info(
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
if (existsInPlex4k || existsInRadarr4k) {
movieExists4k = true;
logger.info(
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
} }
if (existsInPlex4k || existsInRadarr4k) { //jellyfin
movieExists4k = true; if (
logger.info( mediaServerType === MediaServerType.JELLYFIN ||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, mediaServerType === MediaServerType.EMBY
{ ) {
label: 'AvailabilitySync', const { existsInJellyfin } = await this.mediaExistsInJellyfin(
} media,
false
); );
const { existsInJellyfin: existsInJellyfin4k } =
await this.mediaExistsInJellyfin(media, true);
if (existsInJellyfin || existsInRadarr) {
movieExists = true;
logger.info(
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
if (existsInJellyfin4k || existsInRadarr4k) {
movieExists4k = true;
logger.info(
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
} }
if (!movieExists && media.status === MediaStatus.AVAILABLE) { if (!movieExists && media.status === MediaStatus.AVAILABLE) {
await this.mediaUpdater(media, false); await this.mediaUpdater(media, false, mediaServerType);
} }
if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) { if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) {
await this.mediaUpdater(media, true); await this.mediaUpdater(media, true, mediaServerType);
} }
} }
@@ -104,6 +196,8 @@ class AvailabilitySync {
let showExists = false; let showExists = false;
let showExists4k = false; let showExists4k = false;
//plex
const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } = const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } =
await this.mediaExistsInPlex(media, false); await this.mediaExistsInPlex(media, false);
const { const {
@@ -111,6 +205,16 @@ class AvailabilitySync {
seasonsMap: plexSeasonsMap4k = new Map(), seasonsMap: plexSeasonsMap4k = new Map(),
} = await this.mediaExistsInPlex(media, true); } = await this.mediaExistsInPlex(media, true);
//jellyfin
const {
existsInJellyfin,
seasonsMap: jellyfinSeasonsMap = new Map(),
} = await this.mediaExistsInJellyfin(media, false);
const {
existsInJellyfin: existsInJellyfin4k,
seasonsMap: jellyfinSeasonsMap4k = new Map(),
} = await this.mediaExistsInJellyfin(media, true);
const { existsInSonarr, seasonsMap: sonarrSeasonsMap } = const { existsInSonarr, seasonsMap: sonarrSeasonsMap } =
await this.mediaExistsInSonarr(media, false); await this.mediaExistsInSonarr(media, false);
const { const {
@@ -118,24 +222,60 @@ class AvailabilitySync {
seasonsMap: sonarrSeasonsMap4k, seasonsMap: sonarrSeasonsMap4k,
} = await this.mediaExistsInSonarr(media, true); } = await this.mediaExistsInSonarr(media, true);
if (existsInPlex || existsInSonarr) { //plex
showExists = true; if (mediaServerType === MediaServerType.PLEX) {
logger.info( if (existsInPlex || existsInSonarr) {
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, showExists = true;
{ logger.info(
label: 'AvailabilitySync', `The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
} {
); label: 'AvailabilitySync',
}
);
}
} }
if (existsInPlex4k || existsInSonarr4k) { if (mediaServerType === MediaServerType.PLEX) {
showExists4k = true; if (existsInPlex4k || existsInSonarr4k) {
logger.info( showExists4k = true;
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, logger.info(
{ `The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
label: 'AvailabilitySync', {
} label: 'AvailabilitySync',
); }
);
}
}
//jellyfin
if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
if (existsInJellyfin || existsInSonarr) {
showExists = true;
logger.info(
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
}
if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
if (existsInJellyfin4k || existsInSonarr4k) {
showExists4k = true;
logger.info(
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
} }
// Here we will create a final map that will cross compare // Here we will create a final map that will cross compare
@@ -155,11 +295,45 @@ class AvailabilitySync {
filteredSeasonsMap.set(season.seasonNumber, false) filteredSeasonsMap.set(season.seasonNumber, false)
); );
const finalSeasons = new Map([ // non-4k
...filteredSeasonsMap, const finalSeasons: Map<number, boolean> = new Map();
...plexSeasonsMap,
...sonarrSeasonsMap, if (mediaServerType === MediaServerType.PLEX) {
]); plexSeasonsMap.forEach((value, key) => {
finalSeasons.set(key, value);
});
filteredSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
sonarrSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
jellyfinSeasonsMap.forEach((value, key) => {
finalSeasons.set(key, value);
});
filteredSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
sonarrSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
}
const filteredSeasonsMap4k: Map<number, boolean> = new Map(); const filteredSeasonsMap4k: Map<number, boolean> = new Map();
@@ -173,18 +347,64 @@ class AvailabilitySync {
filteredSeasonsMap4k.set(season.seasonNumber, false) filteredSeasonsMap4k.set(season.seasonNumber, false)
); );
const finalSeasons4k = new Map([ // 4k
...filteredSeasonsMap4k, const finalSeasons4k: Map<number, boolean> = new Map();
...plexSeasonsMap4k,
...sonarrSeasonsMap4k, if (mediaServerType === MediaServerType.PLEX) {
]); plexSeasonsMap4k.forEach((value, key) => {
finalSeasons4k.set(key, value);
});
filteredSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
sonarrSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
jellyfinSeasonsMap4k.forEach((value, key) => {
finalSeasons4k.set(key, value);
});
filteredSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
sonarrSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
}
// TODO: Figure out how to run seasonUpdater for each season
if ([...finalSeasons.values()].includes(false)) { if ([...finalSeasons.values()].includes(false)) {
await this.seasonUpdater(media, finalSeasons, false); await this.seasonUpdater(
media,
finalSeasons,
false,
mediaServerType
);
} }
if ([...finalSeasons4k.values()].includes(false)) { if ([...finalSeasons4k.values()].includes(false)) {
await this.seasonUpdater(media, finalSeasons4k, true); await this.seasonUpdater(
media,
finalSeasons4k,
true,
mediaServerType
);
} }
if ( if (
@@ -192,7 +412,7 @@ class AvailabilitySync {
(media.status === MediaStatus.AVAILABLE || (media.status === MediaStatus.AVAILABLE ||
media.status === MediaStatus.PARTIALLY_AVAILABLE) media.status === MediaStatus.PARTIALLY_AVAILABLE)
) { ) {
await this.mediaUpdater(media, false); await this.mediaUpdater(media, false, mediaServerType);
} }
if ( if (
@@ -200,7 +420,7 @@ class AvailabilitySync {
(media.status4k === MediaStatus.AVAILABLE || (media.status4k === MediaStatus.AVAILABLE ||
media.status4k === MediaStatus.PARTIALLY_AVAILABLE) media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
) { ) {
await this.mediaUpdater(media, true); await this.mediaUpdater(media, true, mediaServerType);
} }
} }
} }
@@ -272,7 +492,11 @@ class AvailabilitySync {
return mediaStatus; return mediaStatus;
} }
private async mediaUpdater(media: Media, is4k: boolean): Promise<void> { private async mediaUpdater(
media: Media,
is4k: boolean,
mediaServerType: MediaServerType
): Promise<void> {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
@@ -320,17 +544,32 @@ class AvailabilitySync {
mediaStatus === MediaStatus.PROCESSING mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] ? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
: null; : null;
media[is4k ? 'ratingKey4k' : 'ratingKey'] = if (mediaServerType === MediaServerType.PLEX) {
mediaStatus === MediaStatus.PROCESSING media[is4k ? 'ratingKey4k' : 'ratingKey'] =
? media[is4k ? 'ratingKey4k' : 'ratingKey'] mediaStatus === MediaStatus.PROCESSING
: null; ? media[is4k ? 'ratingKey4k' : 'ratingKey']
: null;
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId']
: null;
}
logger.info( logger.info(
`The ${is4k ? '4K' : 'non-4K'} ${ `The ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'movie' ? 'movie' : 'show' media.mediaType === 'movie' ? 'movie' : 'show'
} [TMDB ID ${media.tmdbId}] was not found in any ${ } [TMDB ID ${media.tmdbId}] was not found in any ${
media.mediaType === 'movie' ? 'Radarr' : 'Sonarr' media.mediaType === 'movie' ? 'Radarr' : 'Sonarr'
} and Plex instance. Status will be changed to unknown.`, } and ${
mediaServerType === MediaServerType.PLEX
? 'plex'
: mediaServerType === MediaServerType.JELLYFIN
? 'jellyfin'
: 'emby'
} instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' } { label: 'AvailabilitySync' }
); );
@@ -358,7 +597,8 @@ class AvailabilitySync {
private async seasonUpdater( private async seasonUpdater(
media: Media, media: Media,
seasons: Map<number, boolean>, seasons: Map<number, boolean>,
is4k: boolean is4k: boolean,
mediaServerType: MediaServerType
): Promise<void> { ): Promise<void> {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
const seasonRequestRepository = getRepository(SeasonRequest); const seasonRequestRepository = getRepository(SeasonRequest);
@@ -370,6 +610,8 @@ class AvailabilitySync {
); );
const seasonKeys = [...seasonsPendingRemoval.keys()]; const seasonKeys = [...seasonsPendingRemoval.keys()];
// let isSeasonRemoved = false;
try { try {
// Need to check and see if there are any related season // Need to check and see if there are any related season
// requests. If they are, we will need to delete them. // requests. If they are, we will need to delete them.
@@ -420,7 +662,13 @@ class AvailabilitySync {
media.tmdbId media.tmdbId
}] was not found in any ${ }] was not found in any ${
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr' media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
} and Plex instance. Status will be changed to unknown.`, } and ${
mediaServerType === MediaServerType.PLEX
? 'plex'
: mediaServerType === MediaServerType.JELLYFIN
? 'jellyfin'
: 'emby'
} instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' } { label: 'AvailabilitySync' }
); );
} catch (ex) { } catch (ex) {
@@ -604,6 +852,7 @@ class AvailabilitySync {
return seasonExists; return seasonExists;
} }
// Plex
private async mediaExistsInPlex( private async mediaExistsInPlex(
media: Media, media: Media,
is4k: boolean is4k: boolean
@@ -719,6 +968,123 @@ class AvailabilitySync {
return seasonExistsInPlex; return seasonExistsInPlex;
} }
// Jellyfin
private async mediaExistsInJellyfin(
media: Media,
is4k: boolean
): Promise<{ existsInJellyfin: boolean; seasonsMap?: Map<number, boolean> }> {
const ratingKey = media.jellyfinMediaId;
const ratingKey4k = media.jellyfinMediaId4k;
let existsInJellyfin = false;
let preventSeasonSearch = false;
// Check each jellyfin instance to see if the media still exists
// If found, we will assume the media exists and prevent removal
// We can use the cache we built when we fetched the series with mediaExistsInJellyfin
try {
let jellyfinMedia: JellyfinLibraryItem | undefined;
if (ratingKey && !is4k) {
jellyfinMedia = await this.jellyfinClient?.getItemData(ratingKey);
if (media.mediaType === 'tv' && jellyfinMedia !== undefined) {
this.jellyfinSeasonsCache[ratingKey] =
await this.jellyfinClient?.getSeasons(ratingKey);
}
}
if (ratingKey4k && is4k) {
jellyfinMedia = await this.jellyfinClient?.getItemData(ratingKey4k);
if (media.mediaType === 'tv' && jellyfinMedia !== undefined) {
this.jellyfinSeasonsCache[ratingKey4k] =
await this.jellyfinClient?.getSeasons(ratingKey4k);
}
}
if (jellyfinMedia) {
existsInJellyfin = true;
}
} catch (ex) {
if (!ex.message.includes('404' || '500')) {
existsInJellyfin = false;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${media.tmdbId}] from Jellyfin.`,
{
errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
}
}
// Here we check each season in jellyfin for availability
// If the API returns an error other than a 404,
// we will have to prevent the season check from happening
if (media.mediaType === 'tv') {
const seasonsMap: Map<number, boolean> = new Map();
if (!preventSeasonSearch) {
const filteredSeasons = media.seasons.filter(
(season) =>
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
season[is4k ? 'status4k' : 'status'] ===
MediaStatus.PARTIALLY_AVAILABLE
);
for (const season of filteredSeasons) {
const seasonExists = await this.seasonExistsInJellyfin(
media,
season,
is4k
);
if (seasonExists) {
seasonsMap.set(season.seasonNumber, true);
}
}
}
return { existsInJellyfin, seasonsMap };
}
return { existsInJellyfin };
}
private async seasonExistsInJellyfin(
media: Media,
season: Season,
is4k: boolean
): Promise<boolean> {
const ratingKey = media.jellyfinMediaId;
const ratingKey4k = media.jellyfinMediaId4k;
let seasonExistsInJellyfin = false;
// Check each jellyfin instance to see if the season exists
let jellyfinSeasons: JellyfinLibraryItem[] | undefined;
if (ratingKey && !is4k) {
jellyfinSeasons = this.jellyfinSeasonsCache[ratingKey];
}
if (ratingKey4k && is4k) {
jellyfinSeasons = this.jellyfinSeasonsCache[ratingKey4k];
}
const seasonIsAvailable = jellyfinSeasons?.find(
(jellyfinSeason) => jellyfinSeason.IndexNumber === season.seasonNumber
);
if (seasonIsAvailable) {
seasonExistsInJellyfin = true;
}
return seasonExistsInJellyfin;
}
} }
const availabilitySync = new AvailabilitySync(); const availabilitySync = new AvailabilitySync();

View File

@@ -62,7 +62,7 @@ class JellyfinScanner {
const metadata = await this.jfClient.getItemData(jellyfinitem.Id); const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
const newMedia = new Media(); const newMedia = new Media();
if (!metadata.Id) { if (!metadata?.Id) {
logger.debug('No Id metadata for this title. Skipping', { logger.debug('No Id metadata for this title. Skipping', {
label: 'Plex Sync', label: 'Plex Sync',
ratingKey: jellyfinitem.Id, ratingKey: jellyfinitem.Id,
@@ -168,9 +168,9 @@ class JellyfinScanner {
newMedia.jellyfinMediaId = newMedia.jellyfinMediaId =
hasOtherResolution || (!this.enable4kMovie && has4k) hasOtherResolution || (!this.enable4kMovie && has4k)
? metadata.Id ? metadata.Id
: undefined; : null;
newMedia.jellyfinMediaId4k = newMedia.jellyfinMediaId4k =
has4k && this.enable4kMovie ? metadata.Id : undefined; has4k && this.enable4kMovie ? metadata.Id : null;
await mediaRepository.save(newMedia); await mediaRepository.save(newMedia);
this.log(`Saved ${metadata.Name}`); this.log(`Saved ${metadata.Name}`);
} }
@@ -197,6 +197,14 @@ class JellyfinScanner {
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id; jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id;
const metadata = await this.jfClient.getItemData(Id); const metadata = await this.jfClient.getItemData(Id);
if (!metadata?.Id) {
logger.debug('No Id metadata for this title. Skipping', {
label: 'Plex Sync',
ratingKey: jellyfinitem.Id,
});
return;
}
if (metadata.ProviderIds.Tvdb) { if (metadata.ProviderIds.Tvdb) {
tvShow = await this.tmdb.getShowByTvdbId({ tvShow = await this.tmdb.getShowByTvdbId({
tvdbId: Number(metadata.ProviderIds.Tvdb), tvdbId: Number(metadata.ProviderIds.Tvdb),
@@ -275,7 +283,7 @@ class JellyfinScanner {
episode.Id episode.Id
); );
ExtendedEpisodeData.MediaSources?.some((MediaSource) => { ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
return MediaSource.MediaStreams.some((MediaStream) => { return MediaSource.MediaStreams.some((MediaStream) => {
if (MediaStream.Type === 'Video') { if (MediaStream.Type === 'Video') {
if ((MediaStream.Width ?? 0) >= 2000) { if ((MediaStream.Width ?? 0) >= 2000) {
@@ -453,8 +461,9 @@ class JellyfinScanner {
tmdbId: tvShow.id, tmdbId: tvShow.id,
tvdbId: tvShow.external_ids.tvdb_id, tvdbId: tvShow.external_ids.tvdb_id,
mediaAddedAt: new Date(metadata.DateCreated ?? ''), mediaAddedAt: new Date(metadata.DateCreated ?? ''),
jellyfinMediaId: Id, jellyfinMediaId: isAllStandardSeasons ? Id : null,
jellyfinMediaId4k: Id, jellyfinMediaId4k:
isAll4kSeasons && this.enable4kShow ? Id : null,
status: isAllStandardSeasons status: isAllStandardSeasons
? MediaStatus.AVAILABLE ? MediaStatus.AVAILABLE
: newSeasons.some( : newSeasons.some(

View File

@@ -11,6 +11,7 @@ import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth'; import { isAuthenticated } from '@server/middleware/auth';
import * as EmailValidator from 'email-validator'; import * as EmailValidator from 'email-validator';
import { Router } from 'express'; import { Router } from 'express';
import gravatarUrl from 'gravatar-url';
const authRoutes = Router(); const authRoutes = Router();
@@ -274,24 +275,87 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
where: { jellyfinUserId: account.User.Id }, where: { jellyfinUserId: account.User.Id },
}); });
if (user) { if (!user && !(await userRepository.count())) {
// Check if user is admin on jellyfin
if (account.User.Policy.IsAdministrator === false) {
throw new Error('not_admin');
}
logger.info(
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
}
);
// User doesn't exist, and there are no users in the database, we'll create the user
// with admin permission
settings.main.mediaServerType = MediaServerType.JELLYFIN;
user = new User({
email: body.email,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
userType: UserType.JELLYFIN,
});
settings.jellyfin.hostname = body.hostname ?? '';
settings.jellyfin.serverId = account.User.ServerId;
settings.save();
startJobs();
await userRepository.save(user);
}
// User already exists, let's update their information
else if (body.username === user?.jellyfinUsername) {
logger.info(
`Found matching ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby'
} user; updating user with ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby'
}`,
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
}
);
// Let's check if their authtoken is up to date // Let's check if their authtoken is up to date
if (user.jellyfinAuthToken !== account.AccessToken) { if (user.jellyfinAuthToken !== account.AccessToken) {
user.jellyfinAuthToken = account.AccessToken; user.jellyfinAuthToken = account.AccessToken;
} }
// Update the users avatar with their jellyfin profile pic (incase it changed) // Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) { if (account.User.PrimaryImageTag) {
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else { } else {
user.avatar = '/os_logo_square.png'; user.avatar = gravatarUrl(user.email, {
default: 'mm',
size: 200,
});
} }
user.jellyfinUsername = account.User.Name; user.jellyfinUsername = account.User.Name;
if (user.username === account.User.Name) { if (user.username === account.User.Name) {
user.username = ''; user.username = '';
} }
// TODO: If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY
// if (process.env.JELLYFIN_TYPE === 'emby') {
// settings.main.mediaServerType = MediaServerType.EMBY;
// settings.save();
// }
await userRepository.save(user); await userRepository.save(user);
} else if (!settings.main.newPlexLogin) { } else if (!settings.main.newPlexLogin) {
logger.warn( logger.warn(
@@ -307,69 +371,38 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
status: 403, status: 403,
message: 'Access denied.', message: 'Access denied.',
}); });
} else { } else if (!user) {
// Here we check if it's the first user. If it is, we create the user with no check logger.info(
// and give them admin permissions 'Sign-in attempt from Jellyfin user with access to the media server; creating new Overseerr user',
const totalUsers = await userRepository.count(); {
if (totalUsers === 0) { label: 'API',
logger.info( ip: req.ip,
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
}
);
user = new User({
email: body.email,
jellyfinUsername: account.User.Name, jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
await userRepository.save(user);
//Update hostname in settings if it doesn't exist (initial configuration)
//Also set mediaservertype to JELLYFIN
if (settings.jellyfin.hostname === '') {
settings.main.mediaServerType = MediaServerType.JELLYFIN;
settings.jellyfin.hostname = body.hostname ?? '';
settings.jellyfin.serverId = account.User.ServerId;
settings.save();
startJobs();
} }
);
if (!body.email) {
throw new Error('add_email');
} }
if (!user) { user = new User({
if (!body.email) { email: body.email,
throw new Error('add_email'); jellyfinUsername: account.User.Name,
} jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
user = new User({ jellyfinAuthToken: account.AccessToken,
email: body.email, permissions: settings.main.defaultPermissions,
jellyfinUsername: account.User.Name, avatar: account.User.PrimaryImageTag
jellyfinUserId: account.User.Id, ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
jellyfinDeviceId: deviceId, : gravatarUrl(body.email, { default: 'mm', size: 200 }),
jellyfinAuthToken: account.AccessToken, userType: UserType.JELLYFIN,
permissions: settings.main.defaultPermissions, });
avatar: account.User.PrimaryImageTag //initialize Jellyfin/Emby users with local login
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` const passedExplicitPassword = body.password && body.password.length > 0;
: '/os_logo_square.png', if (passedExplicitPassword) {
userType: UserType.JELLYFIN, await user.setPassword(body.password ?? '');
});
//initialize Jellyfin/Emby users with local login
const passedExplicitPassword =
body.password && body.password.length > 0;
if (passedExplicitPassword) {
await user.setPassword(body.password ?? '');
}
await userRepository.save(user);
} }
await userRepository.save(user);
} }
// Set logged in session // Set logged in session
@@ -395,11 +428,21 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
status: 401, status: 401,
message: 'Unauthorized', message: 'Unauthorized',
}); });
} else if (e.message === 'not_admin') {
return next({
status: 403,
message: 'CREDENTIAL_ERROR_NOT_ADMIN',
});
} else if (e.message === 'add_email') { } else if (e.message === 'add_email') {
return next({ return next({
status: 406, status: 406,
message: 'CREDENTIAL_ERROR_ADD_EMAIL', message: 'CREDENTIAL_ERROR_ADD_EMAIL',
}); });
} else if (e.message === 'select_server_type') {
return next({
status: 406,
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
});
} else { } else {
logger.error(e.message, { label: 'Auth' }); logger.error(e.message, { label: 'Auth' });
return next({ return next({

View File

@@ -29,6 +29,7 @@ import { getAppVersion } from '@server/utils/appVersion';
import { Router } from 'express'; import { Router } from 'express';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import fs from 'fs'; import fs from 'fs';
import gravatarUrl from 'gravatar-url';
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash'; import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
import { rescheduleJob } from 'node-schedule'; import { rescheduleJob } from 'node-schedule';
import path from 'path'; import path from 'path';
@@ -337,7 +338,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
id: user.Id, id: user.Id,
thumb: user.PrimaryImageTag thumb: user.PrimaryImageTag
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90` ? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
: '/os_logo_square.png', : gravatarUrl(user.Name, { default: 'mm', size: 200 }),
email: user.Name, email: user.Name,
})); }));

View File

@@ -537,7 +537,10 @@ router.post(
permissions: settings.main.defaultPermissions, permissions: settings.main.defaultPermissions,
avatar: jellyfinUser?.PrimaryImageTag avatar: jellyfinUser?.PrimaryImageTag
? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90` ? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
: '/os_logo_square.png', : gravatarUrl(jellyfinUser?.Name ?? '', {
default: 'mm',
size: 200,
}),
userType: UserType.JELLYFIN, userType: UserType.JELLYFIN,
}); });

View File

@@ -24,6 +24,7 @@ const messages = defineMessages({
validationusernamerequired: 'Username required', validationusernamerequired: 'Username required',
validationpasswordrequired: 'Password required', validationpasswordrequired: 'Password required',
loginerror: 'Something went wrong while trying to sign in.', loginerror: 'Something went wrong while trying to sign in.',
adminerror: 'You must use an admin account to sign in.',
credentialerror: 'The username or password is incorrect.', credentialerror: 'The username or password is incorrect.',
signingin: 'Signing in…', signingin: 'Signing in…',
signin: 'Sign In', signin: 'Sign In',
@@ -94,6 +95,8 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
intl.formatMessage( intl.formatMessage(
e.message == 'Request failed with status code 401' e.message == 'Request failed with status code 401'
? messages.credentialerror ? messages.credentialerror
: e.message == 'Request failed with status code 403'
? messages.adminerror
: messages.loginerror : messages.loginerror
), ),
{ {

View File

@@ -155,7 +155,7 @@
"components.TvDetails.overview": "Přehled", "components.TvDetails.overview": "Přehled",
"components.TvDetails.cast": "Obsazení", "components.TvDetails.cast": "Obsazení",
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
"components.StatusChacker.reloadJellyseerr": "Znovu načíst", "components.StatusChacker.reloadOverseerr": "Znovu načíst",
"components.Setup.tip": "Tip", "components.Setup.tip": "Tip",
"components.Setup.setup": "Konfigurace", "components.Setup.setup": "Konfigurace",
"components.Setup.finishing": "Dokončování…", "components.Setup.finishing": "Dokončování…",

View File

@@ -724,7 +724,7 @@
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.Setup.tip": "Tip", "components.Setup.tip": "Tip",
"components.Setup.welcome": "Velkommen til Jellyseerr", "components.Setup.welcome": "Velkommen til Jellyseerr",
"components.StatusChacker.reloadJellyseerr": "Genindlæs", "components.StatusChacker.reloadOverseerr": "Genindlæs",
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
"components.TvDetails.cast": "Roller", "components.TvDetails.cast": "Roller",
"components.TvDetails.episodeRuntimeMinutes": "{runtime} minutter", "components.TvDetails.episodeRuntimeMinutes": "{runtime} minutter",

View File

@@ -889,7 +889,7 @@
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.StatusChacker.newversionDescription": "Jellyseerr wurde aktualisiert! Bitte klicke auf die Schaltfläche unten, um die Seite neu zu laden.", "components.StatusChacker.newversionDescription": "Jellyseerr wurde aktualisiert! Bitte klicke auf die Schaltfläche unten, um die Seite neu zu laden.",
"components.StatusChacker.newversionavailable": "Anwendungsaktualisierung", "components.StatusChacker.newversionavailable": "Anwendungsaktualisierung",
"components.StatusChacker.reloadJellyseerr": "Jellyseerr neu laden", "components.StatusChacker.reloadOverseerr": "Jellyseerr neu laden",
"components.StatusChecker.appUpdated": "{applicationTitle} aktualisiert", "components.StatusChecker.appUpdated": "{applicationTitle} aktualisiert",
"components.StatusChecker.appUpdatedDescription": "Klicke bitte auf die Schaltfläche unten, um die Anwendung neu zu laden.", "components.StatusChecker.appUpdatedDescription": "Klicke bitte auf die Schaltfläche unten, um die Anwendung neu zu laden.",
"components.StatusChecker.reloadApp": "{applicationTitle} neu laden", "components.StatusChecker.reloadApp": "{applicationTitle} neu laden",

View File

@@ -634,7 +634,7 @@
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
"components.TvDetails.TvCrew.fullseriescrew": "Όλο το Πλήρωμα της Σειράς", "components.TvDetails.TvCrew.fullseriescrew": "Όλο το Πλήρωμα της Σειράς",
"components.TvDetails.TvCast.fullseriescast": "Όλοι οι Ηθοποιοί της Σειράς", "components.TvDetails.TvCast.fullseriescast": "Όλοι οι Ηθοποιοί της Σειράς",
"components.StatusChacker.reloadJellyseerr": "Επαναφόρτωση", "components.StatusChacker.reloadOverseerr": "Επαναφόρτωση",
"components.StatusChacker.newversionavailable": "Ενημέρωση εφαρμογής", "components.StatusChacker.newversionavailable": "Ενημέρωση εφαρμογής",
"components.StatusChacker.newversionDescription": "Το Jellyseerr έχει ενημερωθεί! Κάνε κλικ στο παρακάτω κουμπί για να φορτώσει ξανά η σελίδα.", "components.StatusChacker.newversionDescription": "Το Jellyseerr έχει ενημερωθεί! Κάνε κλικ στο παρακάτω κουμπί για να φορτώσει ξανά η σελίδα.",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",

View File

@@ -217,9 +217,10 @@
"components.Layout.UserWarnings.passwordRequired": "A password is required.", "components.Layout.UserWarnings.passwordRequired": "A password is required.",
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind", "components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
"components.Layout.VersionStatus.outofdate": "Out of Date", "components.Layout.VersionStatus.outofdate": "Out of Date",
"components.Layout.VersionStatus.streamdevelop": "Overseerr Develop", "components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
"components.Layout.VersionStatus.streamstable": "Overseerr Stable", "components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
"components.Login.credentialerror": "The username or password is incorrect.", "components.Login.credentialerror": "The username or password is incorrect.",
"components.Login.adminerror": "You must use an admin account to sign in.",
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.", "components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
"components.Login.email": "Email Address", "components.Login.email": "Email Address",
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.", "components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
@@ -582,7 +583,7 @@
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token", "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token",
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "You must select at least one notification type",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token", "components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr", "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent", "components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Device Default", "components.Settings.Notifications.NotificationsPushover.deviceDefault": "Device Default",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.", "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.",
@@ -607,7 +608,7 @@
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration", "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
"components.Settings.Notifications.NotificationsWebPush.agentenabled": "Enable Agent", "components.Settings.Notifications.NotificationsWebPush.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "In order to receive web push notifications, Overseerr must be served over HTTPS.", "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "In order to receive web push notifications, Jellyseerr must be served over HTTPS.",
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web push test notification failed to send.", "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web push test notification failed to send.",
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Sending web push test notification…", "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Sending web push test notification…",
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push test notification sent!", "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push test notification sent!",
@@ -633,7 +634,7 @@
"components.Settings.Notifications.authPass": "SMTP Password", "components.Settings.Notifications.authPass": "SMTP Password",
"components.Settings.Notifications.authUser": "SMTP Username", "components.Settings.Notifications.authUser": "SMTP Username",
"components.Settings.Notifications.botAPI": "Bot Authorization Token", "components.Settings.Notifications.botAPI": "Bot Authorization Token",
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr", "components.Settings.Notifications.botApiTip": "<CreateBotLink>Create a bot</CreateBotLink> for use with Jellyseerr",
"components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL", "components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL",
"components.Settings.Notifications.botUsername": "Bot Username", "components.Settings.Notifications.botUsername": "Bot Username",
"components.Settings.Notifications.botUsernameTip": "Allow users to also start a chat with your bot and configure their own notifications", "components.Settings.Notifications.botUsernameTip": "Allow users to also start a chat with your bot and configure their own notifications",
@@ -748,9 +749,9 @@
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions", "components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions",
"components.Settings.SettingsAbout.helppaycoffee": "Help Pay for Coffee", "components.Settings.SettingsAbout.helppaycoffee": "Help Pay for Coffee",
"components.Settings.SettingsAbout.outofdate": "Out of Date", "components.Settings.SettingsAbout.outofdate": "Out of Date",
"components.Settings.SettingsAbout.overseerrinformation": "About Overseerr", "components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
"components.Settings.SettingsAbout.preferredmethod": "Preferred", "components.Settings.SettingsAbout.preferredmethod": "Preferred",
"components.Settings.SettingsAbout.runningDevelop": "You are running the <code>develop</code> branch of Overseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.", "components.Settings.SettingsAbout.runningDevelop": "You are running the <code>develop</code> branch of Jellyseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.",
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr", "components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
"components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr", "components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr",
"components.Settings.SettingsAbout.timezone": "Time Zone", "components.Settings.SettingsAbout.timezone": "Time Zone",
@@ -760,7 +761,7 @@
"components.Settings.SettingsAbout.version": "Version", "components.Settings.SettingsAbout.version": "Version",
"components.Settings.SettingsJobsCache.availability-sync": "Media Availability Sync", "components.Settings.SettingsJobsCache.availability-sync": "Media Availability Sync",
"components.Settings.SettingsJobsCache.cache": "Cache", "components.Settings.SettingsJobsCache.cache": "Cache",
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.", "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.", "components.Settings.SettingsJobsCache.cacheflushed": "{cachename} cache flushed.",
"components.Settings.SettingsJobsCache.cachehits": "Hits", "components.Settings.SettingsJobsCache.cachehits": "Hits",
"components.Settings.SettingsJobsCache.cachekeys": "Total Keys", "components.Settings.SettingsJobsCache.cachekeys": "Total Keys",
@@ -781,7 +782,7 @@
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache", "components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup", "components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
"components.Settings.SettingsJobsCache.imagecache": "Image Cache", "components.Settings.SettingsJobsCache.imagecache": "Image Cache",
"components.Settings.SettingsJobsCache.imagecacheDescription": "When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.", "components.Settings.SettingsJobsCache.imagecacheDescription": "When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.",
"components.Settings.SettingsJobsCache.imagecachecount": "Images Cached", "components.Settings.SettingsJobsCache.imagecachecount": "Images Cached",
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size", "components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Full Library Scan", "components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Full Library Scan",
@@ -791,7 +792,7 @@
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.", "components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
"components.Settings.SettingsJobsCache.jobname": "Job Name", "components.Settings.SettingsJobsCache.jobname": "Job Name",
"components.Settings.SettingsJobsCache.jobs": "Jobs", "components.Settings.SettingsJobsCache.jobs": "Jobs",
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.", "components.Settings.SettingsJobsCache.jobsDescription": "Jellyseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.",
"components.Settings.SettingsJobsCache.jobsandcache": "Jobs & Cache", "components.Settings.SettingsJobsCache.jobsandcache": "Jobs & Cache",
"components.Settings.SettingsJobsCache.jobstarted": "{jobname} started.", "components.Settings.SettingsJobsCache.jobstarted": "{jobname} started.",
"components.Settings.SettingsJobsCache.jobtype": "Type", "components.Settings.SettingsJobsCache.jobtype": "Type",
@@ -832,7 +833,7 @@
"components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", "components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.SettingsMain.general": "General", "components.Settings.SettingsMain.general": "General",
"components.Settings.SettingsMain.generalsettings": "General Settings", "components.Settings.SettingsMain.generalsettings": "General Settings",
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Overseerr.", "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
"components.Settings.SettingsMain.hideAvailable": "Hide Available Media", "components.Settings.SettingsMain.hideAvailable": "Hide Available Media",
"components.Settings.SettingsMain.locale": "Display Language", "components.Settings.SettingsMain.locale": "Display Language",
"components.Settings.SettingsMain.originallanguage": "Discover Language", "components.Settings.SettingsMain.originallanguage": "Discover Language",
@@ -845,7 +846,7 @@
"components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.", "components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.",
"components.Settings.SettingsMain.toastSettingsSuccess": "Settings saved successfully!", "components.Settings.SettingsMain.toastSettingsSuccess": "Settings saved successfully!",
"components.Settings.SettingsMain.trustProxy": "Enable Proxy Support", "components.Settings.SettingsMain.trustProxy": "Enable Proxy Support",
"components.Settings.SettingsMain.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy", "components.Settings.SettingsMain.trustProxyTip": "Allow Jellyseerr to correctly register client IP addresses behind a proxy",
"components.Settings.SettingsMain.validationApplicationTitle": "You must provide an application title", "components.Settings.SettingsMain.validationApplicationTitle": "You must provide an application title",
"components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL", "components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL",
"components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
@@ -947,7 +948,7 @@
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.", "components.Settings.jellyfinsettingsDescription": "Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.",
"components.Settings.librariesRemaining": "Libraries Remaining: {count}", "components.Settings.librariesRemaining": "Libraries Remaining: {count}",
"components.Settings.manualscan": "Manual Library Scan", "components.Settings.manualscan": "Manual Library Scan",
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!", "components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
"components.Settings.manualscanDescriptionJellyfin": "Normally, this will only be run once every 24 hours. Jellyseerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Jellyseerr, a one-time full manual library scan is recommended!", "components.Settings.manualscanDescriptionJellyfin": "Normally, this will only be run once every 24 hours. Jellyseerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Jellyseerr, a one-time full manual library scan is recommended!",
"components.Settings.manualscanJellyfin": "Manual Library Scan", "components.Settings.manualscanJellyfin": "Manual Library Scan",
"components.Settings.mediaTypeMovie": "movie", "components.Settings.mediaTypeMovie": "movie",
@@ -970,12 +971,12 @@
"components.Settings.notrunning": "Not Running", "components.Settings.notrunning": "Not Running",
"components.Settings.plex": "Plex", "components.Settings.plex": "Plex",
"components.Settings.plexlibraries": "Plex Libraries", "components.Settings.plexlibraries": "Plex Libraries",
"components.Settings.plexlibrariesDescription": "The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.", "components.Settings.plexlibrariesDescription": "The libraries Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.",
"components.Settings.plexsettings": "Plex Settings", "components.Settings.plexsettings": "Plex Settings",
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.", "components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Jellyseerr scans your Plex libraries to determine content availability.",
"components.Settings.port": "Port", "components.Settings.port": "Port",
"components.Settings.radarrsettings": "Radarr Settings", "components.Settings.radarrsettings": "Radarr Settings",
"components.Settings.restartrequiredTooltip": "Overseerr must be restarted for changes to this setting to take effect", "components.Settings.restartrequiredTooltip": "Jellyseerr must be restarted for changes to this setting to take effect",
"components.Settings.save": "Save Changes", "components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving…", "components.Settings.saving": "Saving…",
"components.Settings.scan": "Sync Libraries", "components.Settings.scan": "Sync Libraries",
@@ -997,7 +998,7 @@
"components.Settings.syncing": "Syncing", "components.Settings.syncing": "Syncing",
"components.Settings.tautulliApiKey": "API Key", "components.Settings.tautulliApiKey": "API Key",
"components.Settings.tautulliSettings": "Tautulli Settings", "components.Settings.tautulliSettings": "Tautulli Settings",
"components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Overseerr fetches watch history data for your Plex media from Tautulli.", "components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Jellyseerr fetches watch history data for your Plex media from Tautulli.",
"components.Settings.timeout": "Timeout", "components.Settings.timeout": "Timeout",
"components.Settings.toastPlexConnecting": "Attempting to connect to Plex…", "components.Settings.toastPlexConnecting": "Attempting to connect to Plex…",
"components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.", "components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.",

View File

@@ -1152,7 +1152,7 @@
"components.Settings.SettingsMain.applicationurl": "URL applicazione", "components.Settings.SettingsMain.applicationurl": "URL applicazione",
"components.Settings.SettingsMain.validationApplicationTitle": "Devi fornire un titolo dell'applicazione", "components.Settings.SettingsMain.validationApplicationTitle": "Devi fornire un titolo dell'applicazione",
"components.Settings.SettingsMain.validationApplicationUrl": "Devi fornire un URL valido", "components.Settings.SettingsMain.validationApplicationUrl": "Devi fornire un URL valido",
"components.Settings.restartrequiredTooltip": "Overseerr deve essere riavviato per rendere effettive le modifiche", "components.Settings.restartrequiredTooltip": "Jellyseerr deve essere riavviato per rendere effettive le modifiche",
"components.TitleCard.tvdbid": "TheTVDB ID", "components.TitleCard.tvdbid": "TheTVDB ID",
"components.UserProfile.emptywatchlist": "I media aggiunti alla tua <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> appariranno qui.", "components.UserProfile.emptywatchlist": "I media aggiunti alla tua <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> appariranno qui.",
"components.TvDetails.status4k": "4K {status}", "components.TvDetails.status4k": "4K {status}",

View File

@@ -324,13 +324,13 @@
"components.Settings.SettingsAbout.appDataPath": "데이터 디렉토리", "components.Settings.SettingsAbout.appDataPath": "데이터 디렉토리",
"components.Settings.SettingsAbout.documentation": "문서", "components.Settings.SettingsAbout.documentation": "문서",
"components.Settings.SettingsAbout.gettingsupport": "지원 받기", "components.Settings.SettingsAbout.gettingsupport": "지원 받기",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr 정보", "components.Settings.SettingsAbout.overseerrinformation": "Jellyseerr 정보",
"components.Settings.SettingsAbout.preferredmethod": "선호", "components.Settings.SettingsAbout.preferredmethod": "선호",
"components.Settings.SettingsAbout.runningDevelop": "당신은 <code>개발</code>에 기여하거나 최신 테스트를 지원하는 사람들에게만 권장되는 Overserr 분기를 실행하고 있습니다.", "components.Settings.SettingsAbout.runningDevelop": "당신은 <code>개발</code>에 기여하거나 최신 테스트를 지원하는 사람들에게만 권장되는 Overserr 분기를 실행하고 있습니다.",
"components.Settings.SettingsAbout.timezone": "시간대", "components.Settings.SettingsAbout.timezone": "시간대",
"components.Settings.SettingsJobsCache.availability-sync": "사용가능한 미디어 동기화", "components.Settings.SettingsJobsCache.availability-sync": "사용가능한 미디어 동기화",
"components.Settings.SettingsJobsCache.cache": "캐시", "components.Settings.SettingsJobsCache.cache": "캐시",
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr는 외부 API 엔드포인트에 대한 요청을 캐시하여 성능을 최적화하고 불필요한 API 호출을 방지합니다.", "components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr는 외부 API 엔드포인트에 대한 요청을 캐시하여 성능을 최적화하고 불필요한 API 호출을 방지합니다.",
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} 캐시가 플러시되었습니다.", "components.Settings.SettingsJobsCache.cacheflushed": "{cachename} 캐시가 플러시되었습니다.",
"components.Settings.SettingsJobsCache.cachekeys": "전체 키", "components.Settings.SettingsJobsCache.cachekeys": "전체 키",
"components.Settings.SettingsJobsCache.cacheksize": "키 크기", "components.Settings.SettingsJobsCache.cacheksize": "키 크기",
@@ -342,11 +342,11 @@
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "새 주파수", "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "새 주파수",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "매 {jobScheduleSeconds, plural, one {초} other {{jobScheduleSeconds} 초}}", "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "매 {jobScheduleSeconds, plural, one {초} other {{jobScheduleSeconds} 초}}",
"components.Settings.SettingsJobsCache.imagecache": "이미지 캐시", "components.Settings.SettingsJobsCache.imagecache": "이미지 캐시",
"components.Settings.SettingsJobsCache.imagecacheDescription": "설정에서 활성화하면 Overseerr는 미리 구성된 외부 소스에서 이미지를 프록시하고 캐시합니다. 캐시된 이미지는 구성 폴더에 저장됩니다. <code>{appDataPath}/cache/images</code> 에서 파일을 찾을 수 있습니다.", "components.Settings.SettingsJobsCache.imagecacheDescription": "설정에서 활성화하면 Jellyseerr는 미리 구성된 외부 소스에서 이미지를 프록시하고 캐시합니다. 캐시된 이미지는 구성 폴더에 저장됩니다. <code>{appDataPath}/cache/images</code> 에서 파일을 찾을 수 있습니다.",
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "작업을 저장하는 동안 문제가 발생했습니다.", "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "작업을 저장하는 동안 문제가 발생했습니다.",
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "작업이 성공적으로 수정되었습니다!", "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "작업이 성공적으로 수정되었습니다!",
"components.Settings.SettingsJobsCache.jobs": "작업", "components.Settings.SettingsJobsCache.jobs": "작업",
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr는 특정 유지 관리 작업을 정기적으로 예약된 작업으로 수행하지만 아래에서 수동으로 트리거할 수도 있습니다. 작업을 수동으로 실행해도 일정이 변경되지는 않습니다.", "components.Settings.SettingsJobsCache.jobsDescription": "Jellyseerr는 특정 유지 관리 작업을 정기적으로 예약된 작업으로 수행하지만 아래에서 수동으로 트리거할 수도 있습니다. 작업을 수동으로 실행해도 일정이 변경되지는 않습니다.",
"components.Settings.SettingsJobsCache.jobtype": "유형", "components.Settings.SettingsJobsCache.jobtype": "유형",
"components.Settings.SettingsJobsCache.plex-full-scan": "Plex 전체 라이브러리 스캔", "components.Settings.SettingsJobsCache.plex-full-scan": "Plex 전체 라이브러리 스캔",
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex 최근 추가 스캔", "components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex 최근 추가 스캔",
@@ -420,7 +420,7 @@
"components.Settings.is4k": "4K", "components.Settings.is4k": "4K",
"components.Settings.manualscan": "수동으로 라이브러리 스캔", "components.Settings.manualscan": "수동으로 라이브러리 스캔",
"components.Settings.mediaTypeMovie": "영화", "components.Settings.mediaTypeMovie": "영화",
"components.Settings.manualscanDescription": "일반적으로 이는 24시간에 한 번 실행됩니다. Overseerr는 Plex 서버의 최근 추가 항목을 더 적극적으로 확인합니다. 만약 Plex를 처음 구성하는 경우, 한번은 전체 수동 라이브러리 스캔을 권장합니다!", "components.Settings.manualscanDescription": "일반적으로 이는 24시간에 한 번 실행됩니다. Jellyseerr는 Plex 서버의 최근 추가 항목을 더 적극적으로 확인합니다. 만약 Plex를 처음 구성하는 경우, 한번은 전체 수동 라이브러리 스캔을 권장합니다!",
"components.Settings.mediaTypeSeries": "시리즈", "components.Settings.mediaTypeSeries": "시리즈",
"components.Settings.menuAbout": "정보", "components.Settings.menuAbout": "정보",
"components.Settings.menuGeneralSettings": "일반", "components.Settings.menuGeneralSettings": "일반",
@@ -431,10 +431,10 @@
"components.Settings.noDefaultNon4kServer": "비-4K 및 4K 콘텐츠를 전부 처리하는 유일한 {serverType} 서버가 있는 경우(또는 4K 콘텐츠만 다운로드하는 경우), {serverType} 서버는 4K 서버로 지정되어서는 <strong>안됩니다</strong>.", "components.Settings.noDefaultNon4kServer": "비-4K 및 4K 콘텐츠를 전부 처리하는 유일한 {serverType} 서버가 있는 경우(또는 4K 콘텐츠만 다운로드하는 경우), {serverType} 서버는 4K 서버로 지정되어서는 <strong>안됩니다</strong>.",
"components.Settings.noDefaultServer": "{mediaType} 요청을 처리하기 위해서는 적어도 하나 이상의 {serverType} 서버를 기본 설정해야 합니다.", "components.Settings.noDefaultServer": "{mediaType} 요청을 처리하기 위해서는 적어도 하나 이상의 {serverType} 서버를 기본 설정해야 합니다.",
"components.Settings.notifications": "알림", "components.Settings.notifications": "알림",
"components.Settings.plexlibrariesDescription": "Overseerr에서 타이틀을 스캔하는 라이브러리입니다. Plex 연결 설정을 설정하고 저장한 후, 라이브러리가 표시되지 않는 경우 아래 버튼을 클릭하세요.", "components.Settings.plexlibrariesDescription": "Jellyseerr에서 타이틀을 스캔하는 라이브러리입니다. Plex 연결 설정을 설정하고 저장한 후, 라이브러리가 표시되지 않는 경우 아래 버튼을 클릭하세요.",
"components.Settings.port": "포트", "components.Settings.port": "포트",
"components.Settings.radarrsettings": "Radarr 설정", "components.Settings.radarrsettings": "Radarr 설정",
"components.Settings.restartrequiredTooltip": "이 변경된 설정이 적용되려면 Overseerr를 재시작해야 합니다", "components.Settings.restartrequiredTooltip": "이 변경된 설정이 적용되려면 Jellyseerr를 재시작해야 합니다",
"components.Settings.serverRemote": "원격", "components.Settings.serverRemote": "원격",
"components.Settings.serverSecure": "보안", "components.Settings.serverSecure": "보안",
"components.Settings.serverpreset": "서버", "components.Settings.serverpreset": "서버",
@@ -598,7 +598,7 @@
"components.IssueList.showallissues": "모든 이슈 표시", "components.IssueList.showallissues": "모든 이슈 표시",
"components.IssueModal.CreateIssueModal.toastFailedCreate": "이슈를 제출하는 동안에 문제가 발생했습니다.", "components.IssueModal.CreateIssueModal.toastFailedCreate": "이슈를 제출하는 동안에 문제가 발생했습니다.",
"components.Layout.LanguagePicker.displaylanguage": "표시 언어", "components.Layout.LanguagePicker.displaylanguage": "표시 언어",
"components.Layout.VersionStatus.streamdevelop": "Overseerr 개발", "components.Layout.VersionStatus.streamdevelop": "Jellyseerr 개발",
"components.ManageSlideOver.tvshow": "시리즈", "components.ManageSlideOver.tvshow": "시리즈",
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {재생} other {재생}}", "components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {재생} other {재생}}",
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes 시청자 점수", "components.MovieDetails.rtaudiencescore": "Rotten Tomatoes 시청자 점수",
@@ -690,7 +690,7 @@
"components.Layout.Sidebar.issues": "이슈", "components.Layout.Sidebar.issues": "이슈",
"components.Layout.UserDropdown.myprofile": "프로필", "components.Layout.UserDropdown.myprofile": "프로필",
"components.Login.loginerror": "로그인을 시도하는 중에 문제가 발생했습니다.", "components.Login.loginerror": "로그인을 시도하는 중에 문제가 발생했습니다.",
"components.Layout.VersionStatus.streamstable": "Overseerr 안정", "components.Layout.VersionStatus.streamstable": "Jellyseerr 안정",
"components.Login.signingin": "로그인 중…", "components.Login.signingin": "로그인 중…",
"components.ManageSlideOver.manageModalIssues": "진행 중인 이슈", "components.ManageSlideOver.manageModalIssues": "진행 중인 이슈",
"components.ManageSlideOver.manageModalMedia": "미디어", "components.ManageSlideOver.manageModalMedia": "미디어",
@@ -780,7 +780,7 @@
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet 테스트 알림이 전송되었습니다!", "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet 테스트 알림이 전송되었습니다!",
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Pushbullet 테스트 알림을 보내지 못했습니다.", "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Pushbullet 테스트 알림을 보내지 못했습니다.",
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Pushbullet 테스트 알림 보내기…", "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Pushbullet 테스트 알림 보내기…",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Overseerr와 함께 사용할 <ApplicationRegistrationLink>애플리케이션 등록</ApplicationRegistrationLink>", "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Jellyseerr와 함께 사용할 <ApplicationRegistrationLink>애플리케이션 등록</ApplicationRegistrationLink>",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "에이전트 활성화", "components.Settings.Notifications.NotificationsPushover.agentenabled": "에이전트 활성화",
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "적어도 하나 이상의 알림 유형을 선택해야 합니다", "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "적어도 하나 이상의 알림 유형을 선택해야 합니다",
"components.Settings.Notifications.NotificationsPushover.accessToken": "애플리케이션 API 토큰", "components.Settings.Notifications.NotificationsPushover.accessToken": "애플리케이션 API 토큰",
@@ -796,7 +796,7 @@
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "유효한 JSON 페이로드를 입력해야 합니다", "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "유효한 JSON 페이로드를 입력해야 합니다",
"components.Settings.Notifications.allowselfsigned": "자체 서명된 인증서 허용", "components.Settings.Notifications.allowselfsigned": "자체 서명된 인증서 허용",
"components.Settings.Notifications.authUser": "SMTP 사용자 이름", "components.Settings.Notifications.authUser": "SMTP 사용자 이름",
"components.Settings.Notifications.botApiTip": "Overseerr와 함께 사용할 <CreateBotLink>봇 생성</CreateBotLink>이 필요합니다", "components.Settings.Notifications.botApiTip": "Jellyseerr와 함께 사용할 <CreateBotLink>봇 생성</CreateBotLink>이 필요합니다",
"components.Settings.Notifications.botUsername": "봇 사용자 이름", "components.Settings.Notifications.botUsername": "봇 사용자 이름",
"components.Settings.Notifications.chatIdTip": "봇과 채팅을 시작하고 <GetIdBotLink>@get_id_bot</GetIdBotLink>, <code>/my_id</code> 명령을 실행하세요", "components.Settings.Notifications.chatIdTip": "봇과 채팅을 시작하고 <GetIdBotLink>@get_id_bot</GetIdBotLink>, <code>/my_id</code> 명령을 실행하세요",
"components.Settings.Notifications.discordsettingsfailed": "Discord 알림 설정을 저장하지 못했습니다.", "components.Settings.Notifications.discordsettingsfailed": "Discord 알림 설정을 저장하지 못했습니다.",
@@ -840,7 +840,7 @@
"components.Settings.SettingsMain.originallanguageTip": "원작 언어로 콘텐츠 필터링", "components.Settings.SettingsMain.originallanguageTip": "원작 언어로 콘텐츠 필터링",
"components.Settings.SettingsMain.partialRequestsEnabled": "부분 시리즈 요청 허용", "components.Settings.SettingsMain.partialRequestsEnabled": "부분 시리즈 요청 허용",
"components.Settings.SettingsMain.trustProxy": "프록시 지원 활성화", "components.Settings.SettingsMain.trustProxy": "프록시 지원 활성화",
"components.Settings.SettingsMain.trustProxyTip": "프록시 뒤에서 클라이언트 IP 주소를 정확하게 등록하도록 Overseerr에 허용", "components.Settings.SettingsMain.trustProxyTip": "프록시 뒤에서 클라이언트 IP 주소를 정확하게 등록하도록 Jellyseerr에 허용",
"components.Settings.SettingsUsers.localLoginTip": "사용자가 Plex OAuth 대신 이메일 주소와 암호를 사용하여 로그인하도록 허용", "components.Settings.SettingsUsers.localLoginTip": "사용자가 Plex OAuth 대신 이메일 주소와 암호를 사용하여 로그인하도록 허용",
"components.Settings.SettingsUsers.movieRequestLimitLabel": "전역 영화 요청 제한", "components.Settings.SettingsUsers.movieRequestLimitLabel": "전역 영화 요청 제한",
"components.Settings.SettingsUsers.toastSettingsSuccess": "사용자 설정이 성공적으로 저장되었습니다!", "components.Settings.SettingsUsers.toastSettingsSuccess": "사용자 설정이 성공적으로 저장되었습니다!",
@@ -1081,6 +1081,7 @@
"components.Settings.Notifications.encryptionTip": "대부분의 경우 암시적 TLS는 465 포트를 사용하고 STARTTLS는 587 포트를 사용합니다", "components.Settings.Notifications.encryptionTip": "대부분의 경우 암시적 TLS는 465 포트를 사용하고 STARTTLS는 587 포트를 사용합니다",
"components.Settings.SettingsAbout.Releases.versionChangelog": "{version} 변경 로그", "components.Settings.SettingsAbout.Releases.versionChangelog": "{version} 변경 로그",
"components.Settings.SettingsAbout.supportoverseerr": "Overseerr 지원", "components.Settings.SettingsAbout.supportoverseerr": "Overseerr 지원",
"components.Settings.SettingsAbout.supportjellyseerr": "Jellyseerr 지원",
"components.Settings.SettingsAbout.uptodate": "최신", "components.Settings.SettingsAbout.uptodate": "최신",
"components.Settings.SettingsAbout.githubdiscussions": "GitHub 토론", "components.Settings.SettingsAbout.githubdiscussions": "GitHub 토론",
"components.Settings.SettingsAbout.version": "버전", "components.Settings.SettingsAbout.version": "버전",
@@ -1098,7 +1099,7 @@
"components.Settings.SettingsLogs.extraData": "추가 데이터", "components.Settings.SettingsLogs.extraData": "추가 데이터",
"components.Settings.SettingsLogs.time": "타임스탬프", "components.Settings.SettingsLogs.time": "타임스탬프",
"components.Settings.SettingsMain.cacheImages": "이미지 캐싱 활성화", "components.Settings.SettingsMain.cacheImages": "이미지 캐싱 활성화",
"components.Settings.SettingsMain.generalsettingsDescription": "Overseerr에 대한 전역 및 기본 설정을 구성합니다.", "components.Settings.SettingsMain.generalsettingsDescription": "Jellyseerr에 대한 전역 및 기본 설정을 구성합니다.",
"components.Settings.SettingsLogs.viewdetails": "세부 정보 보기", "components.Settings.SettingsLogs.viewdetails": "세부 정보 보기",
"components.Settings.SettingsMain.applicationurl": "애플리케이션 URL", "components.Settings.SettingsMain.applicationurl": "애플리케이션 URL",
"components.Settings.SettingsMain.apikey": "API 키", "components.Settings.SettingsMain.apikey": "API 키",
@@ -1138,7 +1139,7 @@
"components.Settings.librariesRemaining": "남은 라이브러리: {count}", "components.Settings.librariesRemaining": "남은 라이브러리: {count}",
"components.Settings.noDefault4kServer": "4K {serverType} 서버는 사용자가 4K {mediaType} 요청을 제출할 수 있도록 기본 설정되어야 합니다.", "components.Settings.noDefault4kServer": "4K {serverType} 서버는 사용자가 4K {mediaType} 요청을 제출할 수 있도록 기본 설정되어야 합니다.",
"components.Settings.scan": "라이브러리 동기화", "components.Settings.scan": "라이브러리 동기화",
"components.Settings.plexsettingsDescription": "Plex 서버의 설정을 구성하세요. Overseerr는 Plex 라이브러리를 스캔하여 콘텐츠의 사용 가능성을 판단합니다.", "components.Settings.plexsettingsDescription": "Plex 서버의 설정을 구성하세요. Jellyseerr는 Plex 라이브러리를 스캔하여 콘텐츠의 사용 가능성을 판단합니다.",
"components.Settings.notificationsettings": "알림 설정", "components.Settings.notificationsettings": "알림 설정",
"components.Settings.plexsettings": "Plex 설정", "components.Settings.plexsettings": "Plex 설정",
"components.Settings.notificationAgentSettingsDescription": "알림 에이전트를 구성하고 활성화합니다.", "components.Settings.notificationAgentSettingsDescription": "알림 에이전트를 구성하고 활성화합니다.",
@@ -1155,7 +1156,7 @@
"components.Settings.webAppUrlTip": "사용자에게 \"호스팅된\" 웹 앱 대신 서버의 웹 앱으로 이동하도록 선택적으로 설정할 수 있습니다", "components.Settings.webAppUrlTip": "사용자에게 \"호스팅된\" 웹 앱 대신 서버의 웹 앱으로 이동하도록 선택적으로 설정할 수 있습니다",
"components.Setup.setup": "설정", "components.Setup.setup": "설정",
"components.Setup.tip": "팁", "components.Setup.tip": "팁",
"components.Setup.welcome": "Overseerr에 오신 것을 환영합니다", "components.Setup.welcome": "Jellyseerr에 오신 것을 환영합니다",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.StatusBadge.status": "{status}", "components.StatusBadge.status": "{status}",
"components.TvDetails.play4konplex": "Plex에서 4K로 재생", "components.TvDetails.play4konplex": "Plex에서 4K로 재생",
@@ -1226,7 +1227,7 @@
"pages.errormessagewithcode": "{statusCode} - {error}", "pages.errormessagewithcode": "{statusCode} - {error}",
"pages.serviceunavailable": "서비스를 사용할 수 없음", "pages.serviceunavailable": "서비스를 사용할 수 없음",
"components.Settings.settingUpPlexDescription": "Plex를 설정하려면, 세부 정보를 수동으로 입력하거나 <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>에서 검색된 서버를 선택할 수 있습니다. 사용 가능한 서버 목록을 불러오려면 드롭다운 오른쪽에 있는 버튼을 누르세요.", "components.Settings.settingUpPlexDescription": "Plex를 설정하려면, 세부 정보를 수동으로 입력하거나 <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>에서 검색된 서버를 선택할 수 있습니다. 사용 가능한 서버 목록을 불러오려면 드롭다운 오른쪽에 있는 버튼을 누르세요.",
"components.Settings.tautulliSettingsDescription": "선택적으로 Tautulli 서버의 설정을 구성하세요. Overseerr는 Tautulli로부터 Plex 미디어의 시청 기록 데이터를 불러옵니다.", "components.Settings.tautulliSettingsDescription": "선택적으로 Tautulli 서버의 설정을 구성하세요. Jellyseerr는 Tautulli로부터 Plex 미디어의 시청 기록 데이터를 불러옵니다.",
"components.RequestBlock.requestdate": "요청 일자", "components.RequestBlock.requestdate": "요청 일자",
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# 선택한 필터} other {# 선택한 필터}}", "components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# 선택한 필터} other {# 선택한 필터}}",
"components.QuotaSelector.seasons": "{count, plural, one {시즌} other {시즌}}", "components.QuotaSelector.seasons": "{count, plural, one {시즌} other {시즌}}",

View File

@@ -223,7 +223,7 @@
"components.TvDetails.network": "{networkCount, plural, one {Netwerk} other {Netwerken}}", "components.TvDetails.network": "{networkCount, plural, one {Netwerk} other {Netwerken}}",
"components.TvDetails.firstAirDate": "Datum eerste uitzending", "components.TvDetails.firstAirDate": "Datum eerste uitzending",
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
"components.StatusChacker.reloadJellyseerr": "Herladen", "components.StatusChacker.reloadOverseerr": "Herladen",
"components.StatusChacker.newversionavailable": "Toepassingsupdate", "components.StatusChacker.newversionavailable": "Toepassingsupdate",
"components.StatusChacker.newversionDescription": "Jellyseerr is geüpdatet! Klik op de onderstaande knop om de pagina opnieuw te laden.", "components.StatusChacker.newversionDescription": "Jellyseerr is geüpdatet! Klik op de onderstaande knop om de pagina opnieuw te laden.",
"components.Settings.toastSettingsSuccess": "Instellingen succesvol opgeslagen!", "components.Settings.toastSettingsSuccess": "Instellingen succesvol opgeslagen!",

View File

@@ -332,7 +332,7 @@
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
"components.TvDetails.TvCrew.fullseriescrew": "Equipa Técnica Completa da Série", "components.TvDetails.TvCrew.fullseriescrew": "Equipa Técnica Completa da Série",
"components.TvDetails.TvCast.fullseriescast": "Elenco Completo da Série", "components.TvDetails.TvCast.fullseriescast": "Elenco Completo da Série",
"components.StatusChacker.reloadJellyseerr": "Recarregar", "components.StatusChacker.reloadOverseerr": "Recarregar",
"components.StatusChacker.newversionavailable": "Atualização de Aplicação", "components.StatusChacker.newversionavailable": "Atualização de Aplicação",
"components.StatusChacker.newversionDescription": "Jellyseerr foi atualizado! Clique no botão abaixo para recarregar a página.", "components.StatusChacker.newversionDescription": "Jellyseerr foi atualizado! Clique no botão abaixo para recarregar a página.",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",

View File

@@ -1092,7 +1092,7 @@
"components.RequestList.RequestItem.tvdbid": "TheTVDB ID", "components.RequestList.RequestItem.tvdbid": "TheTVDB ID",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Synkronisering av Plex Watchlist", "components.Settings.SettingsJobsCache.plex-watchlist-sync": "Synkronisering av Plex Watchlist",
"components.Settings.advancedTooltip": "Om du konfigurerar den här inställningen felaktigt kan det leda till att funktionerna inte fungerar", "components.Settings.advancedTooltip": "Om du konfigurerar den här inställningen felaktigt kan det leda till att funktionerna inte fungerar",
"components.Settings.restartrequiredTooltip": "Overseerr måste startas om för att ändringarna i den här inställningen ska träda i kraft", "components.Settings.restartrequiredTooltip": "Jellyseerr måste startas om för att ändringarna i den här inställningen ska träda i kraft",
"components.TvDetails.reportissue": "Rapportera ett problem", "components.TvDetails.reportissue": "Rapportera ett problem",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Begär automatiskt serier", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Begär automatiskt serier",
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Begär automatiskt serier på din <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink>", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Begär automatiskt serier på din <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink>",
@@ -1103,7 +1103,7 @@
"components.StatusBadge.playonplex": "Spela upp på Plex", "components.StatusBadge.playonplex": "Spela upp på Plex",
"components.TitleCard.tvdbid": "TheTVDB ID", "components.TitleCard.tvdbid": "TheTVDB ID",
"components.Settings.SettingsLogs.viewdetails": "Visa detaljer", "components.Settings.SettingsLogs.viewdetails": "Visa detaljer",
"components.Settings.SettingsJobsCache.imagecacheDescription": "När det är aktiverat i inställningarna kommer Overseerr att göra proxy- och cache-bilder från förkonfigurerade externa källor. Cachade bilder sparas i din konfigurationsmapp. Du hittar filerna i <code>{appDataPath}/cache/images</code>.", "components.Settings.SettingsJobsCache.imagecacheDescription": "När det är aktiverat i inställningarna kommer Jellyseerrr att göra proxy- och cache-bilder från förkonfigurerade externa källor. Cachade bilder sparas i din konfigurationsmapp. Du hittar filerna i <code>{appDataPath}/cache/images</code>.",
"components.TitleCard.cleardata": "Rensa data", "components.TitleCard.cleardata": "Rensa data",
"components.TitleCard.mediaerror": "{mediaType} Hittades inte", "components.TitleCard.mediaerror": "{mediaType} Hittades inte",
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer", "components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
@@ -1183,7 +1183,7 @@
"components.Settings.SettingsMain.csrfProtectionTip": "Ställ in extern API-åtkomst till skrivskyddad (kräver HTTPS)", "components.Settings.SettingsMain.csrfProtectionTip": "Ställ in extern API-åtkomst till skrivskyddad (kräver HTTPS)",
"components.Settings.SettingsMain.general": "Allmänt", "components.Settings.SettingsMain.general": "Allmänt",
"components.Settings.SettingsMain.generalsettings": "Generella inställningar", "components.Settings.SettingsMain.generalsettings": "Generella inställningar",
"components.Settings.SettingsMain.generalsettingsDescription": "Konfigurera globala och standard-inställningar för Overseerr.", "components.Settings.SettingsMain.generalsettingsDescription": "Konfigurera globala och standard-inställningar för Jellyseerr.",
"components.Settings.SettingsMain.locale": "Visningsspråk", "components.Settings.SettingsMain.locale": "Visningsspråk",
"components.Settings.SettingsMain.originallanguage": "Upptäck språk", "components.Settings.SettingsMain.originallanguage": "Upptäck språk",
"components.Settings.SettingsMain.originallanguageTip": "Filtrera innehållet efter originalspråk", "components.Settings.SettingsMain.originallanguageTip": "Filtrera innehållet efter originalspråk",
@@ -1192,7 +1192,7 @@
"components.Settings.SettingsMain.regionTip": "Filtrera innehållet efter regional tillgänglighet", "components.Settings.SettingsMain.regionTip": "Filtrera innehållet efter regional tillgänglighet",
"components.Settings.SettingsMain.toastApiKeyFailure": "Något gick fel när du genererade en ny API-nyckel.", "components.Settings.SettingsMain.toastApiKeyFailure": "Något gick fel när du genererade en ny API-nyckel.",
"components.Settings.SettingsMain.toastApiKeySuccess": "Ny API-nyckel genererades framgångsrikt!", "components.Settings.SettingsMain.toastApiKeySuccess": "Ny API-nyckel genererades framgångsrikt!",
"components.Settings.SettingsMain.trustProxyTip": "Tillåt Overseerr att korrekt registrera klienternas IP-adresser bakom en proxy", "components.Settings.SettingsMain.trustProxyTip": "Tillåt Jellyseerr att korrekt registrera klienternas IP-adresser bakom en proxy",
"components.Discover.resetwarning": "Återställer alla skjutreglage till standardvärdet. Detta raderar också alla anpassade skjutreglage!", "components.Discover.resetwarning": "Återställer alla skjutreglage till standardvärdet. Detta raderar också alla anpassade skjutreglage!",
"components.Discover.stopediting": "Sluta redigera", "components.Discover.stopediting": "Sluta redigera",
"components.Discover.tmdbmoviegenre": "TMDB filmgenre", "components.Discover.tmdbmoviegenre": "TMDB filmgenre",