Skip to content

Commit 0ca3d43

Browse files
authored
feat: add option to cache images locally (seerr-team#1213)
1 parent dfd4ff9 commit 0ca3d43

File tree

26 files changed

+293
-108
lines changed

26 files changed

+293
-108
lines changed

next.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ module.exports = {
22
env: {
33
commitTag: process.env.COMMIT_TAG || 'local',
44
},
5+
images: {
6+
domains: ['image.tmdb.org'],
7+
},
58
webpack(config) {
69
config.module.rules.push({
710
test: /\.svg$/,

server/interfaces/api/settingsInterfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface PublicSettingsResponse {
2828
region: string;
2929
originalLanguage: string;
3030
partialRequestsEnabled: boolean;
31+
cacheImages: boolean;
3132
}
3233

3334
export interface CacheItem {

server/lib/settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface MainSettings {
6666
applicationTitle: string;
6767
applicationUrl: string;
6868
csrfProtection: boolean;
69+
cacheImages: boolean;
6970
defaultPermissions: number;
7071
hideAvailable: boolean;
7172
localLogin: boolean;
@@ -88,6 +89,7 @@ interface FullPublicSettings extends PublicSettings {
8889
region: string;
8990
originalLanguage: string;
9091
partialRequestsEnabled: boolean;
92+
cacheImages: boolean;
9193
}
9294

9395
export interface NotificationAgentConfig {
@@ -195,6 +197,7 @@ class Settings {
195197
applicationTitle: 'Overseerr',
196198
applicationUrl: '',
197199
csrfProtection: false,
200+
cacheImages: false,
198201
defaultPermissions: Permission.REQUEST,
199202
hideAvailable: false,
200203
localLogin: true,
@@ -349,6 +352,7 @@ class Settings {
349352
region: this.data.main.region,
350353
originalLanguage: this.data.main.originalLanguage,
351354
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
355+
cacheImages: this.data.main.cacheImages,
352356
};
353357
}
354358

src/components/CollectionDetails/index.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useUser, Permission } from '../../hooks/useUser';
2121
import useSettings from '../../hooks/useSettings';
2222
import Link from 'next/link';
2323
import { uniq } from 'lodash';
24+
import CachedImage from '../Common/CachedImage';
2425

2526
const messages = defineMessages({
2627
overviewunavailable: 'Overview unavailable.',
@@ -203,9 +204,26 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
203204
className="media-page"
204205
style={{
205206
height: 493,
206-
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,
207207
}}
208208
>
209+
{data.backdropPath && (
210+
<div className="media-page-bg-image">
211+
<CachedImage
212+
alt=""
213+
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
214+
layout="fill"
215+
objectFit="cover"
216+
priority
217+
/>
218+
<div
219+
className="absolute inset-0"
220+
style={{
221+
backgroundImage:
222+
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)',
223+
}}
224+
/>
225+
</div>
226+
)}
209227
<PageTitle title={data.name} />
210228
<Transition
211229
enter="opacity-0 transition duration-300"
@@ -268,11 +286,20 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
268286
</Modal>
269287
</Transition>
270288
<div className="media-header">
271-
<img
272-
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
273-
alt=""
274-
className="media-poster"
275-
/>
289+
<div className="media-poster">
290+
<CachedImage
291+
src={
292+
data.posterPath
293+
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
294+
: '/images/overseerr_poster_not_found.png'
295+
}
296+
alt=""
297+
layout="responsive"
298+
width={600}
299+
height={900}
300+
priority
301+
/>
302+
</div>
276303
<div className="media-title">
277304
<div className="media-status">
278305
<StatusBadge
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Image, { ImageProps } from 'next/image';
2+
import React from 'react';
3+
import useSettings from '../../../hooks/useSettings';
4+
5+
/**
6+
* The CachedImage component should be used wherever
7+
* we want to offer the option to locally cache images.
8+
*
9+
* It uses the `next/image` Image component but overrides
10+
* the `unoptimized` prop based on the application setting `cacheImages`.
11+
**/
12+
const CachedImage: React.FC<ImageProps> = (props) => {
13+
const { currentSettings } = useSettings();
14+
15+
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
16+
};
17+
18+
export default CachedImage;

src/components/Common/ImageFader/index.tsx

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import React, {
44
HTMLAttributes,
55
ForwardRefRenderFunction,
66
} from 'react';
7-
import Image from 'next/image';
7+
import CachedImage from '../CachedImage';
88

99
interface ImageFaderProps extends HTMLAttributes<HTMLDivElement> {
1010
backgroundImages: string[];
1111
rotationSpeed?: number;
1212
isDarker?: boolean;
13-
useImage?: boolean;
13+
forceOptimize?: boolean;
1414
}
1515

1616
const DEFAULT_ROTATION_SPEED = 6000;
@@ -20,7 +20,7 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
2020
backgroundImages,
2121
rotationSpeed = DEFAULT_ROTATION_SPEED,
2222
isDarker,
23-
useImage,
23+
forceOptimize,
2424
...props
2525
},
2626
ref
@@ -46,6 +46,14 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
4646
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)';
4747
}
4848

49+
let overrides = {};
50+
51+
if (forceOptimize) {
52+
overrides = {
53+
unoptimized: false,
54+
};
55+
}
56+
4957
return (
5058
<div ref={ref}>
5159
{backgroundImages.map((imageUrl, i) => (
@@ -54,29 +62,20 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
5462
className={`absolute inset-0 bg-cover bg-center transition-opacity duration-300 ease-in ${
5563
i === activeIndex ? 'opacity-100' : 'opacity-0'
5664
}`}
57-
style={{
58-
backgroundImage: !useImage
59-
? `${gradient}, url(${imageUrl})`
60-
: undefined,
61-
}}
6265
{...props}
6366
>
64-
{useImage && (
65-
<>
66-
<Image
67-
className="absolute inset-0 w-full h-full"
68-
alt=""
69-
src={imageUrl}
70-
layout="fill"
71-
objectFit="cover"
72-
quality={100}
73-
/>
74-
<div
75-
className="absolute inset-0"
76-
style={{ backgroundImage: gradient }}
77-
/>
78-
</>
79-
)}
67+
<CachedImage
68+
className="absolute inset-0 w-full h-full"
69+
alt=""
70+
src={imageUrl}
71+
layout="fill"
72+
objectFit="cover"
73+
{...overrides}
74+
/>
75+
<div
76+
className="absolute inset-0"
77+
style={{ backgroundImage: gradient }}
78+
/>
8079
</div>
8180
))}
8281
</div>

src/components/Discover/MovieGenreList/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const MovieGenreList: React.FC = () => {
4040
<li key={`genre-${genre.id}-${index}`}>
4141
<GenreCard
4242
name={genre.name}
43-
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
43+
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
4444
genreColorMap[genre.id] ?? genreColorMap[0]
4545
})${genre.backdrops[4]}`}
4646
url={`/discover/movies/genre/${genre.id}`}

src/components/Discover/MovieGenreSlider/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const MovieGenreSlider: React.FC = () => {
5454
<GenreCard
5555
key={`genre-${genre.id}-${index}`}
5656
name={genre.name}
57-
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
57+
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
5858
genreColorMap[genre.id] ?? genreColorMap[0]
5959
})${genre.backdrops[4]}`}
6060
url={`/discover/movies/genre/${genre.id}`}

src/components/Discover/TvGenreList/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const TvGenreList: React.FC = () => {
4040
<li key={`genre-${genre.id}-${index}`}>
4141
<GenreCard
4242
name={genre.name}
43-
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
43+
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
4444
genreColorMap[genre.id] ?? genreColorMap[0]
4545
})${genre.backdrops[4]}`}
4646
url={`/discover/tv/genre/${genre.id}`}

src/components/Discover/TvGenreSlider/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const TvGenreSlider: React.FC = () => {
5454
<GenreCard
5555
key={`genre-tv-${genre.id}-${index}`}
5656
name={genre.name}
57-
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
57+
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
5858
genreColorMap[genre.id] ?? genreColorMap[0]
5959
})${genre.backdrops[4]}`}
6060
url={`/discover/tv/genre/${genre.id}`}

0 commit comments

Comments
 (0)