mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-25 03:11:39 -05:00
Compare commits
13 Commits
test-force
...
preview-av
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac21bb814 | ||
|
|
3f8ebc75d5 | ||
|
|
065d3002e0 | ||
|
|
b83367cbf2 | ||
|
|
0fd03f3848 | ||
|
|
9cb7e1495a | ||
|
|
0357d17205 | ||
|
|
049bc59d2d | ||
|
|
7c969f4235 | ||
|
|
bb95c7009f | ||
|
|
d4a6cb268a | ||
|
|
fb8677f29c | ||
|
|
c7284f473c |
@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
|
||||
name: jellyseerr-chart
|
||||
description: Jellyseerr helm chart for Kubernetes
|
||||
type: application
|
||||
version: 2.5.0
|
||||
appVersion: "2.6.0"
|
||||
version: 2.6.0
|
||||
appVersion: "2.7.0"
|
||||
maintainers:
|
||||
- name: Jellyseerr
|
||||
url: https://github.com/Fallenbagel/jellyseerr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# jellyseerr-chart
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Jellyseerr helm chart for Kubernetes
|
||||
|
||||
|
||||
@@ -105,6 +105,12 @@ In some places (like China), the ISP blocks not only the DNS resolution but also
|
||||
|
||||
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
|
||||
|
||||
### Option 3: Force IPV4 resolution first
|
||||
|
||||
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
||||
|
||||
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting Jellyseerr.
|
||||
|
||||
### Option 4: Check that your server can reach TMDB API
|
||||
|
||||
Make sure that your server can reach the TMDB API by running the following command:
|
||||
|
||||
@@ -4107,7 +4107,7 @@ paths:
|
||||
type: string
|
||||
userAgent:
|
||||
type: string
|
||||
/user/{userId}/pushSubscription/{key}:
|
||||
/user/{userId}/pushSubscription/{endpoint}:
|
||||
get:
|
||||
summary: Get web push notification settings for a user
|
||||
description: |
|
||||
@@ -4121,7 +4121,7 @@ paths:
|
||||
schema:
|
||||
type: number
|
||||
- in: path
|
||||
name: key
|
||||
name: endpoint
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@@ -4153,7 +4153,7 @@ paths:
|
||||
schema:
|
||||
type: number
|
||||
- in: path
|
||||
name: key
|
||||
name: endpoint
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
"@supercharge/request-ip": "1.2.0",
|
||||
"@svgr/webpack": "6.5.1",
|
||||
"@tanem/react-nprogress": "5.0.30",
|
||||
"@types/wink-jaro-distance": "^2.0.2",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/wink-jaro-distance": "^2.0.2",
|
||||
"ace-builds": "1.15.2",
|
||||
"axios": "1.3.4",
|
||||
"axios-rate-limit": "1.3.0",
|
||||
@@ -65,6 +65,8 @@
|
||||
"express-session": "1.17.3",
|
||||
"formik": "^2.4.6",
|
||||
"gravatar-url": "3.1.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"lodash": "4.17.21",
|
||||
"mime": "3",
|
||||
"next": "^14.2.25",
|
||||
@@ -101,8 +103,8 @@
|
||||
"swr": "2.2.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"typeorm": "0.3.12",
|
||||
"undici": "^7.3.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"undici": "^7.3.0",
|
||||
"web-push": "3.5.0",
|
||||
"wink-jaro-distance": "^2.0.0",
|
||||
"winston": "3.8.2",
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -107,6 +107,12 @@ importers:
|
||||
gravatar-url:
|
||||
specifier: 3.1.0
|
||||
version: 3.1.0
|
||||
http-proxy-agent:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
https-proxy-agent:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@@ -3626,6 +3632,10 @@ packages:
|
||||
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
agent-base@7.1.3:
|
||||
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
agentkeepalive@4.5.0:
|
||||
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -5788,8 +5798,8 @@ packages:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-signals@1.1.1:
|
||||
@@ -11122,7 +11132,7 @@ snapshots:
|
||||
'@babel/helper-split-export-declaration': 7.24.7
|
||||
'@babel/parser': 7.24.7
|
||||
'@babel/types': 7.24.7
|
||||
debug: 4.3.5
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -13339,7 +13349,7 @@ snapshots:
|
||||
fs-extra: 11.2.0
|
||||
globby: 11.1.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.5
|
||||
https-proxy-agent: 7.0.6
|
||||
issue-parser: 6.0.0
|
||||
lodash: 4.17.21
|
||||
mime: 3.0.0
|
||||
@@ -13961,7 +13971,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.2.0
|
||||
'@typescript-eslint/visitor-keys': 7.2.0
|
||||
debug: 4.3.5
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.3
|
||||
@@ -14052,10 +14062,12 @@ snapshots:
|
||||
|
||||
agent-base@7.1.1:
|
||||
dependencies:
|
||||
debug: 4.3.5
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.3: {}
|
||||
|
||||
agentkeepalive@4.5.0:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
@@ -15799,7 +15811,7 @@ snapshots:
|
||||
debug: 4.3.5
|
||||
enhanced-resolve: 5.17.0
|
||||
eslint: 8.35.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
@@ -15821,7 +15833,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
@@ -16821,7 +16833,7 @@ snapshots:
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.5
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -16848,10 +16860,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.5
|
||||
agent-base: 7.1.3
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ class ExternalAPI {
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
this.axios.interceptors.request = axios.interceptors.request;
|
||||
this.axios.interceptors.response = axios.interceptors.response;
|
||||
|
||||
if (options.rateLimit) {
|
||||
this.axios = rateLimit(this.axios, {
|
||||
|
||||
@@ -130,9 +130,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
const safeDeviceId =
|
||||
deviceId && deviceId.length > 0
|
||||
? deviceId
|
||||
: Buffer.from(`BOT_jellyseerr_fallback_${Date.now()}`).toString(
|
||||
'base64'
|
||||
);
|
||||
: Buffer.from('BOT_jellyseerr').toString('base64');
|
||||
|
||||
let authHeaderVal: string;
|
||||
if (authToken) {
|
||||
|
||||
@@ -123,6 +123,8 @@ class TautulliAPI {
|
||||
}${settings.urlBase ?? ''}`,
|
||||
params: { apikey: settings.apiKey },
|
||||
});
|
||||
this.axios.interceptors.request = axios.interceptors.request;
|
||||
this.axios.interceptors.response = axios.interceptors.response;
|
||||
}
|
||||
|
||||
public async getInfo(): Promise<TautulliInfo> {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { getAppVersion } from '@server/utils/appVersion';
|
||||
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import axios from 'axios';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
@@ -35,6 +36,8 @@ import express from 'express';
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
import type { Store } from 'express-session';
|
||||
import session from 'express-session';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import next from 'next';
|
||||
import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
@@ -73,6 +76,11 @@ app
|
||||
const settings = await getSettings().load();
|
||||
restartFlag.initializeSettings(settings);
|
||||
|
||||
if (settings.network.forceIpv4First) {
|
||||
axios.defaults.httpAgent = new http.Agent({ family: 4 });
|
||||
axios.defaults.httpsAgent = new https.Agent({ family: 4 });
|
||||
}
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.network.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.network.proxy);
|
||||
|
||||
@@ -150,6 +150,8 @@ class ImageProxy {
|
||||
baseURL: baseUrl,
|
||||
headers: options.headers,
|
||||
});
|
||||
this.axios.interceptors.request = axios.interceptors.request;
|
||||
this.axios.interceptors.response = axios.interceptors.response;
|
||||
|
||||
if (options.rateLimitOptions) {
|
||||
this.axios = rateLimit(this.axios, options.rateLimitOptions);
|
||||
|
||||
@@ -140,6 +140,7 @@ export interface MainSettings {
|
||||
|
||||
export interface NetworkSettings {
|
||||
csrfProtection: boolean;
|
||||
forceIpv4First: boolean;
|
||||
trustProxy: boolean;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
@@ -544,6 +545,7 @@ class Settings {
|
||||
},
|
||||
network: {
|
||||
csrfProtection: false,
|
||||
forceIpv4First: false,
|
||||
trustProxy: false,
|
||||
proxy: {
|
||||
enabled: false,
|
||||
|
||||
@@ -277,11 +277,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
select: { id: true, jellyfinDeviceId: true },
|
||||
});
|
||||
|
||||
let deviceId = '';
|
||||
if (user) {
|
||||
deviceId = user.jellyfinDeviceId ?? '';
|
||||
} else {
|
||||
deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString(
|
||||
let deviceId = 'BOT_jellyseerr';
|
||||
if (user && user.id === 1) {
|
||||
// Admin is always BOT_jellyseerr
|
||||
deviceId = 'BOT_jellyseerr';
|
||||
} else if (user && user.jellyfinDeviceId) {
|
||||
deviceId = user.jellyfinDeviceId;
|
||||
} else if (body.username) {
|
||||
deviceId = Buffer.from(`BOT_jellyseerr_${body.username}`).toString(
|
||||
'base64'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ async function initAvatarImageProxy() {
|
||||
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const deviceId = admin?.jellyfinDeviceId;
|
||||
const deviceId = admin?.jellyfinDeviceId || 'BOT_jellyseerr';
|
||||
const authToken = getSettings().jellyfin.apiKey;
|
||||
_avatarImageProxy = new ImageProxy('avatar', '', {
|
||||
headers: {
|
||||
|
||||
@@ -240,8 +240,8 @@ router.get<{ userId: number }>(
|
||||
}
|
||||
);
|
||||
|
||||
router.get<{ userId: number; key: string }>(
|
||||
'/:userId/pushSubscription/:key',
|
||||
router.get<{ userId: number; endpoint: string }>(
|
||||
'/:userId/pushSubscription/:endpoint',
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||
@@ -252,7 +252,7 @@ router.get<{ userId: number; key: string }>(
|
||||
},
|
||||
where: {
|
||||
user: { id: req.params.userId },
|
||||
p256dh: req.params.key,
|
||||
endpoint: req.params.endpoint,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -263,8 +263,8 @@ router.get<{ userId: number; key: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
router.delete<{ userId: number; key: string }>(
|
||||
'/:userId/pushSubscription/:key',
|
||||
router.delete<{ userId: number; endpoint: string }>(
|
||||
'/:userId/pushSubscription/:endpoint',
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||
@@ -275,7 +275,7 @@ router.delete<{ userId: number; key: string }>(
|
||||
},
|
||||
where: {
|
||||
user: { id: req.params.userId },
|
||||
p256dh: req.params.key,
|
||||
endpoint: req.params.endpoint,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -284,7 +284,7 @@ router.delete<{ userId: number; key: string }>(
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong deleting the user push subcription', {
|
||||
label: 'API',
|
||||
key: req.params.key,
|
||||
endpoint: req.params.endpoint,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
|
||||
@@ -421,7 +421,9 @@ userSettingsRoutes.post<{ username: string; password: string }>(
|
||||
|
||||
const hostname = getHostname();
|
||||
const deviceId = Buffer.from(
|
||||
`BOT_jellyseerr_${req.user.username ?? ''}`
|
||||
req.user?.id === 1
|
||||
? 'BOT_jellyseerr'
|
||||
: `BOT_jellyseerr_${req.user.username ?? ''}`
|
||||
).toString('base64');
|
||||
|
||||
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ProxySettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type { Dispatcher } from 'undici';
|
||||
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
@@ -54,17 +56,33 @@ export default async function createCustomProxyAgent(
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const proxyUrl = `${proxySettings.useSsl ? 'https' : 'http'}://${
|
||||
proxySettings.hostname
|
||||
}:${proxySettings.port}`;
|
||||
const proxyAgent = new ProxyAgent({
|
||||
uri:
|
||||
(proxySettings.useSsl ? 'https://' : 'http://') +
|
||||
proxySettings.hostname +
|
||||
':' +
|
||||
proxySettings.port,
|
||||
uri: proxyUrl,
|
||||
token,
|
||||
keepAliveTimeout: 5000,
|
||||
});
|
||||
|
||||
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||
|
||||
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, {
|
||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
||||
});
|
||||
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, {
|
||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
||||
});
|
||||
axios.interceptors.request.use((config) => {
|
||||
const url = config.baseURL
|
||||
? new URL(config.baseURL + (config.url || ''))
|
||||
: config.url;
|
||||
if (url && skipUrl(url)) {
|
||||
config.httpAgent = false;
|
||||
config.httpsAgent = false;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
||||
label: 'Proxy',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ ForwardedLink.displayName = 'ForwardedLink';
|
||||
|
||||
const UserDropdown = () => {
|
||||
const intl = useIntl();
|
||||
const { user, revalidate } = useUser();
|
||||
const { user, revalidate, hasPermission } = useUser();
|
||||
|
||||
const logout = async () => {
|
||||
const response = await axios.post('/api/v1/auth/logout');
|
||||
@@ -118,7 +118,14 @@ const UserDropdown = () => {
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<ForwardedLink
|
||||
href={`/users/${user?.id}/requests?filter=all`}
|
||||
href={
|
||||
hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? `/users/${user?.id}/requests?filter=all`
|
||||
: '/requests'
|
||||
}
|
||||
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
|
||||
active
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
|
||||
|
||||
@@ -74,6 +74,14 @@ const MediaSlider = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.currentSettings.hideBlacklisted) {
|
||||
titles = titles.filter(
|
||||
(i) =>
|
||||
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
|
||||
i.mediaInfo?.status !== MediaStatus.BLACKLISTED
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
titles.length < 24 &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import useRequestOverride from '@app/hooks/useRequestOverride';
|
||||
@@ -95,36 +96,58 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
|
||||
<div className="white mb-1 flex flex-nowrap">
|
||||
<Tooltip content={intl.formatMessage(messages.requestedby)}>
|
||||
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
<span className="flex w-40 items-center truncate md:w-auto">
|
||||
<Tooltip content={intl.formatMessage(messages.requestedby)}>
|
||||
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<Link
|
||||
href={
|
||||
request.requestedBy.id === user?.id
|
||||
? '/profile'
|
||||
: `/users/${request.requestedBy.id}`
|
||||
}
|
||||
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
className="flex items-center font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
>
|
||||
<span className="avatar-sm">
|
||||
<CachedImage
|
||||
type="avatar"
|
||||
src={request.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</span>
|
||||
{request.requestedBy.displayName}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
{request.modifiedBy && (
|
||||
<div className="flex flex-nowrap">
|
||||
<Tooltip content={intl.formatMessage(messages.lastmodifiedby)}>
|
||||
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<span className="w-40 truncate md:w-auto">
|
||||
<span className="flex w-40 items-center truncate md:w-auto">
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.lastmodifiedby)}
|
||||
>
|
||||
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
<Link
|
||||
href={
|
||||
request.modifiedBy.id === user?.id
|
||||
? '/profile'
|
||||
: `/users/${request.modifiedBy.id}`
|
||||
}
|
||||
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
className="flex items-center font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
>
|
||||
<span className="avatar-sm">
|
||||
<CachedImage
|
||||
type="avatar"
|
||||
src={request.modifiedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</span>
|
||||
{request.modifiedBy.displayName}
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
@@ -6,6 +6,7 @@ import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { isValidURL } from '@app/utils/urlValidationHelper';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import type { NotificationAgentNtfy } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
@@ -44,7 +45,7 @@ const NotificationsNtfy = () => {
|
||||
data,
|
||||
error,
|
||||
mutate: revalidate,
|
||||
} = useSWR('/api/v1/settings/notifications/ntfy');
|
||||
} = useSWR<NotificationAgentNtfy>('/api/v1/settings/notifications/ntfy');
|
||||
|
||||
const NotificationsNtfySchema = Yup.object().shape({
|
||||
url: Yup.string()
|
||||
@@ -78,15 +79,15 @@ const NotificationsNtfy = () => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
enabled: data.enabled,
|
||||
types: data.types,
|
||||
url: data.options.url,
|
||||
topic: data.options.topic,
|
||||
authMethodUsernamePassword: data.options.authMethod,
|
||||
username: data.options.username,
|
||||
password: data.options.password,
|
||||
authMethodToken: data.options.authMethodToken,
|
||||
token: data.options.token,
|
||||
enabled: data?.enabled,
|
||||
types: data?.types,
|
||||
url: data?.options.url,
|
||||
topic: data?.options.topic,
|
||||
authMethodUsernamePassword: data?.options.authMethodUsernamePassword,
|
||||
username: data?.options.username,
|
||||
password: data?.options.password,
|
||||
authMethodToken: data?.options.authMethodToken,
|
||||
token: data?.options.token,
|
||||
}}
|
||||
validationSchema={NotificationsNtfySchema}
|
||||
onSubmit={async (values) => {
|
||||
@@ -302,7 +303,7 @@ const NotificationsNtfy = () => {
|
||||
</div>
|
||||
)}
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.enabled ? values.types : 0}
|
||||
currentTypes={values.enabled ? values.types || 0 : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
|
||||
@@ -66,6 +66,8 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
||||
enableSpecialEpisodes: 'Allow Special Episodes Requests',
|
||||
locale: 'Display Language',
|
||||
youtubeUrl: 'YouTube URL',
|
||||
youtubeUrlTip:
|
||||
'Base URL for YouTube videos if a self-hosted YouTube instance is used.',
|
||||
validationUrl: 'You must provide a valid URL',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
});
|
||||
@@ -536,6 +538,9 @@ const SettingsMain = () => {
|
||||
<div className="form-row">
|
||||
<label htmlFor="youtubeUrl" className="text-label">
|
||||
{intl.formatMessage(messages.youtubeUrl)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.youtubeUrlTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
|
||||
@@ -42,6 +42,9 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
|
||||
networkDisclaimer:
|
||||
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
|
||||
docs: 'documentation',
|
||||
forceIpv4First: 'Force IPv4 Resolution First',
|
||||
forceIpv4FirstTip:
|
||||
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
|
||||
});
|
||||
|
||||
const SettingsNetwork = () => {
|
||||
@@ -86,6 +89,7 @@ const SettingsNetwork = () => {
|
||||
<Formik
|
||||
initialValues={{
|
||||
csrfProtection: data?.csrfProtection,
|
||||
forceIpv4First: data?.forceIpv4First,
|
||||
trustProxy: data?.trustProxy,
|
||||
proxyEnabled: data?.proxy?.enabled,
|
||||
proxyHostname: data?.proxy?.hostname,
|
||||
@@ -102,6 +106,7 @@ const SettingsNetwork = () => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/network', {
|
||||
csrfProtection: values.csrfProtection,
|
||||
forceIpv4First: values.forceIpv4First,
|
||||
trustProxy: values.trustProxy,
|
||||
proxy: {
|
||||
enabled: values.proxyEnabled,
|
||||
@@ -193,6 +198,29 @@ const SettingsNetwork = () => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="forceIpv4First" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.forceIpv4First)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
<SettingsBadge badgeType="experimental" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.forceIpv4FirstTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="forceIpv4First"
|
||||
name="forceIpv4First"
|
||||
onChange={() => {
|
||||
setFieldValue('forceIpv4First', !values.forceIpv4First);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
|
||||
@@ -33,13 +33,14 @@ const messages = defineMessages(
|
||||
|
||||
const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
const intl = useIntl();
|
||||
const parsedUserAgent = UAParser(device.userAgent);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
|
||||
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
|
||||
<div className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
|
||||
{UAParser(device.userAgent).device.type === 'mobile' ? (
|
||||
{parsedUserAgent.device.type === 'mobile' ? (
|
||||
<DevicePhoneMobileIcon />
|
||||
) : (
|
||||
<ComputerDesktopIcon />
|
||||
@@ -56,8 +57,8 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
: 'N/A'}
|
||||
</div>
|
||||
<div className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
|
||||
{device.userAgent
|
||||
? UAParser(device.userAgent).device.model
|
||||
{device.userAgent && parsedUserAgent.device.model
|
||||
? parsedUserAgent.device.model
|
||||
: intl.formatMessage(messages.unknown)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +69,7 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
{intl.formatMessage(messages.operatingsystem)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{device.userAgent ? UAParser(device.userAgent).os.name : 'N/A'}
|
||||
{device.userAgent ? parsedUserAgent.os.name : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-field">
|
||||
@@ -76,9 +77,7 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
{intl.formatMessage(messages.browser)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{device.userAgent
|
||||
? UAParser(device.userAgent).browser.name
|
||||
: 'N/A'}
|
||||
{device.userAgent ? parsedUserAgent.browser.name : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-field">
|
||||
@@ -86,16 +85,14 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
{intl.formatMessage(messages.engine)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{device.userAgent
|
||||
? UAParser(device.userAgent).engine.name
|
||||
: 'N/A'}
|
||||
{device.userAgent ? parsedUserAgent.engine.name : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||
<ConfirmButton
|
||||
onClick={() => disablePushNotifications(device.p256dh)}
|
||||
onClick={() => disablePushNotifications(device.endpoint)}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
|
||||
@@ -113,7 +113,7 @@ const UserWebPushSettings = () => {
|
||||
|
||||
// Unsubscribes from the push manager
|
||||
// Deletes/disables corresponding push subscription from database
|
||||
const disablePushNotifications = async (p256dh?: string) => {
|
||||
const disablePushNotifications = async (endpoint?: string) => {
|
||||
if ('serviceWorker' in navigator && user?.id) {
|
||||
navigator.serviceWorker.getRegistration('/sw.js').then((registration) => {
|
||||
registration?.pushManager
|
||||
@@ -122,17 +122,21 @@ const UserWebPushSettings = () => {
|
||||
const parsedSub = JSON.parse(JSON.stringify(subscription));
|
||||
|
||||
await axios.delete(
|
||||
`/api/v1/user/${user?.id}/pushSubscription/${
|
||||
p256dh ? p256dh : parsedSub.keys.p256dh
|
||||
}`
|
||||
`/api/v1/user/${user.id}/pushSubscription/${encodeURIComponent(
|
||||
endpoint ?? parsedSub.endpoint
|
||||
)}`
|
||||
);
|
||||
if (subscription && (p256dh === parsedSub.keys.p256dh || !p256dh)) {
|
||||
|
||||
if (
|
||||
subscription &&
|
||||
(endpoint === parsedSub.endpoint || !endpoint)
|
||||
) {
|
||||
subscription.unsubscribe();
|
||||
setWebPushEnabled(false);
|
||||
}
|
||||
addToast(
|
||||
intl.formatMessage(
|
||||
p256dh
|
||||
endpoint
|
||||
? messages.subscriptiondeleted
|
||||
: messages.webpushhasbeendisabled
|
||||
),
|
||||
@@ -145,7 +149,7 @@ const UserWebPushSettings = () => {
|
||||
.catch(function () {
|
||||
addToast(
|
||||
intl.formatMessage(
|
||||
p256dh
|
||||
endpoint
|
||||
? messages.subscriptiondeleteerror
|
||||
: messages.disablingwebpusherror
|
||||
),
|
||||
@@ -176,12 +180,17 @@ const UserWebPushSettings = () => {
|
||||
const parsedKey = JSON.parse(JSON.stringify(subscription));
|
||||
const currentUserPushSub =
|
||||
await axios.get<UserPushSubscription>(
|
||||
`/api/v1/user/${user.id}/pushSubscription/${parsedKey.keys.p256dh}`
|
||||
`/api/v1/user/${
|
||||
user.id
|
||||
}/pushSubscription/${encodeURIComponent(
|
||||
parsedKey.endpoint
|
||||
)}`
|
||||
);
|
||||
|
||||
if (currentUserPushSub.data.p256dh !== parsedKey.keys.p256dh) {
|
||||
if (currentUserPushSub.data.endpoint !== parsedKey.endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWebPushEnabled(true);
|
||||
} else {
|
||||
setWebPushEnabled(false);
|
||||
|
||||
@@ -160,9 +160,12 @@ const UserProfile = () => {
|
||||
<dd className="mt-1 text-3xl font-semibold text-white">
|
||||
<Link
|
||||
href={
|
||||
user.id === currentUser?.id
|
||||
? '/profile/requests?filter=all'
|
||||
: `/users/${user?.id}/requests?filter=all`
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? `/users/${user?.id}/requests?filter=all`
|
||||
: '/requests'
|
||||
}
|
||||
>
|
||||
{intl.formatNumber(user.requestCount)}
|
||||
@@ -293,9 +296,12 @@ const UserProfile = () => {
|
||||
<div className="slider-header">
|
||||
<Link
|
||||
href={
|
||||
user.id === currentUser?.id
|
||||
? '/profile/requests?filter=all'
|
||||
: `/users/${user?.id}/requests?filter=all`
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? `/users/${user?.id}/requests?filter=all`
|
||||
: '/requests'
|
||||
}
|
||||
className="slider-title"
|
||||
>
|
||||
|
||||
@@ -978,10 +978,13 @@
|
||||
"components.Settings.SettingsMain.validationUrl": "You must provide a valid URL",
|
||||
"components.Settings.SettingsMain.validationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
"components.Settings.SettingsMain.youtubeUrl": "YouTube URL",
|
||||
"components.Settings.SettingsMain.youtubeUrlTip": "Base URL for YouTube videos if a self-hosted YouTube instance is used.",
|
||||
"components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||
"components.Settings.SettingsNetwork.docs": "documentation",
|
||||
"components.Settings.SettingsNetwork.forceIpv4First": "Force IPv4 Resolution First",
|
||||
"components.Settings.SettingsNetwork.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6",
|
||||
"components.Settings.SettingsNetwork.network": "Network",
|
||||
"components.Settings.SettingsNetwork.networkDisclaimer": "Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.",
|
||||
"components.Settings.SettingsNetwork.networksettings": "Network Settings",
|
||||
@@ -1213,7 +1216,7 @@
|
||||
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
|
||||
"components.Setup.servertype": "Choose Server Type",
|
||||
"components.Setup.setup": "Setup",
|
||||
"components.Setup.signin": "Sign in to your account",
|
||||
"components.Setup.signin": "Sign In",
|
||||
"components.Setup.signinMessage": "Get started by signing in",
|
||||
"components.Setup.signinWithEmby": "Enter your Emby details",
|
||||
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import RequestList from '@app/components/RequestList';
|
||||
import type { NextPage } from 'next';
|
||||
|
||||
const UserRequestsPage: NextPage = () => {
|
||||
return <RequestList />;
|
||||
};
|
||||
|
||||
export default UserRequestsPage;
|
||||
Reference in New Issue
Block a user