added Support for Jellyfin Media Server

This commit is contained in:
Juan D. Jara
2021-09-27 02:56:02 +02:00
committed by notfakie
parent 5125abdbf0
commit 3661eea8bb
36 changed files with 2862 additions and 86 deletions

View File

@@ -1,6 +1,8 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import JellyfinAPI from '../api/jellyfin';
import PlexTvAPI from '../api/plextv';
import { MediaServerType } from '../constants/server';
import { UserType } from '../constants/user';
import { User } from '../entity/User';
import { Permission } from '../lib/permissions';
@@ -36,6 +38,13 @@ authRoutes.post('/plex', async (req, res, next) => {
message: 'Authentication token required.',
});
}
if (
settings.main.mediaServerType != MediaServerType.PLEX &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
) {
return res.status(500).json({ error: 'Plex login is disabled' });
}
try {
// First we need to use this auth token to get the user's email from plex.tv
const plextv = new PlexTvAPI(body.authToken);
@@ -171,6 +180,169 @@ authRoutes.post('/plex', async (req, res, next) => {
}
});
authRoutes.post('/jellyfin', async (req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);
const body = req.body as {
username?: string;
password?: string;
hostname?: string;
email?: string;
};
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.jellyfin.hostname !== ''
) {
return res.status(500).json({ error: 'Jellyfin login is disabled' });
} else if (!body.username || !body.password) {
return res
.status(500)
.json({ error: 'You must provide an username and a password' });
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
return res
.status(500)
.json({ error: 'Jellyfin hostname already configured' });
} else if (settings.jellyfin.hostname === '' && !body.hostname) {
return res.status(500).json({ error: 'No hostname provided.' });
}
try {
const hostname =
settings.jellyfin.hostname !== ''
? settings.jellyfin.hostname
: body.hostname;
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
let user = await userRepository.findOne({
where: { jellyfinUsername: body.username },
});
let deviceId = '';
if (user) {
deviceId = user.jellyfinDeviceId ?? '';
} else {
deviceId = Buffer.from(`BOT_overseerr_${body.username ?? ''}`).toString(
'base64'
);
}
// First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
const account = await jellyfinserver.login(body.username, body.password);
// Next let's see if the user already exists
user = await userRepository.findOne({
where: { jellyfinUserId: account.User.Id },
});
if (user) {
// 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 = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else {
user.avatar = '/os_logo_square.png';
}
user.jellyfinUsername = account.User.Name;
if (user.username === account.User.Name) {
user.username = '';
}
await userRepository.save(user);
} 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) {
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
? `${hostname}/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();
}
}
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
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
await userRepository.save(user);
}
}
// Set logged in session
if (req.session) {
req.session.userId = user?.id;
}
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
if (e.message === 'Unauthorized') {
logger.info(
'Failed login attempt from user with incorrect Jellyfin credentials',
{
label: 'Auth',
account: {
ip: req.ip,
email: body.username,
password: '__REDACTED__',
},
}
);
return next({
status: 401,
message: 'Unauthorized',
});
} else if (e.message === 'add_email') {
return next({
status: 406,
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
});
} else {
logger.error(e.message, { label: 'Auth' });
return next({
status: 500,
message: 'Something went wrong.',
});
}
}
});
authRoutes.post('/local', async (req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);

View File

@@ -7,6 +7,7 @@ import path from 'path';
import semver from 'semver';
import { getRepository } from 'typeorm';
import { URL } from 'url';
import JellyfinAPI from '../../api/jellyfin';
import PlexAPI from '../../api/plexapi';
import PlexTvAPI from '../../api/plextv';
import TautulliAPI from '../../api/tautulli';
@@ -19,11 +20,12 @@ import {
LogsResultsResponse,
SettingsAboutResponse,
} from '../../interfaces/api/settingsInterfaces';
import { jobJellyfinFullSync } from '../../job/jellyfinsync';
import { scheduledJobs } from '../../job/schedule';
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
import { Permission } from '../../lib/permissions';
import { plexFullScanner } from '../../lib/scanners/plex';
import { getSettings, MainSettings } from '../../lib/settings';
import { getSettings, Library, MainSettings } from '../../lib/settings';
import logger from '../../logger';
import { isAuthenticated } from '../../middleware/auth';
import { appDataPath } from '../../utils/appDataVolume';
@@ -238,6 +240,79 @@ settingsRoutes.post('/plex/sync', (req, res) => {
return res.status(200).json(plexFullScanner.status());
});
settingsRoutes.get('/jellyfin', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.jellyfin);
});
settingsRoutes.post('/jellyfin', (req, res) => {
const settings = getSettings();
settings.jellyfin = merge(settings.jellyfin, req.body);
settings.save();
return res.status(200).json(settings.jellyfin);
});
settingsRoutes.get('/jellyfin/library', async (req, res) => {
const settings = getSettings();
if (req.query.sync) {
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
order: { id: 'ASC' },
});
const jellyfinClient = new JellyfinAPI(
settings.jellyfin.hostname ?? '',
admin.jellyfinAuthToken ?? '',
admin.jellyfinDeviceId ?? ''
);
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const libraries = await jellyfinClient.getLibraries();
const newLibraries: Library[] = libraries.map((library) => {
const existing = settings.jellyfin.libraries.find(
(l) => l.id === library.key && l.name === library.title
);
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
type: library.type,
};
});
settings.jellyfin.libraries = newLibraries;
}
const enabledLibraries = req.query.enable
? (req.query.enable as string).split(',')
: [];
settings.jellyfin.libraries = settings.jellyfin.libraries.map((library) => ({
...library,
enabled: enabledLibraries.includes(library.id),
}));
settings.save();
return res.status(200).json(settings.jellyfin.libraries);
});
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
return res.status(200).json(jobJellyfinFullSync.status());
});
settingsRoutes.post('/jellyfin/sync', (req, res) => {
if (req.body.cancel) {
jobJellyfinFullSync.cancel();
} else if (req.body.start) {
jobJellyfinFullSync.run();
}
return res.status(200).json(jobJellyfinFullSync.status());
});
settingsRoutes.get('/tautulli', (_req, res) => {
const settings = getSettings();