fix: rewrite avatarproxy and CachedImage (#1016)
* fix: rewrite avatarproxy and CachedImage Avatar proxy was allowing every request to be proxied, no matter the original ressource's origin or filetype. This PR fixes it be allowing only relevant resources to be cached, i.e. Jellyfin/Emby images and TMDB images. fix #1012, #1013 * fix: resolve CodeQL error * fix: resolve CodeQL error * fix: resolve review comments * fix: resolve review comment * fix: resolve CodeQL error * fix: update imageproxy path
This commit is contained in:
@@ -262,8 +262,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
urlBase: body.urlBase,
|
urlBase: body.urlBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { externalHostname } = getSettings().jellyfin;
|
|
||||||
|
|
||||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||||
let user = await userRepository.findOne({
|
let user = await userRepository.findOne({
|
||||||
where: { jellyfinUsername: body.username },
|
where: { jellyfinUsername: body.username },
|
||||||
@@ -281,11 +279,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
// First we need to attempt to log the user in to jellyfin
|
// First we need to attempt to log the user in to jellyfin
|
||||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||||
|
|
||||||
const jellyfinHost =
|
|
||||||
externalHostname && externalHostname.length > 0
|
|
||||||
? externalHostname
|
|
||||||
: hostname;
|
|
||||||
|
|
||||||
const ip = req.ip;
|
const ip = req.ip;
|
||||||
let clientIp;
|
let clientIp;
|
||||||
|
|
||||||
@@ -336,7 +329,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
: gravatarUrl(body.email || account.User.Name, {
|
||||||
default: 'mm',
|
default: 'mm',
|
||||||
size: 200,
|
size: 200,
|
||||||
@@ -355,7 +348,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
: gravatarUrl(body.email || account.User.Name, {
|
||||||
default: 'mm',
|
default: 'mm',
|
||||||
size: 200,
|
size: 200,
|
||||||
@@ -410,7 +403,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
);
|
);
|
||||||
// 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) {
|
||||||
const avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
const avatar = `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||||
if (avatar !== user.avatar) {
|
if (avatar !== user.avatar) {
|
||||||
const avatarProxy = new ImageProxy('avatar', '');
|
const avatarProxy = new ImageProxy('avatar', '');
|
||||||
avatarProxy.clearCachedImage(user.avatar);
|
avatarProxy.clearCachedImage(user.avatar);
|
||||||
@@ -467,7 +460,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
: gravatarUrl(body.email || account.User.Name, {
|
||||||
default: 'mm',
|
default: 'mm',
|
||||||
size: 200,
|
size: 200,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -7,9 +10,25 @@ const router = Router();
|
|||||||
const avatarImageProxy = new ImageProxy('avatar', '');
|
const avatarImageProxy = new ImageProxy('avatar', '');
|
||||||
// Proxy avatar images
|
// Proxy avatar images
|
||||||
router.get('/*', async (req, res) => {
|
router.get('/*', async (req, res) => {
|
||||||
const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
|
let imagePath = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const jellyfinAvatar = req.url.match(
|
||||||
|
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
||||||
|
)?.[1];
|
||||||
|
if (!jellyfinAvatar) {
|
||||||
|
const mediaServerType = getSettings().main.mediaServerType;
|
||||||
|
throw new Error(
|
||||||
|
`Provided URL is not ${
|
||||||
|
mediaServerType === MediaServerType.JELLYFIN
|
||||||
|
? 'a Jellyfin'
|
||||||
|
: 'an Emby'
|
||||||
|
} avatar.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = new URL(jellyfinAvatar, getHostname());
|
||||||
|
imagePath = imageUrl.toString();
|
||||||
|
|
||||||
const imageData = await avatarImageProxy.getImage(imagePath);
|
const imageData = await avatarImageProxy.getImage(imagePath);
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
|
|||||||
@@ -377,11 +377,6 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|||||||
|
|
||||||
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const { externalHostname } = settings.jellyfin;
|
|
||||||
const jellyfinHost =
|
|
||||||
externalHostname && externalHostname.length > 0
|
|
||||||
? externalHostname
|
|
||||||
: getHostname();
|
|
||||||
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOneOrFail({
|
const admin = await userRepository.findOneOrFail({
|
||||||
@@ -401,7 +396,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
username: user.Name,
|
username: user.Name,
|
||||||
id: user.Id,
|
id: user.Id,
|
||||||
thumb: user.PrimaryImageTag
|
thumb: user.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
? `/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
||||||
email: user.Name,
|
email: user.Name,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -516,12 +516,6 @@ router.post(
|
|||||||
|
|
||||||
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
||||||
const createdUsers: User[] = [];
|
const createdUsers: User[] = [];
|
||||||
const { externalHostname } = getSettings().jellyfin;
|
|
||||||
|
|
||||||
const jellyfinHost =
|
|
||||||
externalHostname && externalHostname.length > 0
|
|
||||||
? externalHostname
|
|
||||||
: hostname;
|
|
||||||
|
|
||||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||||
const jellyfinUsers = await jellyfinClient.getUsers();
|
const jellyfinUsers = await jellyfinClient.getUsers();
|
||||||
@@ -546,7 +540,7 @@ router.post(
|
|||||||
email: jellyfinUser?.Name,
|
email: jellyfinUser?.Name,
|
||||||
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`
|
? `/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(jellyfinUser?.Name ?? '', {
|
: gravatarUrl(jellyfinUser?.Name ?? '', {
|
||||||
default: 'mm',
|
default: 'mm',
|
||||||
size: 200,
|
size: 200,
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
|
|||||||
{title && title.backdropPath && (
|
{title && title.backdropPath && (
|
||||||
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -293,6 +294,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
|
|||||||
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
title?.posterPath
|
title?.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
@@ -355,6 +357,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
|
|||||||
<Link href={`/users/${item.user.id}`}>
|
<Link href={`/users/${item.user.id}`}>
|
||||||
<span className="group flex items-center truncate">
|
<span className="group flex items-center truncate">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={item.user.avatar}
|
src={item.user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm ml-1.5"
|
className="avatar-sm ml-1.5"
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
|||||||
{data.backdropPath && (
|
{data.backdropPath && (
|
||||||
<div className="media-page-bg-image">
|
<div className="media-page-bg-image">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
alt=""
|
alt=""
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -228,6 +229,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
|||||||
<div className="media-header">
|
<div className="media-header">
|
||||||
<div className="media-poster">
|
<div className="media-poster">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
|
|||||||
@@ -4,24 +4,34 @@ import Image from 'next/image';
|
|||||||
|
|
||||||
const imageLoader: ImageLoader = ({ src }) => src;
|
const imageLoader: ImageLoader = ({ src }) => src;
|
||||||
|
|
||||||
|
export type CachedImageProps = ImageProps & {
|
||||||
|
src: string;
|
||||||
|
type: 'tmdb' | 'avatar';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CachedImage component should be used wherever
|
* The CachedImage component should be used wherever
|
||||||
* we want to offer the option to locally cache images.
|
* we want to offer the option to locally cache images.
|
||||||
**/
|
**/
|
||||||
const CachedImage = ({ src, ...props }: ImageProps) => {
|
const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
|
||||||
const { currentSettings } = useSettings();
|
const { currentSettings } = useSettings();
|
||||||
|
|
||||||
let imageUrl = src;
|
let imageUrl: string;
|
||||||
|
|
||||||
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
|
if (type === 'tmdb') {
|
||||||
const parsedUrl = new URL(imageUrl);
|
// tmdb stuff
|
||||||
|
imageUrl =
|
||||||
if (parsedUrl.host === 'image.tmdb.org') {
|
currentSettings.cacheImages && !src.startsWith('/')
|
||||||
if (currentSettings.cacheImages)
|
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
||||||
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
|
: src;
|
||||||
} else if (parsedUrl.host !== 'gravatar.com') {
|
} else if (type === 'avatar') {
|
||||||
imageUrl = '/avatarproxy/' + imageUrl;
|
// jellyfin avatar (in any)
|
||||||
}
|
const jellyfinAvatar = src.match(
|
||||||
|
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
||||||
|
)?.[1];
|
||||||
|
imageUrl = jellyfinAvatar ? `/avatarproxy` + jellyfinAvatar : src;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;
|
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
className="absolute inset-0 h-full w-full"
|
className="absolute inset-0 h-full w-full"
|
||||||
alt=""
|
alt=""
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
{backdrop && (
|
{backdrop && (
|
||||||
<div className="absolute top-0 left-0 right-0 z-0 h-64 max-h-full w-full">
|
<div className="absolute top-0 left-0 right-0 z-0 h-64 max-h-full w-full">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
alt=""
|
alt=""
|
||||||
src={backdrop}
|
src={backdrop}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
|
|||||||
>
|
>
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={image}
|
src={image}
|
||||||
alt={name}
|
alt={name}
|
||||||
className="relative z-40 h-full w-full"
|
className="relative z-40 h-full w-full"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={image}
|
src={image}
|
||||||
alt=""
|
alt=""
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ const IssueComment = ({
|
|||||||
</Transition>
|
</Transition>
|
||||||
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
|
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
src={`${comment.user.avatar}`}
|
type="avatar"
|
||||||
|
src={comment.user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
width={40}
|
width={40}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ const IssueDetails = () => {
|
|||||||
{data.backdropPath && (
|
{data.backdropPath && (
|
||||||
<div className="media-page-bg-image">
|
<div className="media-page-bg-image">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
alt=""
|
alt=""
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -235,6 +236,7 @@ const IssueDetails = () => {
|
|||||||
<div className="media-header">
|
<div className="media-header">
|
||||||
<div className="media-poster">
|
<div className="media-poster">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
@@ -287,7 +289,8 @@ const IssueDetails = () => {
|
|||||||
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
|
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
src={`${issueData.createdBy.avatar}`}
|
type="avatar"
|
||||||
|
src={issueData.createdBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
|
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
|
||||||
width={20}
|
width={20}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
|||||||
{title.backdropPath && (
|
{title.backdropPath && (
|
||||||
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -137,6 +138,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
|||||||
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
title.posterPath
|
title.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
@@ -226,7 +228,8 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
|||||||
className="group flex items-center truncate"
|
className="group flex items-center truncate"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
src={'/avatarproxy/' + issue.createdBy.avatar}
|
type="avatar"
|
||||||
|
src={issue.createdBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm ml-1.5 object-cover"
|
className="avatar-sm ml-1.5 object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const UserDropdown = () => {
|
|||||||
data-testid="user-menu"
|
data-testid="user-menu"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||||
src={user ? user.avatar : ''}
|
src={user ? user.avatar : ''}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -80,6 +81,7 @@ const UserDropdown = () => {
|
|||||||
<div className="flex flex-col space-y-4 px-4 py-4">
|
<div className="flex flex-col space-y-4 px-4 py-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||||
src={user ? user.avatar : ''}
|
src={user ? user.avatar : ''}
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const ManageSlideOver = ({
|
|||||||
content={user.displayName}
|
content={user.displayName}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
@@ -530,6 +531,7 @@ const ManageSlideOver = ({
|
|||||||
content={user.displayName}
|
content={user.displayName}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
{data.backdropPath && (
|
{data.backdropPath && (
|
||||||
<div className="media-page-bg-image">
|
<div className="media-page-bg-image">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
alt=""
|
alt=""
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -494,6 +495,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
<div className="media-header">
|
<div className="media-header">
|
||||||
<div className="media-poster">
|
<div className="media-poster">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
@@ -741,6 +743,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
<div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500">
|
<div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500">
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const PersonCard = ({
|
|||||||
{profilePath ? (
|
{profilePath ? (
|
||||||
<div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700">
|
<div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
|
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ const PersonDetails = () => {
|
|||||||
{data.profilePath && (
|
{data.profilePath && (
|
||||||
<div className="relative mb-6 mr-0 h-36 w-36 flex-shrink-0 overflow-hidden rounded-full ring-1 ring-gray-700 lg:mb-0 lg:mr-6 lg:h-44 lg:w-44">
|
<div className="relative mb-6 mr-0 h-36 w-36 flex-shrink-0 overflow-hidden rounded-full ring-1 ring-gray-700 lg:mb-0 lg:mr-6 lg:h-44 lg:w-44">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`}
|
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
|||||||
>
|
>
|
||||||
<span className="avatar-sm">
|
<span className="avatar-sm">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
@@ -345,6 +346,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
{title.backdropPath && (
|
{title.backdropPath && (
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
alt=""
|
alt=""
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -390,6 +392,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
>
|
>
|
||||||
<span className="avatar-sm">
|
<span className="avatar-sm">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
@@ -602,6 +605,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"
|
className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
title.posterPath
|
title.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ const RequestItemError = ({
|
|||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
@@ -250,6 +251,7 @@ const RequestItemError = ({
|
|||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={requestData.modifiedBy.avatar}
|
src={requestData.modifiedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
@@ -418,6 +420,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
{title.backdropPath && (
|
{title.backdropPath && (
|
||||||
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -443,6 +446,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
title.posterPath
|
title.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
@@ -570,6 +574,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
@@ -629,6 +634,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ const AdvancedRequester = ({
|
|||||||
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
|
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={selectedUser.avatar}
|
src={selectedUser.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||||
@@ -614,6 +615,7 @@ const AdvancedRequester = ({
|
|||||||
} flex items-center`}
|
} flex items-center`}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ const CollectionRequestModal = ({
|
|||||||
>
|
>
|
||||||
<div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
|
<div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
part.posterPath
|
part.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ export const WatchProviderSelector = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
|
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
@@ -497,6 +498,7 @@ export const WatchProviderSelector = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
|
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ const TitleCard = ({
|
|||||||
>
|
>
|
||||||
<div className="absolute inset-0 h-full w-full overflow-hidden">
|
<div className="absolute inset-0 h-full w-full overflow-hidden">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
className="absolute inset-0 h-full w-full"
|
className="absolute inset-0 h-full w-full"
|
||||||
alt=""
|
alt=""
|
||||||
src={
|
src={
|
||||||
|
|||||||
@@ -471,6 +471,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
{data.backdropPath && (
|
{data.backdropPath && (
|
||||||
<div className="media-page-bg-image">
|
<div className="media-page-bg-image">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
alt=""
|
alt=""
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
@@ -527,6 +528,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
<div className="media-header">
|
<div className="media-header">
|
||||||
<div className="media-poster">
|
<div className="media-poster">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="tmdb"
|
||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
className="h-10 w-10 flex-shrink-0 rounded-full"
|
className="h-10 w-10 flex-shrink-0 rounded-full"
|
||||||
src={user.thumb}
|
src={user.thumb}
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -634,6 +634,7 @@ const UserList = () => {
|
|||||||
className="h-10 w-10 flex-shrink-0"
|
className="h-10 w-10 flex-shrink-0"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
className="h-10 w-10 rounded-full object-cover"
|
className="h-10 w-10 rounded-full object-cover"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
|
type="avatar"
|
||||||
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
|
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
Reference in New Issue
Block a user