Compare commits

..

43 Commits

Author SHA1 Message Date
fallenbagel
fd18a9fe9a refactor: merges develop into weblate 2024-08-16 22:34:15 +05:00
Mattias Magnusson
70aab8b0a9 Translated using Weblate (Swedish)
Currently translated at 99.7% (1303 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/sv/
2024-08-15 22:42:52 +00:00
N/A
1c176dc71e Translated using Weblate (Turkish)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-08-15 22:42:52 +00:00
N/A
c7d2386799 Translated using Weblate (Turkish)
Currently translated at 85.0% (1111 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-08-12 03:42:51 +00:00
N/A
b04f280fbd Translated using Weblate (Turkish)
Currently translated at 75.6% (988 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-07-26 19:42:50 +00:00
N/A
6b0909264d Translated using Weblate (Turkish)
Currently translated at 44.6% (583 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-07-25 13:42:50 +00:00
N/A
7923fc67e8 Translated using Weblate (Turkish)
Currently translated at 42.7% (558 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-07-23 21:42:56 +00:00
Jakob Števanec
afed6e8879 Translated using Weblate (Slovenian)
Currently translated at 4.2% (56 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/sl/
2024-07-23 21:42:55 +00:00
N/A
341c7171cd Translated using Weblate (Turkish)
Currently translated at 19.2% (252 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-07-22 10:42:50 +00:00
Wiktor Kowalski
3ab184be20 Translated using Weblate (Polish)
Currently translated at 94.4% (1233 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/
2024-07-22 10:42:50 +00:00
Adrian Konopczynski
7be97a0e12 Translated using Weblate (German)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/de/
2024-06-25 23:52:58 +00:00
Nir Israel Hen
78e6fdb7bf Translated using Weblate (Hebrew)
Currently translated at 26.4% (346 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/he/
2024-06-24 04:52:58 +00:00
C W
fc95e9da4b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/zh_Hans/
2024-06-22 17:52:58 +00:00
Albert Einstien
e6b5707190 Translated using Weblate (Hebrew)
Currently translated at 23.0% (301 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/he/
2024-06-02 20:52:56 +00:00
michael
1218876fbf Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-05-08 11:30:56 +00:00
A a
b9336fc589 Translated using Weblate (Catalan)
Currently translated at 94.1% (1230 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ca/
2024-05-05 23:30:56 +00:00
michael
20762143db Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-05-03 08:12:21 +00:00
michael
9d10efd277 Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-05-03 07:11:39 +00:00
michael
ba81ed25e2 Translated using Weblate (Ukrainian)
Currently translated at 99.8% (1304 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-05-03 05:52:36 +00:00
uqlel
05a4adf369 Translated using Weblate (Polish)
Currently translated at 84.1% (1099 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/
2024-04-17 17:22:21 +00:00
michael
1449883629 Translated using Weblate (Ukrainian)
Currently translated at 94.3% (1232 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-04-10 10:22:20 +00:00
michael
9f4329e243 Translated using Weblate (Ukrainian)
Currently translated at 94.3% (1232 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-04-06 17:22:20 +00:00
michael
5743105db4 Translated using Weblate (Ukrainian)
Currently translated at 93.7% (1225 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/uk/
2024-04-04 14:37:59 +00:00
Flashk
fce8231710 Translated using Weblate (Spanish)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/es/
2024-03-08 21:22:18 +00:00
uqlel
53be59ad51 Translated using Weblate (Polish)
Currently translated at 83.9% (1096 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/
2024-03-02 14:29:27 +00:00
uqlel
7b8adf5bdf Translated using Weblate (Polish)
Currently translated at 82.6% (1079 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pl/
2024-03-01 15:22:18 +00:00
osh
7ee45ef6e1 Translated using Weblate (Hebrew)
Currently translated at 15.2% (199 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/he/
2024-02-18 13:04:53 +00:00
grayair
50488db05a Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.0% (1254 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/pt_BR/
2024-02-11 18:37:47 +00:00
Quack6765
a132105b45 Translated using Weblate (French)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/fr/
2024-02-09 21:57:30 +00:00
Eduard Perez Mendez
cb172cc737 Translated using Weblate (Spanish)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/es/
2024-02-02 22:07:09 +00:00
宿命
cee3be2c64 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/zh_Hans/
2024-01-30 05:07:14 +00:00
Bas Muldder
8ecb459808 Translated using Weblate (Dutch)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/nl/
2024-01-23 19:24:36 +00:00
Aleksandr
372b3649b5 Translated using Weblate (Russian)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ru/
2024-01-18 23:33:18 +00:00
Aleksandr
ee472e11af Translated using Weblate (Russian)
Currently translated at 97.4% (1273 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ru/
2024-01-18 18:56:18 +00:00
Aleksandr
1fd3a00284 Translated using Weblate (Russian)
Currently translated at 96.6% (1262 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ru/
2024-01-18 18:37:28 +00:00
Cosmin Mocan
c97c40e04c Translated using Weblate (Romanian)
Currently translated at 33.5% (438 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/ro/
2024-01-18 15:13:17 +00:00
Ramon Stohr
12f4d78692 Translated using Weblate (German)
Currently translated at 100.0% (1306 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/de/
2024-01-16 02:15:13 +00:00
N/A
18e3f140eb Translated using Weblate (Turkish)
Currently translated at 7.2% (95 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/tr/
2024-01-12 14:22:15 +00:00
N/A
226e910728 Added translation using Weblate (Turkish) 2024-01-11 10:18:55 +00:00
Bas
fb6140972a Translated using Weblate (Dutch)
Currently translated at 99.4% (1299 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/nl/
2024-01-09 11:22:14 +00:00
Boštjan KOLAR
9499b364af Translated using Weblate (Slovenian)
Currently translated at 4.1% (54 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/sl/
2024-01-05 21:13:39 +00:00
Alex F
a15f0ec029 Translated using Weblate (German)
Currently translated at 93.1% (1216 of 1306 strings)
Translate-URL: http://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/de/
2024-01-05 21:13:39 +00:00
Boštjan KOLAR
5c269368aa Added translation using Weblate (Slovenian) 2024-01-03 10:05:27 +00:00
25 changed files with 1459 additions and 238 deletions

View File

@@ -14,6 +14,7 @@ module.exports = {
remotePatterns: [
{ hostname: 'gravatar.com' },
{ hostname: 'image.tmdb.org' },
{ hostname: '*', protocol: 'https' },
],
},
webpack(config) {

View File

@@ -2775,15 +2775,6 @@ paths:
imageCount:
type: number
example: 123
avatar:
type: object
properties:
size:
type: number
example: 123456
imageCount:
type: number
example: 123
apiCaches:
type: array
items:

View File

@@ -62,7 +62,6 @@
"formik": "^2.4.6",
"gravatar-url": "3.1.0",
"lodash": "4.17.21",
"mime": "3",
"next": "^14.2.4",
"node-cache": "5.1.2",
"node-gyp": "9.3.1",
@@ -120,7 +119,6 @@
"@types/express": "4.17.17",
"@types/express-session": "1.17.6",
"@types/lodash": "4.14.191",
"@types/mime": "3",
"@types/node": "20.14.8",
"@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7",

25
pnpm-lock.yaml generated
View File

@@ -98,9 +98,6 @@ importers:
lodash:
specifier: 4.17.21
version: 4.17.21
mime:
specifier: '3'
version: 3.0.0
next:
specifier: ^14.2.4
version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -267,9 +264,6 @@ importers:
'@types/lodash':
specifier: 4.14.191
version: 4.14.191
'@types/mime':
specifier: '3'
version: 3.0.4
'@types/node':
specifier: 20.14.8
version: 20.14.8
@@ -2854,9 +2848,6 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/mime@3.0.4':
resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==}
'@types/minimatch@3.0.5':
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
@@ -10845,7 +10836,7 @@ snapshots:
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
semver: 7.3.8
semver: 7.6.2
tar: 6.2.1
transitivePeerDependencies:
- encoding
@@ -10920,13 +10911,13 @@ snapshots:
'@npmcli/fs@1.1.1':
dependencies:
'@gar/promisify': 1.1.3
semver: 7.3.8
semver: 7.6.2
optional: true
'@npmcli/fs@2.1.2':
dependencies:
'@gar/promisify': 1.1.3
semver: 7.3.8
semver: 7.6.2
'@npmcli/move-file@1.1.2':
dependencies:
@@ -12335,7 +12326,7 @@ snapshots:
read-pkg: 5.2.0
registry-auth-token: 5.0.2
semantic-release: 19.0.5(encoding@0.1.13)
semver: 7.3.8
semver: 7.6.2
tempy: 1.0.1
'@semantic-release/release-notes-generator@10.0.3(semantic-release@19.0.5(encoding@0.1.13))':
@@ -12679,8 +12670,6 @@ snapshots:
'@types/mime@1.3.5': {}
'@types/mime@3.0.4': {}
'@types/minimatch@3.0.5': {}
'@types/minimist@1.2.5': {}
@@ -12898,7 +12887,7 @@ snapshots:
debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.3.8
semver: 7.6.2
tsutils: 3.21.0(typescript@4.9.5)
optionalDependencies:
typescript: 4.9.5
@@ -17280,7 +17269,7 @@ snapshots:
nopt: 5.0.0
npmlog: 6.0.2
rimraf: 3.0.2
semver: 7.3.8
semver: 7.6.2
tar: 6.2.1
which: 2.0.2
transitivePeerDependencies:
@@ -17359,7 +17348,7 @@ snapshots:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.14.0
semver: 7.3.8
semver: 7.6.2
validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {}

View File

@@ -19,7 +19,6 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import clearCookies from '@server/middleware/clearcookies';
import routes from '@server/routes';
import avatarproxy from '@server/routes/avatarproxy';
import imageproxy from '@server/routes/imageproxy';
import { getAppVersion } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag';
@@ -203,7 +202,6 @@ app
// Do not set cookies so CDNs can cache them
server.use('/imageproxy', clearCookies, imageproxy);
server.use('/avatarproxy', clearCookies, avatarproxy);
server.get('*', (req, res) => handle(req, res));
server.use(

View File

@@ -58,7 +58,7 @@ export interface CacheItem {
export interface CacheResponse {
apiCaches: CacheItem[];
imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>;
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
}
export interface StatusResponse {

View File

@@ -227,9 +227,6 @@ export const startJobs = (): void => {
});
// Clean TMDB image cache
ImageProxy.clearCache('tmdb');
// Clean users avatar image cache
ImageProxy.clearCache('avatar');
}),
});

View File

@@ -3,7 +3,6 @@ import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit';
import { createHash } from 'crypto';
import { promises } from 'fs';
import mime from 'mime/lite';
import path, { join } from 'path';
type ImageResponse = {
@@ -12,7 +11,7 @@ type ImageResponse = {
curRevalidate: number;
isStale: boolean;
etag: string;
extension: string | null;
extension: string;
cacheKey: string;
cacheMiss: boolean;
};
@@ -28,45 +27,29 @@ class ImageProxy {
let deletedImages = 0;
const cacheDirectory = path.join(baseCacheDirectory, key);
try {
const files = await promises.readdir(cacheDirectory);
const files = await promises.readdir(cacheDirectory);
for (const file of files) {
const filePath = path.join(cacheDirectory, file);
const stat = await promises.lstat(filePath);
for (const file of files) {
const filePath = path.join(cacheDirectory, file);
const stat = await promises.lstat(filePath);
if (stat.isDirectory()) {
const imageFiles = await promises.readdir(filePath);
if (stat.isDirectory()) {
const imageFiles = await promises.readdir(filePath);
for (const imageFile of imageFiles) {
const [, expireAtSt] = imageFile.split('.');
const expireAt = Number(expireAtSt);
const now = Date.now();
for (const imageFile of imageFiles) {
const [, expireAtSt] = imageFile.split('.');
const expireAt = Number(expireAtSt);
const now = Date.now();
if (now > expireAt) {
await promises.rm(path.join(filePath), {
recursive: true,
});
deletedImages += 1;
}
if (now > expireAt) {
await promises.rm(path.join(filePath, imageFile));
deletedImages += 1;
}
}
}
} catch (e) {
if (e.code === 'ENOENT') {
logger.error('Directory not found', {
label: 'Image Cache',
message: e.message,
});
} else {
logger.error('Failed to read directory', {
label: 'Image Cache',
message: e.message,
});
}
}
logger.info(`Cleared ${deletedImages} stale image(s) from cache '${key}'`, {
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, {
label: 'Image Cache',
});
}
@@ -86,49 +69,33 @@ class ImageProxy {
}
private static async getDirectorySize(dir: string): Promise<number> {
try {
const files = await promises.readdir(dir, {
withFileTypes: true,
});
const files = await promises.readdir(dir, {
withFileTypes: true,
});
const paths = files.map(async (file) => {
const path = join(dir, file.name);
const paths = files.map(async (file) => {
const path = join(dir, file.name);
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
if (file.isFile()) {
const { size } = await promises.stat(path);
if (file.isFile()) {
const { size } = await promises.stat(path);
return size;
}
return 0;
});
return (await Promise.all(paths))
.flat(Infinity)
.reduce((i, size) => i + size, 0);
} catch (e) {
if (e.code === 'ENOENT') {
return 0;
return size;
}
}
return 0;
return 0;
});
return (await Promise.all(paths))
.flat(Infinity)
.reduce((i, size) => i + size, 0);
}
private static async getImageCount(dir: string) {
try {
const files = await promises.readdir(dir);
const files = await promises.readdir(dir);
return files.length;
} catch (e) {
if (e.code === 'ENOENT') {
return 0;
}
}
return 0;
return files.length;
}
private fetch: typeof fetch;
@@ -180,27 +147,6 @@ class ImageProxy {
return imageResponse;
}
public async clearCachedImage(path: string) {
// find cacheKey
const cacheKey = this.getCacheKey(path);
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const files = await promises.readdir(directory);
await promises.rm(directory, { recursive: true });
logger.info(`Cleared ${files[0]} image from cache 'avatar'`, {
label: 'Image Cache',
});
} catch (e) {
logger.error('Failed to clear cached image', {
label: 'Image Cache',
message: e.message,
});
}
}
private async get(cacheKey: string): Promise<ImageResponse | null> {
try {
const directory = join(this.getCacheDirectory(), cacheKey);
@@ -241,25 +187,16 @@ class ImageProxy {
const directory = join(this.getCacheDirectory(), cacheKey);
const href =
this.baseUrl +
(this.baseUrl.length > 0
? this.baseUrl.endsWith('/')
? ''
: '/'
: '') +
(this.baseUrl.endsWith('/') ? '' : '/') +
(path.startsWith('/') ? path.slice(1) : path);
const response = await this.fetch(href);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const extension = mime.getExtension(
response.headers.get('content-type') ?? ''
);
let maxAge = Number(
const extension = path.split('.').pop() ?? '';
const maxAge = Number(
(response.headers.get('cache-control') ?? '0').split('=')[1]
);
if (!maxAge) maxAge = 86400;
const expireAt = Date.now() + maxAge * 1000;
const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
@@ -295,7 +232,7 @@ class ImageProxy {
private async writeToCacheDir(
dir: string,
extension: string | null,
extension: string,
maxAge: number,
expireAt: number,
buffer: Buffer,

View File

@@ -6,7 +6,6 @@ import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import { startJobs } from '@server/job/schedule';
import ImageProxy from '@server/lib/imageproxy';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
@@ -377,23 +376,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
);
// Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) {
const avatar = `${jellyfinHost}/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;
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else {
const avatar = gravatarUrl(user.email || account.User.Name, {
user.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;

View File

@@ -1,31 +0,0 @@
import ImageProxy from '@server/lib/imageproxy';
import logger from '@server/logger';
import { Router } from 'express';
const router = Router();
const avatarImageProxy = new ImageProxy('avatar', '');
// Proxy avatar images
router.get('/*', async (req, res) => {
const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
try {
const imageData = await avatarImageProxy.getImage(imagePath);
res.writeHead(200, {
'Content-Type': `image/${imageData.meta.extension}`,
'Content-Length': imageData.imageBuffer.length,
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
'OS-Cache-Key': imageData.meta.cacheKey,
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
});
res.end(imageData.imageBuffer);
} catch (e) {
logger.error('Failed to proxy avatar image', {
imagePath,
errorMessage: e.message,
});
}
});
export default router;

View File

@@ -746,13 +746,11 @@ settingsRoutes.get('/cache', async (_req, res) => {
}));
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
const avatarImageCache = await ImageProxy.getImageStats('avatar');
return res.status(200).json({
apiCaches,
imageCache: {
tmdb: tmdbImageCache,
avatar: avatarImageCache,
},
});
});

View File

@@ -1,5 +1,4 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Modal from '@app/components/Common/Modal';
import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
@@ -7,6 +6,7 @@ import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import type { default as IssueCommentType } from '@server/entity/IssueComment';
import { Field, Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link';
import { Fragment, useState } from 'react';
import { FormattedRelativeTime, useIntl } from 'react-intl';
@@ -88,8 +88,8 @@ const IssueComment = ({
</Modal>
</Transition>
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
<CachedImage
src={`/avatarproxy/${comment.user.avatar}`}
<Image
src={comment.user.avatar}
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"
width={40}

View File

@@ -29,6 +29,7 @@ import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
@@ -288,10 +289,10 @@ const IssueDetails = () => {
}
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
>
<CachedImage
src={`/avatarproxy/${issueData.createdBy.avatar}`}
alt=""
<Image
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"
src={issueData.createdBy.avatar}
alt=""
width={20}
height={20}
/>

View File

@@ -1,4 +1,3 @@
import CachedImage from '@app/components/Common/CachedImage';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
@@ -8,6 +7,7 @@ import {
ClockIcon,
} from '@heroicons/react/24/outline';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import Image from 'next/image';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { forwardRef, Fragment } from 'react';
@@ -56,9 +56,9 @@ const UserDropdown = () => {
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
data-testid="user-menu"
>
<CachedImage
<Image
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user && user.avatar ? `/avatarproxy/${user.avatar}` : ''}
src={user?.avatar || ''}
alt=""
width={40}
height={40}
@@ -79,9 +79,9 @@ const UserDropdown = () => {
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
<div className="flex flex-col space-y-4 px-4 py-4">
<div className="flex items-center space-x-2">
<CachedImage
<Image
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user && user.avatar ? `/avatarproxy/${user.avatar}` : ''}
src={user?.avatar || ''}
alt=""
width={40}
height={40}

View File

@@ -1,5 +1,4 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import SlideOver from '@app/components/Common/SlideOver';
import Tooltip from '@app/components/Common/Tooltip';
@@ -28,6 +27,7 @@ import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useIntl } from 'react-intl';
import useSWR from 'swr';
@@ -355,8 +355,8 @@ const ManageSlideOver = ({
key={`watch-user-${user.id}`}
content={user.displayName}
>
<CachedImage
src={`/avatarproxy/${user.avatar}`}
<Image
src={user.avatar}
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"
width={32}
@@ -516,8 +516,8 @@ const ManageSlideOver = ({
key={`watch-user-${user.id}`}
content={user.displayName}
>
<CachedImage
src={`/avatarproxy/${user.avatar}`}
<Image
src={user.avatar}
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"
width={32}

View File

@@ -22,6 +22,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer';
@@ -115,8 +116,8 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
className="group flex items-center"
>
<span className="avatar-sm">
<CachedImage
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
<Image
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
@@ -389,8 +390,8 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
className="group flex items-center"
>
<span className="avatar-sm">
<CachedImage
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
<Image
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}

View File

@@ -21,6 +21,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
@@ -189,8 +190,8 @@ const RequestItemError = ({
className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<CachedImage
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
<Image
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
@@ -248,8 +249,8 @@ const RequestItemError = ({
className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<CachedImage
src={`/avatarproxy/${requestData.modifiedBy.avatar}`}
<Image
src={requestData.modifiedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
@@ -556,8 +557,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<CachedImage
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
<Image
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
@@ -615,8 +616,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<CachedImage
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
<Image
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}

View File

@@ -1,5 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import CachedImage from '@app/components/Common/CachedImage';
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -15,6 +14,7 @@ import type {
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions';
import { isEqual } from 'lodash';
import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import Select from 'react-select';
@@ -561,8 +561,8 @@ const AdvancedRequester = ({
<span className="inline-block w-full rounded-md shadow-sm">
<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">
<CachedImage
src={`/avatarproxy/${selectedUser.avatar}`}
<Image
src={selectedUser.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
width={24}
@@ -613,8 +613,8 @@ const AdvancedRequester = ({
selected ? 'font-semibold' : 'font-normal'
} flex items-center`}
>
<CachedImage
src={`/avatarproxy/${user.avatar}`}
<Image
src={user.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
width={24}

View File

@@ -81,7 +81,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
'When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.',
imagecachecount: 'Images Cached',
imagecachesize: 'Total Cache Size',
useravatars: 'User Avatars',
}
);
@@ -559,19 +558,6 @@ const SettingsJobs = () => {
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
</Table.TD>
</tr>
<tr>
<Table.TD>
{intl.formatMessage(messages.useravatars)} (avatar)
</Table.TD>
<Table.TD>
{intl.formatNumber(
cacheData?.imageCache.avatar.imageCount ?? 0
)}
</Table.TD>
<Table.TD>
{formatBytes(cacheData?.imageCache.avatar.size ?? 0)}
</Table.TD>
</tr>
</Table.TBody>
</Table>
</div>

View File

@@ -1,11 +1,11 @@
import Alert from '@app/components/Common/Alert';
import CachedImage from '@app/components/Common/CachedImage';
import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import getConfig from 'next/config';
import Image from 'next/image';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -243,9 +243,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
</td>
<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">
<CachedImage
<Image
className="h-10 w-10 flex-shrink-0 rounded-full"
src={`/avatarproxy/${user.thumb}`}
src={user.thumb}
alt=""
width={40}
height={40}

View File

@@ -1,7 +1,6 @@
import Alert from '@app/components/Common/Alert';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Header from '@app/components/Common/Header';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal';
@@ -30,6 +29,7 @@ import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'
import { hasPermission } from '@server/lib/permissions';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -634,9 +634,9 @@ const UserList = () => {
href={`/users/${user.id}`}
className="h-10 w-10 flex-shrink-0"
>
<CachedImage
<Image
className="h-10 w-10 rounded-full object-cover"
src={`/avatarproxy/${user.avatar}`}
src={user.avatar}
alt=""
width={40}
height={40}

View File

@@ -1,9 +1,9 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import Image from 'next/image';
import Link from 'next/link';
import { useIntl } from 'react-intl';
@@ -42,9 +42,9 @@ const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
<div className="flex items-end justify-items-end space-x-5">
<div className="flex-shrink-0">
<div className="relative">
<CachedImage
<Image
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
src={`/avatarproxy/${user.avatar}`}
src={user.avatar}
alt=""
width={96}
height={96}

View File

@@ -820,7 +820,6 @@
"components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
"components.Settings.SettingsJobsCache.useravatars": "User Avatars",
"components.Settings.SettingsLogs.copiedLogMessage": "Copied log message to clipboard.",
"components.Settings.SettingsLogs.copyToClipboard": "Copy to Clipboard",
"components.Settings.SettingsLogs.extraData": "Additional Data",

59
src/i18n/locale/sl.json Normal file
View File

@@ -0,0 +1,59 @@
{
"components.Discover.CreateSlider.editsuccess": "Urejen drsnik in shranjene nastavitve prilagajanja odkrivanja.",
"components.CollectionDetails.numberofmovies": "{count} film/ov",
"components.Discover.CreateSlider.slidernameplaceholder": "Ime drsnika",
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Premiera ↓",
"components.AppDataWarning.dockerVolumeMissingDescription": "Pripenjanje nosilca <code>{appDataPath}</code> ni bilo pravilno konfigurirano. Vsi podatki bodo izbrisani, ko se vsebnik zaustavi ali znova zažene.",
"components.Discover.DiscoverMovies.sortPopularityDesc": "Priljubljenost ↑",
"components.AirDateBadge.airsrelative": "Predvajanje {relativeTime}",
"components.CollectionDetails.overview": "Pregled",
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Active Filter} drugo {# Active Filters}}",
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Ocena TMDB ↓",
"components.AirDateBadge.airedrelative": "Predvajano {relativeTime}",
"components.Discover.CreateSlider.searchStudios": "Iskanje studiev …",
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Datum izdaje ↑",
"components.Discover.CreateSlider.providetmdbnetwork": "Navedite ID omrežja TMDB",
"components.Discover.CreateSlider.addfail": "Novega drsnika ni bilo mogoče ustvariti.",
"components.CollectionDetails.requestcollection": "Zahtevaj zbirko",
"components.Discover.DiscoverMovieGenre.genreMovies": "Filmi: {genre}",
"components.Discover.DiscoverMovieLanguage.languageMovies": "Filmi: {language}",
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.DiscoverMovies.sortPopularityAsc": "Priljubljenost ↓",
"components.Discover.CreateSlider.needresults": "Imeti morate vsaj 1 rezultat.",
"components.Discover.CreateSlider.addcustomslider": "Ustvari drsnik po meri",
"components.Discover.DiscoverTv.sortPopularityAsc": "Priljubljenost ↓",
"components.Discover.CreateSlider.editSlider": "Uredi drsnik",
"components.Discover.DiscoverTv.sortTitleAsc": "Naslov (a-ž) ↓",
"components.Discover.CreateSlider.validationDatarequired": "Navesti morate vrednost podatkov.",
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Premiera ↑",
"components.Discover.DiscoverTv.discovertv": "Serije",
"components.Discover.DiscoverSliderEdit.deletefail": "Drsnika ni bilo mogoče izbrisati.",
"components.Discover.CreateSlider.providetmdbstudio": "Navedite ID studia v TMDB",
"components.Discover.DiscoverMovies.sortTitleDesc": "Naslov (a-ž) ↑",
"components.Discover.DiscoverStudio.studioMovies": "{studio} filmi",
"components.Discover.DiscoverTv.sortPopularityDesc": "Priljubljenost ↑",
"components.Discover.CreateSlider.searchGenres": "Išči žanre …",
"components.Discover.CreateSlider.editfail": "Drsnika ni bilo mogoče urediti.",
"components.Discover.CreateSlider.starttyping": "Tipkajte za iskanje.",
"components.Discover.DiscoverSliderEdit.enable": "Preklopi vidnost",
"components.Discover.CreateSlider.addSlider": "Dodaj drsnik",
"components.CollectionDetails.requestcollection4k": "Zahtevaj zbirko 4K",
"components.Discover.CreateSlider.providetmdbsearch": "Vnesite iskalno poizvedbo",
"components.Discover.DiscoverNetwork.networkSeries": "{network} serije",
"components.Discover.CreateSlider.providetmdbkeywordid": "Navedite ID ključne besede TMDB",
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Filmi: {keywordTitle}",
"components.Discover.CreateSlider.validationTitlerequired": "Navesti morate naslov.",
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Datum izdaje ↓",
"components.Discover.CreateSlider.nooptions": "Ni zadetkov.",
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Ocena TMDB ↑",
"components.Discover.CreateSlider.searchKeywords": "Iskanje po ključnih besedah …",
"components.Discover.CreateSlider.addsuccess": "Ustvarjen nov drsnik in shranjene nastavitve prilagajanja odkrivanja.",
"components.Discover.DiscoverSliderEdit.deletesuccess": "Drsnik je bil uspešno izbrisan.",
"components.Discover.DiscoverMovies.discovermovies": "Filmi",
"components.Discover.DiscoverMovies.sortTitleAsc": "Naslov (a-ž) ↓",
"components.Discover.CreateSlider.providetmdbgenreid": "Navedite ID žanra TMDB",
"components.Discover.DiscoverTv.sortTitleDesc": "Naslov (a-ž) ↑",
"components.Discover.DiscoverSliderEdit.remove": "Odstrani",
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB ↓",
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series"
}

1308
src/i18n/locale/tr.json Normal file

File diff suppressed because it is too large Load Diff