mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Merge remote-tracking branch 'og/develop' into v1.7.0/postgresql
This commit is contained in:
@@ -295,6 +295,33 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aleksasiriski",
|
||||
"name": "Aleksa Siriški",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31509435?v=4",
|
||||
"profile": "https://aleksasiriski.dev",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Danish-H",
|
||||
"name": "Danish Humair",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/121830048?v=4",
|
||||
"profile": "http://danishhumair.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "trackmastersteve",
|
||||
"name": "Stephen Harris",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16858514?v=4",
|
||||
"profile": "https://arm0.red",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
|
||||
23
README.md
23
README.md
@@ -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="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-31-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 -->
|
||||
|
||||
**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!_
|
||||
|
||||
@@ -40,11 +40,11 @@ With more features on the way! Check out our [issue tracker](https://github.com/
|
||||
|
||||
#### 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)
|
||||
|
||||
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
|
||||
|
||||
### Database configuration
|
||||
@@ -99,16 +99,16 @@ yarn run build
|
||||
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
|
||||
|
||||
**Pre-requisites:**
|
||||
|
||||
- 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
|
||||
|
||||
**Steps:**
|
||||
@@ -119,7 +119,7 @@ _to set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` i
|
||||
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
|
||||
git clone https://github.com/Fallenbagel/jellyseerr.git && cd jellyseerr
|
||||
@@ -138,9 +138,9 @@ yarn run build
|
||||
5. If you want to run jellyseerr as a _Systemd-service:_
|
||||
|
||||
- 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.
|
||||
@@ -260,6 +260,9 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdll23"><img src="https://avatars.githubusercontent.com/u/142844478?v=4?s=100" width="100px;" alt="Michael Dallinger"/><br /><sub><b>Michael Dallinger</b></sub></a><br /><a href="#translation-mdll23" title="Translation">🌍</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="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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import logger from '@server/logger';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
@@ -241,7 +242,9 @@ class JellyfinAPI {
|
||||
}
|
||||
}
|
||||
|
||||
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> {
|
||||
public async getItemData(
|
||||
id: string
|
||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${this.userId}/Items/${id}`
|
||||
@@ -249,6 +252,11 @@ class JellyfinAPI {
|
||||
|
||||
return contents.data;
|
||||
} catch (e) {
|
||||
if (availabilitySync.running) {
|
||||
if (e.response && e.response.status === 500) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
@@ -261,9 +269,7 @@ class JellyfinAPI {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
|
||||
|
||||
return contents.data.Items.filter(
|
||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||
);
|
||||
return contents.data.Items;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import {
|
||||
@@ -167,7 +168,7 @@ export const startJobs = (): void => {
|
||||
});
|
||||
|
||||
// Checks if media is still available in plex/sonarr/radarr libs
|
||||
/* scheduledJobs.push({
|
||||
scheduledJobs.push({
|
||||
id: 'availability-sync',
|
||||
name: 'Media Availability Sync',
|
||||
type: 'process',
|
||||
@@ -182,7 +183,6 @@ export const startJobs = (): void => {
|
||||
running: () => availabilitySync.running,
|
||||
cancelFn: () => availabilitySync.cancel(),
|
||||
});
|
||||
*/
|
||||
|
||||
// Run download sync every minute
|
||||
scheduledJobs.push({
|
||||
|
||||
@@ -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 PlexAPI from '@server/api/plexapi';
|
||||
import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
|
||||
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import MediaRequest from '@server/entity/MediaRequest';
|
||||
@@ -18,14 +21,20 @@ class AvailabilitySync {
|
||||
public running = false;
|
||||
private plexClient: PlexAPI;
|
||||
private plexSeasonsCache: Record<string, PlexMetadata[]>;
|
||||
|
||||
private jellyfinClient: JellyfinAPI;
|
||||
private jellyfinSeasonsCache: Record<string, JellyfinLibraryItem[]>;
|
||||
|
||||
private sonarrSeasonsCache: Record<string, SonarrSeason[]>;
|
||||
private radarrServers: RadarrSettings[];
|
||||
private sonarrServers: SonarrSettings[];
|
||||
|
||||
async run() {
|
||||
const settings = getSettings();
|
||||
const mediaServerType = getSettings().main.mediaServerType;
|
||||
this.running = true;
|
||||
this.plexSeasonsCache = {};
|
||||
this.jellyfinSeasonsCache = {};
|
||||
this.sonarrSeasonsCache = {};
|
||||
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
|
||||
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
|
||||
@@ -37,13 +46,53 @@ class AvailabilitySync {
|
||||
const pageSize = 50;
|
||||
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (admin) {
|
||||
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
||||
// If it is plex admin is selected using plexToken if jellyfin admin is selected using jellyfinUserID
|
||||
|
||||
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 {
|
||||
logger.error('An admin is not configured.');
|
||||
}
|
||||
@@ -60,41 +109,84 @@ class AvailabilitySync {
|
||||
let movieExists = false;
|
||||
let movieExists4k = false;
|
||||
|
||||
const { existsInPlex } = await this.mediaExistsInPlex(media, false);
|
||||
const { existsInPlex: existsInPlex4k } = await this.mediaExistsInPlex(
|
||||
media,
|
||||
true
|
||||
);
|
||||
// if (mediaServerType === MediaServerType.PLEX) {
|
||||
// await this.mediaExistsInPlex(media, false);
|
||||
// } else if (
|
||||
// mediaServerType === MediaServerType.JELLYFIN ||
|
||||
// mediaServerType === MediaServerType.EMBY
|
||||
// ) {
|
||||
// await this.mediaExistsInJellyfin(media, false);
|
||||
// }
|
||||
|
||||
const existsInRadarr = await this.mediaExistsInRadarr(media, false);
|
||||
const existsInRadarr4k = await this.mediaExistsInRadarr(media, true);
|
||||
|
||||
if (existsInPlex || existsInRadarr) {
|
||||
movieExists = true;
|
||||
logger.info(
|
||||
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
// plex
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
const { existsInPlex } = await this.mediaExistsInPlex(media, false);
|
||||
const { existsInPlex: existsInPlex4k } =
|
||||
await this.mediaExistsInPlex(media, true);
|
||||
|
||||
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) {
|
||||
movieExists4k = true;
|
||||
logger.info(
|
||||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
//jellyfin
|
||||
if (
|
||||
mediaServerType === MediaServerType.JELLYFIN ||
|
||||
mediaServerType === MediaServerType.EMBY
|
||||
) {
|
||||
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) {
|
||||
await this.mediaUpdater(media, false);
|
||||
await this.mediaUpdater(media, false, mediaServerType);
|
||||
}
|
||||
|
||||
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 showExists4k = false;
|
||||
|
||||
//plex
|
||||
|
||||
const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } =
|
||||
await this.mediaExistsInPlex(media, false);
|
||||
const {
|
||||
@@ -111,6 +205,16 @@ class AvailabilitySync {
|
||||
seasonsMap: plexSeasonsMap4k = new Map(),
|
||||
} = 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 } =
|
||||
await this.mediaExistsInSonarr(media, false);
|
||||
const {
|
||||
@@ -118,24 +222,60 @@ class AvailabilitySync {
|
||||
seasonsMap: sonarrSeasonsMap4k,
|
||||
} = await this.mediaExistsInSonarr(media, true);
|
||||
|
||||
if (existsInPlex || existsInSonarr) {
|
||||
showExists = true;
|
||||
logger.info(
|
||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
//plex
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
if (existsInPlex || existsInSonarr) {
|
||||
showExists = true;
|
||||
logger.info(
|
||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsInPlex4k || existsInSonarr4k) {
|
||||
showExists4k = true;
|
||||
logger.info(
|
||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
}
|
||||
);
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
if (existsInPlex4k || existsInSonarr4k) {
|
||||
showExists4k = true;
|
||||
logger.info(
|
||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
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
|
||||
@@ -155,11 +295,45 @@ class AvailabilitySync {
|
||||
filteredSeasonsMap.set(season.seasonNumber, false)
|
||||
);
|
||||
|
||||
const finalSeasons = new Map([
|
||||
...filteredSeasonsMap,
|
||||
...plexSeasonsMap,
|
||||
...sonarrSeasonsMap,
|
||||
]);
|
||||
// non-4k
|
||||
const finalSeasons: Map<number, boolean> = new Map();
|
||||
|
||||
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();
|
||||
|
||||
@@ -173,18 +347,64 @@ class AvailabilitySync {
|
||||
filteredSeasonsMap4k.set(season.seasonNumber, false)
|
||||
);
|
||||
|
||||
const finalSeasons4k = new Map([
|
||||
...filteredSeasonsMap4k,
|
||||
...plexSeasonsMap4k,
|
||||
...sonarrSeasonsMap4k,
|
||||
]);
|
||||
// 4k
|
||||
const finalSeasons4k: Map<number, boolean> = new Map();
|
||||
|
||||
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)) {
|
||||
await this.seasonUpdater(media, finalSeasons, false);
|
||||
await this.seasonUpdater(
|
||||
media,
|
||||
finalSeasons,
|
||||
false,
|
||||
mediaServerType
|
||||
);
|
||||
}
|
||||
|
||||
if ([...finalSeasons4k.values()].includes(false)) {
|
||||
await this.seasonUpdater(media, finalSeasons4k, true);
|
||||
await this.seasonUpdater(
|
||||
media,
|
||||
finalSeasons4k,
|
||||
true,
|
||||
mediaServerType
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -192,7 +412,7 @@ class AvailabilitySync {
|
||||
(media.status === MediaStatus.AVAILABLE ||
|
||||
media.status === MediaStatus.PARTIALLY_AVAILABLE)
|
||||
) {
|
||||
await this.mediaUpdater(media, false);
|
||||
await this.mediaUpdater(media, false, mediaServerType);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -200,7 +420,7 @@ class AvailabilitySync {
|
||||
(media.status4k === MediaStatus.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;
|
||||
}
|
||||
|
||||
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 requestRepository = getRepository(MediaRequest);
|
||||
|
||||
@@ -320,17 +544,32 @@ class AvailabilitySync {
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
||||
: null;
|
||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||
: null;
|
||||
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||
: undefined;
|
||||
} else if (
|
||||
mediaServerType === MediaServerType.JELLYFIN ||
|
||||
mediaServerType === MediaServerType.EMBY
|
||||
) {
|
||||
media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId']
|
||||
: undefined;
|
||||
}
|
||||
logger.info(
|
||||
`The ${is4k ? '4K' : 'non-4K'} ${
|
||||
media.mediaType === 'movie' ? 'movie' : 'show'
|
||||
} [TMDB ID ${media.tmdbId}] was not found in any ${
|
||||
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' }
|
||||
);
|
||||
|
||||
@@ -358,7 +597,8 @@ class AvailabilitySync {
|
||||
private async seasonUpdater(
|
||||
media: Media,
|
||||
seasons: Map<number, boolean>,
|
||||
is4k: boolean
|
||||
is4k: boolean,
|
||||
mediaServerType: MediaServerType
|
||||
): Promise<void> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||
@@ -370,6 +610,8 @@ class AvailabilitySync {
|
||||
);
|
||||
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
||||
|
||||
// let isSeasonRemoved = false;
|
||||
|
||||
try {
|
||||
// Need to check and see if there are any related season
|
||||
// requests. If they are, we will need to delete them.
|
||||
@@ -420,7 +662,13 @@ class AvailabilitySync {
|
||||
media.tmdbId
|
||||
}] was not found in any ${
|
||||
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' }
|
||||
);
|
||||
} catch (ex) {
|
||||
@@ -604,6 +852,7 @@ class AvailabilitySync {
|
||||
return seasonExists;
|
||||
}
|
||||
|
||||
// Plex
|
||||
private async mediaExistsInPlex(
|
||||
media: Media,
|
||||
is4k: boolean
|
||||
@@ -719,6 +968,123 @@ class AvailabilitySync {
|
||||
|
||||
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();
|
||||
|
||||
@@ -62,7 +62,7 @@ class JellyfinScanner {
|
||||
const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
|
||||
const newMedia = new Media();
|
||||
|
||||
if (!metadata.Id) {
|
||||
if (!metadata?.Id) {
|
||||
logger.debug('No Id metadata for this title. Skipping', {
|
||||
label: 'Plex Sync',
|
||||
ratingKey: jellyfinitem.Id,
|
||||
@@ -197,6 +197,14 @@ class JellyfinScanner {
|
||||
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.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) {
|
||||
tvShow = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||
@@ -275,7 +283,7 @@ class JellyfinScanner {
|
||||
episode.Id
|
||||
);
|
||||
|
||||
ExtendedEpisodeData.MediaSources?.some((MediaSource) => {
|
||||
ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
if (MediaStream.Type === 'Video') {
|
||||
if ((MediaStream.Width ?? 0) >= 2000) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -274,24 +275,82 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
if (!user && !(await userRepository.count())) {
|
||||
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
|
||||
if (user.jellyfinAuthToken !== account.AccessToken) {
|
||||
user.jellyfinAuthToken = account.AccessToken;
|
||||
}
|
||||
|
||||
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
||||
if (account.User.PrimaryImageTag) {
|
||||
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||
} else {
|
||||
user.avatar = '/os_logo_square.png';
|
||||
user.avatar = gravatarUrl(user.email, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
}
|
||||
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
|
||||
if (user.username === account.User.Name) {
|
||||
user.username = '';
|
||||
}
|
||||
|
||||
// If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY
|
||||
if (process.env.JELLYFIN_TYPE === 'emby') {
|
||||
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||
settings.save();
|
||||
}
|
||||
|
||||
await userRepository.save(user);
|
||||
} else if (!settings.main.newPlexLogin) {
|
||||
logger.warn(
|
||||
@@ -307,69 +366,38 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
status: 403,
|
||||
message: 'Access denied.',
|
||||
});
|
||||
} else {
|
||||
// Here we check if it's the first user. If it is, we create the user with no check
|
||||
// and give them admin permissions
|
||||
const totalUsers = await userRepository.count();
|
||||
if (totalUsers === 0) {
|
||||
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 = new User({
|
||||
email: body.email,
|
||||
} else if (!user) {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating new Overseerr user',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
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) {
|
||||
if (!body.email) {
|
||||
throw new Error('add_email');
|
||||
}
|
||||
|
||||
user = new User({
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: '/os_logo_square.png',
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
//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);
|
||||
user = new User({
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
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,
|
||||
});
|
||||
//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);
|
||||
}
|
||||
|
||||
// Set logged in session
|
||||
@@ -400,6 +428,11 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
|
||||
});
|
||||
} else if (e.message === 'select_server_type') {
|
||||
return next({
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
|
||||
});
|
||||
} else {
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getAppVersion } from '@server/utils/appVersion';
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
||||
import { rescheduleJob } from 'node-schedule';
|
||||
import path from 'path';
|
||||
@@ -337,7 +338,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
id: user.Id,
|
||||
thumb: user.PrimaryImageTag
|
||||
? `${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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -537,7 +537,10 @@ router.post(
|
||||
permissions: settings.main.defaultPermissions,
|
||||
avatar: jellyfinUser?.PrimaryImageTag
|
||||
? `${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,
|
||||
});
|
||||
|
||||
|
||||
20
src/assets/services/letterboxd.svg
Normal file
20
src/assets/services/letterboxd.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
@@ -1,6 +1,7 @@
|
||||
import EmbyLogo from '@app/assets/services/emby.svg';
|
||||
import ImdbLogo from '@app/assets/services/imdb.svg';
|
||||
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
|
||||
import LetterboxdLogo from '@app/assets/services/letterboxd.svg';
|
||||
import PlexLogo from '@app/assets/services/plex.svg';
|
||||
import RTLogo from '@app/assets/services/rt.svg';
|
||||
import TmdbLogo from '@app/assets/services/tmdb.svg';
|
||||
@@ -103,6 +104,16 @@ const ExternalLinkBlock = ({
|
||||
<TraktLogo />
|
||||
</a>
|
||||
)}
|
||||
{tmdbId && mediaType === MediaType.MOVIE && (
|
||||
<a
|
||||
href={`https://letterboxd.com/tmdb/${tmdbId}`}
|
||||
className="w-8 opacity-50 transition duration-300 hover:opacity-100"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<LetterboxdLogo />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user