mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
2 Commits
85cf420438
...
preview-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
329527bd03 | ||
|
|
269fd69dff |
@@ -135,6 +135,7 @@ class ImageProxy {
|
|||||||
private cacheVersion;
|
private cacheVersion;
|
||||||
private key;
|
private key;
|
||||||
private baseUrl;
|
private baseUrl;
|
||||||
|
private headers: HeadersInit | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
key: string,
|
key: string,
|
||||||
@@ -142,6 +143,7 @@ class ImageProxy {
|
|||||||
options: {
|
options: {
|
||||||
cacheVersion?: number;
|
cacheVersion?: number;
|
||||||
rateLimitOptions?: RateLimitOptions;
|
rateLimitOptions?: RateLimitOptions;
|
||||||
|
headers?: HeadersInit;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
this.cacheVersion = options.cacheVersion ?? 1;
|
this.cacheVersion = options.cacheVersion ?? 1;
|
||||||
@@ -155,9 +157,13 @@ class ImageProxy {
|
|||||||
} else {
|
} else {
|
||||||
this.fetch = fetch;
|
this.fetch = fetch;
|
||||||
}
|
}
|
||||||
|
this.headers = options.headers || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getImage(path: string): Promise<ImageResponse> {
|
public async getImage(
|
||||||
|
path: string,
|
||||||
|
fallbackPath?: string
|
||||||
|
): Promise<ImageResponse> {
|
||||||
const cacheKey = this.getCacheKey(path);
|
const cacheKey = this.getCacheKey(path);
|
||||||
|
|
||||||
const imageResponse = await this.get(cacheKey);
|
const imageResponse = await this.get(cacheKey);
|
||||||
@@ -166,7 +172,11 @@ class ImageProxy {
|
|||||||
const newImage = await this.set(path, cacheKey);
|
const newImage = await this.set(path, cacheKey);
|
||||||
|
|
||||||
if (!newImage) {
|
if (!newImage) {
|
||||||
throw new Error('Failed to load image');
|
if (fallbackPath) {
|
||||||
|
return await this.getImage(fallbackPath);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to load image');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newImage;
|
return newImage;
|
||||||
@@ -247,7 +257,12 @@ class ImageProxy {
|
|||||||
: '/'
|
: '/'
|
||||||
: '') +
|
: '') +
|
||||||
(path.startsWith('/') ? path.slice(1) : path);
|
(path.startsWith('/') ? path.slice(1) : path);
|
||||||
const response = await this.fetch(href);
|
const response = await this.fetch(href, {
|
||||||
|
headers: this.headers || undefined,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { UserType } from '@server/constants/user';
|
|||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import { startJobs } from '@server/job/schedule';
|
import { startJobs } from '@server/job/schedule';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
@@ -15,7 +14,6 @@ import { ApiError } from '@server/types/error';
|
|||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
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';
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
@@ -328,12 +326,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: `/avatarproxy/${account.User.Id}`,
|
||||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType: UserType.EMBY,
|
userType: UserType.EMBY,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,12 +340,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: `/avatarproxy/${account.User.Id}`,
|
||||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -401,27 +389,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
user.avatar = `/avatarproxy/${account.User.Id}`;
|
||||||
if (account.User.PrimaryImageTag) {
|
|
||||||
const avatar = `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
|
||||||
if (avatar !== user.avatar) {
|
|
||||||
const avatarProxy = new ImageProxy('avatar', '');
|
|
||||||
avatarProxy.clearCachedImage(user.avatar);
|
|
||||||
}
|
|
||||||
user.avatar = avatar;
|
|
||||||
} else {
|
|
||||||
const avatar = gravatarUrl(user.email || account.User.Name, {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (avatar !== user.avatar) {
|
|
||||||
const avatarProxy = new ImageProxy('avatar', '');
|
|
||||||
avatarProxy.clearCachedImage(user.avatar);
|
|
||||||
}
|
|
||||||
|
|
||||||
user.avatar = avatar;
|
|
||||||
}
|
|
||||||
user.jellyfinUsername = account.User.Name;
|
user.jellyfinUsername = account.User.Name;
|
||||||
|
|
||||||
if (user.username === account.User.Name) {
|
if (user.username === account.User.Name) {
|
||||||
@@ -459,12 +427,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: `/avatarproxy/${account.User.Id}`,
|
||||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
|
|||||||
@@ -1,21 +1,39 @@
|
|||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import gravatarUrl from 'gravatar-url';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
const avatarImageProxy = new ImageProxy('avatar', '');
|
let _avatarImageProxy: ImageProxy | null = null;
|
||||||
// Proxy avatar images
|
async function initAvatarImageProxy() {
|
||||||
router.get('/*', async (req, res) => {
|
if (!_avatarImageProxy) {
|
||||||
let imagePath = '';
|
const userRepository = getRepository(User);
|
||||||
|
const admin = await userRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
const deviceId = admin?.jellyfinDeviceId;
|
||||||
|
const authToken = getSettings().jellyfin.apiKey;
|
||||||
|
_avatarImageProxy = new ImageProxy('avatar', '', {
|
||||||
|
headers: {
|
||||||
|
'X-Emby-Authorization': `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _avatarImageProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/:jellyfinUserId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const jellyfinAvatar = req.url.match(
|
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
|
||||||
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
|
||||||
)?.[1];
|
|
||||||
if (!jellyfinAvatar) {
|
|
||||||
const mediaServerType = getSettings().main.mediaServerType;
|
const mediaServerType = getSettings().main.mediaServerType;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Provided URL is not ${
|
`Provided URL is not ${
|
||||||
@@ -26,10 +44,28 @@ router.get('/*', async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = new URL(jellyfinAvatar, getHostname());
|
const avatarImageCache = await initAvatarImageProxy();
|
||||||
imagePath = imageUrl.toString();
|
|
||||||
|
|
||||||
const imageData = await avatarImageProxy.getImage(imagePath);
|
const user = await getRepository(User).findOne({
|
||||||
|
where: { jellyfinUserId: req.params.jellyfinUserId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbackUrl = gravatarUrl(user?.email || 'none', {
|
||||||
|
default: 'mm',
|
||||||
|
size: 200,
|
||||||
|
});
|
||||||
|
const jellyfinAvatarUrl = `${getHostname()}/UserImage?UserId=${
|
||||||
|
req.params.jellyfinUserId
|
||||||
|
}`;
|
||||||
|
let imageData = await avatarImageCache.getImage(
|
||||||
|
jellyfinAvatarUrl,
|
||||||
|
fallbackUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imageData.meta.extension === 'json') {
|
||||||
|
// this is a 404
|
||||||
|
imageData = await avatarImageCache.getImage(fallbackUrl);
|
||||||
|
}
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': `image/${imageData.meta.extension}`,
|
'Content-Type': `image/${imageData.meta.extension}`,
|
||||||
@@ -42,7 +78,6 @@ router.get('/*', async (req, res) => {
|
|||||||
res.end(imageData.imageBuffer);
|
res.end(imageData.imageBuffer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to proxy avatar image', {
|
logger.error('Failed to proxy avatar image', {
|
||||||
imagePath,
|
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { getHostname } from '@server/utils/getHostname';
|
|||||||
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';
|
||||||
@@ -395,9 +394,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
const users = resp.users.map((user) => ({
|
const users = resp.users.map((user) => ({
|
||||||
username: user.Name,
|
username: user.Name,
|
||||||
id: user.Id,
|
id: user.Id,
|
||||||
thumb: user.PrimaryImageTag
|
thumb: `/avatarproxy/${user.Id}`,
|
||||||
? `/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
|
||||||
email: user.Name,
|
email: user.Name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -539,12 +539,7 @@ router.post(
|
|||||||
).toString('base64'),
|
).toString('base64'),
|
||||||
email: jellyfinUser?.Name,
|
email: jellyfinUser?.Name,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: jellyfinUser?.PrimaryImageTag
|
avatar: `/avatarproxy/${jellyfinUser?.Id}`,
|
||||||
? `/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(jellyfinUser?.Name ?? '', {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
|
|||||||
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
||||||
: src;
|
: src;
|
||||||
} else if (type === 'avatar') {
|
} else if (type === 'avatar') {
|
||||||
// jellyfin avatar (in any)
|
// jellyfin avatar (if any)
|
||||||
const jellyfinAvatar = src.match(
|
imageUrl = src;
|
||||||
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
|
||||||
)?.[1];
|
|
||||||
imageUrl = jellyfinAvatar ? `/avatarproxy` + jellyfinAvatar : src;
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user