Compare commits

..

1 Commits

Author SHA1 Message Date
gauthier-th
f797e06e18 fix(proxy): modify the registration of the axios interceptors
The previous way of adding Axios interceptors added a new interceptor each time, causing lags after
a while because of all the duplicate interceptors added.

fix #1787
2025-07-20 00:14:56 +02:00
32 changed files with 274 additions and 1001 deletions

View File

@@ -4,7 +4,6 @@ dist/
config/
CHANGELOG.md
pnpm-lock.yaml
cypress/config/settings.cypress.json
# assets
src/assets/

View File

@@ -21,11 +21,5 @@ module.exports = {
rangeEnd: 0, // default: Infinity
},
},
{
files: 'cypress/config/settings.cypress.json',
options: {
rangeEnd: 0,
},
},
],
};

View File

@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
name: jellyseerr-chart
description: Jellyseerr helm chart for Kubernetes
type: application
version: 2.6.2
appVersion: "2.7.3"
version: 2.6.1
appVersion: "2.7.1"
maintainers:
- name: Jellyseerr
url: https://github.com/Fallenbagel/jellyseerr

View File

@@ -1,6 +1,6 @@
# jellyseerr-chart
![Version: 2.6.2](https://img.shields.io/badge/Version-2.6.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.7.3](https://img.shields.io/badge/AppVersion-2.7.3-informational?style=flat-square)
![Version: 2.6.1](https://img.shields.io/badge/Version-2.6.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.7.1](https://img.shields.io/badge/AppVersion-2.7.1-informational?style=flat-square)
Jellyseerr helm chart for Kubernetes

View File

@@ -6,6 +6,7 @@
"apiKey": "testkey",
"applicationTitle": "Jellyseerr",
"applicationUrl": "",
"csrfProtection": false,
"cacheImages": false,
"defaultPermissions": 32,
"defaultQuotas": {
@@ -179,26 +180,5 @@
"image-cache-cleanup": {
"schedule": "0 0 5 * * *"
}
},
"network": {
"csrfProtection": false,
"trustProxy": false,
"forceIpv4First": false,
"dnsServers": "",
"proxy": {
"enabled": false,
"hostname": "",
"port": 8080,
"useSsl": false,
"user": "",
"password": "",
"bypassFilter": "",
"bypassLocalAddresses": true
},
"dnsCache": {
"enabled": false,
"forceMinTtl": 0,
"forceMaxTtl": -1
}
}
}

View File

@@ -1,16 +0,0 @@
---
title: DNS Caching
description: Configure DNS caching settings.
sidebar_position: 7
---
# DNS Caching
Jellyseerr uses DNS caching to improve performance and reduce the number of DNS lookups required for external API calls. This can help speed up response times and reduce load on DNS servers, when something like a Pi-hole is used as a DNS resolver.
## Configuration
You can enable the DNS caching settings in the Network tab of the Jellyseerr settings. The default values follow the standard DNS caching behavior.
- **Force Minimum TTL**: Set a minimum time-to-live (TTL) in seconds for DNS cache entries. This ensures that frequently accessed DNS records are cached for a longer period, reducing the need for repeated lookups. Default is 0.
- **Force Maximum TTL**: Set a maximum time-to-live (TTL) in seconds for DNS cache entries. This prevents infrequently accessed DNS records from being cached indefinitely, allowing for more up-to-date information to be retrieved. Default is -1 (unlimited).

View File

@@ -1,7 +1,6 @@
---
title: Jobs & Cache
description: Configure jobs and cache settings.
sidebar_position: 6
---
# Jobs & Cache

View File

@@ -141,83 +141,14 @@ components:
UserSettings:
type: object
properties:
username:
type: string
nullable: true
example: 'Mr User'
email:
type: string
example: 'user@example.com'
discordId:
type: string
nullable: true
example: '123456789'
locale:
type: string
nullable: true
example: 'en'
discoverRegion:
type: string
nullable: true
example: 'US'
streamingRegion:
type: string
nullable: true
example: 'US'
originalLanguage:
type: string
nullable: true
example: 'en'
movieQuotaLimit:
type: number
nullable: true
description: 'Maximum number of movie requests allowed'
example: 10
movieQuotaDays:
type: number
nullable: true
description: 'Time period in days for movie quota'
example: 30
tvQuotaLimit:
type: number
nullable: true
description: 'Maximum number of TV requests allowed'
example: 5
tvQuotaDays:
type: number
nullable: true
description: 'Time period in days for TV quota'
example: 14
globalMovieQuotaDays:
type: number
nullable: true
description: 'Global movie quota days setting'
example: 30
globalMovieQuotaLimit:
type: number
nullable: true
description: 'Global movie quota limit setting'
example: 10
globalTvQuotaLimit:
type: number
nullable: true
description: 'Global TV quota limit setting'
example: 5
globalTvQuotaDays:
type: number
nullable: true
description: 'Global TV quota days setting'
example: 14
watchlistSyncMovies:
type: boolean
nullable: true
description: 'Enable watchlist sync for movies'
example: true
watchlistSyncTv:
type: boolean
nullable: true
description: 'Enable watchlist sync for TV'
example: false
streamingRegion:
type: string
MainSettings:
type: object
properties:
@@ -260,51 +191,9 @@ components:
csrfProtection:
type: boolean
example: false
forceIpv4First:
type: boolean
example: false
trustProxy:
type: boolean
example: false
proxy:
type: object
properties:
enabled:
type: boolean
example: false
hostname:
type: string
example: ''
port:
type: number
example: 8080
useSsl:
type: boolean
example: false
user:
type: string
example: ''
password:
type: string
example: ''
bypassFilter:
type: string
example: ''
bypassLocalAddresses:
type: boolean
example: true
dnsCache:
type: object
properties:
enabled:
type: boolean
example: false
forceMinTtl:
type: number
example: 0
forceMaxTtl:
type: number
example: -1
example: true
PlexLibrary:
type: object
properties:
@@ -3009,68 +2898,6 @@ paths:
imageCount:
type: number
example: 123
dnsCache:
type: object
properties:
stats:
type: object
properties:
size:
type: number
example: 1
maxSize:
type: number
example: 500
hits:
type: number
example: 19
misses:
type: number
example: 1
failures:
type: number
example: 0
ipv4Fallbacks:
type: number
example: 0
hitRate:
type: number
example: 0.95
entries:
type: array
additionalProperties:
type: object
properties:
addresses:
type: object
properties:
ipv4:
type: number
example: 1
ipv6:
type: number
example: 1
activeAddress:
type: string
example: 127.0.0.1
family:
type: number
example: 4
age:
type: number
example: 10
ttl:
type: number
example: 10
networkErrors:
type: number
example: 0
hits:
type: number
example: 1
misses:
type: number
example: 1
apiCaches:
type: array
items:
@@ -3110,21 +2937,6 @@ paths:
responses:
'204':
description: 'Flushed cache'
/settings/cache/dns/{dnsEntry}/flush:
post:
summary: Flush a specific DNS cache entry
description: Flushes a specific DNS cache entry
tags:
- settings
parameters:
- in: path
name: dnsEntry
required: true
schema:
type: string
responses:
'204':
description: 'Flushed dns cache'
/settings/logs:
get:
summary: Returns logs
@@ -4657,7 +4469,11 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UserSettings'
type: object
properties:
username:
type: string
example: 'Mr User'
post:
summary: Update general settings for a user
description: Updates and returns general settings for a specific user. Requires `MANAGE_USERS` permission if editing other users.
@@ -4674,14 +4490,22 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UserSettings'
type: object
properties:
username:
type: string
nullable: true
responses:
'200':
description: Updated user general settings returned
content:
application/json:
schema:
$ref: '#/components/schemas/UserSettings'
type: object
properties:
username:
type: string
example: 'Mr User'
/user/{userId}/settings/password:
get:
summary: Get password page informatiom
@@ -6775,16 +6599,9 @@ paths:
example: '1'
schema:
type: string
- in: query
name: is4k
description: Whether to remove from 4K service instance (true) or regular service instance (false)
required: false
example: false
schema:
type: boolean
responses:
'204':
description: Successfully removed media item
description: Succesfully removed media item
/media/{mediaId}/{status}:
post:
summary: Update media status
@@ -7451,22 +7268,11 @@ paths:
example: 1
responses:
'200':
description: Keyword returned (null if not found)
description: Keyword returned
content:
application/json:
schema:
nullable: true
$ref: '#/components/schemas/Keyword'
'500':
description: Internal server error
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: 'Unable to retrieve keyword data.'
/watchproviders/regions:
get:
summary: Get watch provider regions

View File

@@ -57,7 +57,6 @@
"cronstrue": "2.23.0",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"dns-caching": "^0.2.4",
"email-templates": "12.0.1",
"email-validator": "2.0.4",
"express": "4.21.2",

214
pnpm-lock.yaml generated
View File

@@ -83,9 +83,6 @@ importers:
dayjs:
specifier: 1.11.7
version: 1.11.7
dns-caching:
specifier: ^0.2.4
version: 0.2.4
email-templates:
specifier: 12.0.1
version: 12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7)
@@ -691,8 +688,8 @@ packages:
resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.28.2':
resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==}
'@babel/helpers@7.27.6':
resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
engines: {node: '>=6.9.0'}
'@babel/highlight@7.24.7':
@@ -1458,8 +1455,8 @@ packages:
resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.2':
resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==}
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/template@7.24.7':
@@ -1486,8 +1483,8 @@ packages:
resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.2':
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
'@babel/types@7.28.1':
resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==}
engines: {node: '>=6.9.0'}
'@codedependant/semantic-release-docker@5.1.0':
@@ -1978,8 +1975,8 @@ packages:
resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/gen-mapping@0.3.12':
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
@@ -1993,20 +1990,20 @@ packages:
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.11':
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
'@jridgewell/source-map@0.3.10':
resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/sourcemap-codec@1.5.4':
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@jridgewell/trace-mapping@0.3.30':
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
@@ -3232,8 +3229,8 @@ packages:
'@swc/helpers@0.5.5':
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
'@swc/types@0.1.24':
resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==}
'@swc/types@0.1.23':
resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==}
'@tailwindcss/aspect-ratio@0.4.2':
resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==}
@@ -3394,8 +3391,8 @@ packages:
'@types/node@17.0.45':
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
'@types/node@18.19.122':
resolution: {integrity: sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==}
'@types/node@18.19.118':
resolution: {integrity: sha512-hIPK0hSrrcaoAu/gJMzN3QClXE4QdCdFvaenJ0JsjIbExP1JFFVH+RHcBt25c9n8bx5dkIfqKE+uw6BmBns7ug==}
'@types/node@20.5.1':
resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==}
@@ -4090,6 +4087,11 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.24.3:
resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.25.1:
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -4194,8 +4196,8 @@ packages:
caniuse-lite@1.0.30001700:
resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==}
caniuse-lite@1.0.30001734:
resolution: {integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==}
caniuse-lite@1.0.30001727:
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@@ -4424,8 +4426,8 @@ packages:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'}
compression@1.8.1:
resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
compression@1.8.0:
resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==}
engines: {node: '>= 0.8.0'}
computed-style@0.1.4:
@@ -4545,8 +4547,8 @@ packages:
core-js-compat@3.37.1:
resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==}
core-js-compat@3.45.0:
resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==}
core-js-compat@3.44.0:
resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==}
core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@@ -4864,9 +4866,6 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dns-caching@0.2.4:
resolution: {integrity: sha512-J48CLnMScOAtWIdExkz+522A0nPUwG5o+w7vVsXBJDipVLugCnps5AVJMn9bOkqQm4GarHtutMHYJEryCTeMjA==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -4938,8 +4937,8 @@ packages:
electron-to-chromium@1.4.810:
resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==}
electron-to-chromium@1.5.200:
resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==}
electron-to-chromium@1.5.182:
resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==}
email-templates@12.0.1:
resolution: {integrity: sha512-849pjBFVUAWWTa3HqhDjxlXHaSWmxf4CZOlZ9iVkrSAbQ8YCYi+7KiKqt35L6F20WhSViWX7lmMjno6zBv2rNQ==}
@@ -5507,8 +5506,8 @@ packages:
flow-enums-runtime@0.0.6:
resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
flow-parser@0.278.0:
resolution: {integrity: sha512-9oUcYDHf9n+E/t0FXndgBqGbaUsGEcmWqIr1ldqCzTzctsJV5E/bHusOj4ThB72Ss2mqWpLFNz0+o2c1O8J6+A==}
flow-parser@0.275.0:
resolution: {integrity: sha512-fHNwawoA2LM7FsxhU/1lTRGq9n6/Q8k861eHgN7GKtamYt9Qrxpg/ZSrev8o1WX7fQ2D3Gg3+uvYN15PmsG7Yw==}
engines: {node: '>=0.4.0'}
fn.name@1.1.0:
@@ -6745,10 +6744,6 @@ packages:
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
engines: {node: 14 || >=16.14}
lru-cache@11.1.0:
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
engines: {node: 20 || >=22}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -7539,10 +7534,6 @@ packages:
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
engines: {node: '>= 0.8'}
on-headers@1.1.0:
resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -8847,9 +8838,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'}
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
@@ -9902,8 +9893,8 @@ packages:
engines: {node: '>= 14'}
hasBin: true
yaml@2.8.1:
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
@@ -10047,11 +10038,11 @@ snapshots:
'@babel/generator': 7.28.0
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
'@babel/helpers': 7.28.2
'@babel/helpers': 7.27.6
'@babel/parser': 7.28.0
'@babel/template': 7.27.2
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
convert-source-map: 2.0.0
debug: 4.4.1
gensync: 1.0.0-beta.2
@@ -10070,9 +10061,9 @@ snapshots:
'@babel/generator@7.28.0':
dependencies:
'@babel/parser': 7.28.0
'@babel/types': 7.28.2
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.30
'@babel/types': 7.28.1
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.24.7':
@@ -10081,7 +10072,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
'@babel/types': 7.28.2
'@babel/types': 7.28.1
'@babel/helper-builder-binary-assignment-operator-visitor@7.24.7':
dependencies:
@@ -10102,7 +10093,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.28.0
'@babel/helper-validator-option': 7.27.1
browserslist: 4.25.1
browserslist: 4.24.3
lru-cache: 5.1.1
semver: 6.3.1
@@ -10208,7 +10199,7 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
transitivePeerDependencies:
- supports-color
@@ -10222,7 +10213,7 @@ snapshots:
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
transitivePeerDependencies:
- supports-color
@@ -10261,7 +10252,7 @@ snapshots:
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
'@babel/types': 7.28.2
'@babel/types': 7.28.1
'@babel/helper-plugin-utils@7.24.7': {}
@@ -10329,7 +10320,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
transitivePeerDependencies:
- supports-color
@@ -10366,7 +10357,7 @@ snapshots:
dependencies:
'@babel/template': 7.27.2
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
transitivePeerDependencies:
- supports-color
@@ -10375,10 +10366,10 @@ snapshots:
'@babel/template': 7.24.7
'@babel/types': 7.24.7
'@babel/helpers@7.28.2':
'@babel/helpers@7.27.6':
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.28.2
'@babel/types': 7.28.1
'@babel/highlight@7.24.7':
dependencies:
@@ -10397,7 +10388,7 @@ snapshots:
'@babel/parser@7.28.0':
dependencies:
'@babel/types': 7.28.2
'@babel/types': 7.28.1
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)':
dependencies:
@@ -11089,7 +11080,7 @@ snapshots:
'@babel/helper-module-imports': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.24.7)
'@babel/types': 7.28.2
'@babel/types': 7.28.1
transitivePeerDependencies:
- supports-color
@@ -11383,7 +11374,7 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.2': {}
'@babel/runtime@7.27.6': {}
'@babel/template@7.24.7':
dependencies:
@@ -11395,7 +11386,7 @@ snapshots:
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
'@babel/traverse@7.24.7':
dependencies:
@@ -11419,7 +11410,7 @@ snapshots:
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.28.0
'@babel/template': 7.27.2
'@babel/types': 7.28.2
'@babel/types': 7.28.1
debug: 4.4.1
transitivePeerDependencies:
- supports-color
@@ -11435,7 +11426,7 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.28.2':
'@babel/types@7.28.1':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
@@ -12100,10 +12091,10 @@ snapshots:
'@types/yargs': 17.0.33
chalk: 4.1.2
'@jridgewell/gen-mapping@0.3.13':
'@jridgewell/gen-mapping@0.3.12':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.30
'@jridgewell/sourcemap-codec': 1.5.4
'@jridgewell/trace-mapping': 0.3.29
'@jridgewell/gen-mapping@0.3.5':
dependencies:
@@ -12115,24 +12106,24 @@ snapshots:
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/source-map@0.3.11':
'@jridgewell/source-map@0.3.10':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.30
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
'@jridgewell/sourcemap-codec@1.4.15': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/sourcemap-codec@1.5.4': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping@0.3.30':
'@jridgewell/trace-mapping@0.3.29':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/sourcemap-codec': 1.5.4
'@jridgewell/trace-mapping@0.3.9':
dependencies:
@@ -12880,7 +12871,7 @@ snapshots:
semver: 7.7.1
strip-ansi: 5.2.0
wcwidth: 1.0.1
yaml: 2.8.1
yaml: 2.8.0
transitivePeerDependencies:
- encoding
@@ -12925,7 +12916,7 @@ snapshots:
dependencies:
'@react-native-community/cli-debugger-ui': 13.6.8
'@react-native-community/cli-tools': 13.6.8(encoding@0.1.13)
compression: 1.8.1
compression: 1.8.0
connect: 3.7.0
errorhandler: 1.5.1
nocache: 3.0.4
@@ -13401,7 +13392,7 @@ snapshots:
'@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0)':
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.27.6
'@types/react-reconciler': 0.26.7
'@types/webxr': 0.5.22
base64-js: 1.5.1
@@ -13556,7 +13547,7 @@ snapshots:
'@rnx-kit/chromium-edge-launcher@1.0.0':
dependencies:
'@types/node': 18.19.122
'@types/node': 18.19.118
escape-string-regexp: 4.0.0
is-wsl: 2.2.0
lighthouse-logger: 1.4.2
@@ -13819,7 +13810,7 @@ snapshots:
'@swc/core@1.6.5(@swc/helpers@0.5.11)':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.24
'@swc/types': 0.1.23
optionalDependencies:
'@swc/core-darwin-arm64': 1.6.5
'@swc/core-darwin-x64': 1.6.5
@@ -13844,7 +13835,7 @@ snapshots:
'@swc/counter': 0.1.3
tslib: 2.8.1
'@swc/types@0.1.24':
'@swc/types@0.1.23':
dependencies:
'@swc/counter': 0.1.3
@@ -14022,7 +14013,7 @@ snapshots:
'@types/node@17.0.45': {}
'@types/node@18.19.122':
'@types/node@18.19.118':
dependencies:
undici-types: 5.26.5
@@ -14698,7 +14689,7 @@ snapshots:
dependencies:
'@babel/core': 7.24.7
'@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.24.7)
core-js-compat: 3.45.0
core-js-compat: 3.44.0
transitivePeerDependencies:
- supports-color
@@ -14813,10 +14804,17 @@ snapshots:
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.1)
browserslist@4.24.3:
dependencies:
caniuse-lite: 1.0.30001727
electron-to-chromium: 1.5.182
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.3)
browserslist@4.25.1:
dependencies:
caniuse-lite: 1.0.30001734
electron-to-chromium: 1.5.200
caniuse-lite: 1.0.30001727
electron-to-chromium: 1.5.182
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1)
@@ -14947,7 +14945,7 @@ snapshots:
caniuse-lite@1.0.30001700: {}
caniuse-lite@1.0.30001734: {}
caniuse-lite@1.0.30001727: {}
caseless@0.12.0: {}
@@ -15197,13 +15195,13 @@ snapshots:
dependencies:
mime-db: 1.54.0
compression@1.8.1:
compression@1.8.0:
dependencies:
bytes: 3.1.2
compressible: 2.0.18
debug: 2.6.9
negotiator: 0.6.4
on-headers: 1.1.0
on-headers: 1.0.2
safe-buffer: 5.2.1
vary: 1.1.2
transitivePeerDependencies:
@@ -15334,7 +15332,7 @@ snapshots:
dependencies:
browserslist: 4.23.1
core-js-compat@3.45.0:
core-js-compat@3.44.0:
dependencies:
browserslist: 4.25.1
@@ -15700,10 +15698,6 @@ snapshots:
dlv@1.1.3: {}
dns-caching@0.2.4:
dependencies:
lru-cache: 11.1.0
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@@ -15788,7 +15782,7 @@ snapshots:
electron-to-chromium@1.4.810: {}
electron-to-chromium@1.5.200: {}
electron-to-chromium@1.5.182: {}
email-templates@12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7):
dependencies:
@@ -16708,7 +16702,7 @@ snapshots:
flow-enums-runtime@0.0.6: {}
flow-parser@0.278.0: {}
flow-parser@0.275.0: {}
fn.name@1.1.0: {}
@@ -17098,7 +17092,7 @@ snapshots:
hermes-profile-transformer@0.0.6:
dependencies:
source-map: 0.7.6
source-map: 0.7.4
highlight.js@10.7.3: {}
@@ -17691,7 +17685,7 @@ snapshots:
'@babel/register': 7.27.1(@babel/core@7.28.0)
babel-core: 7.0.0-bridge.0(@babel/core@7.28.0)
chalk: 4.1.2
flow-parser: 0.278.0
flow-parser: 0.275.0
graceful-fs: 4.2.11
micromatch: 4.0.8
neo-async: 2.6.2
@@ -18042,8 +18036,6 @@ snapshots:
lru-cache@10.2.2: {}
lru-cache@11.1.0: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -18315,13 +18307,13 @@ snapshots:
metro-runtime@0.80.12:
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.27.6
flow-enums-runtime: 0.0.6
metro-source-map@0.80.12:
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
flow-enums-runtime: 0.0.6
invariant: 2.2.4
metro-symbolicate: 0.80.12
@@ -18360,7 +18352,7 @@ snapshots:
'@babel/core': 7.28.0
'@babel/generator': 7.28.0
'@babel/parser': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
flow-enums-runtime: 0.0.6
metro: 0.80.12
metro-babel-transformer: 0.80.12
@@ -18383,7 +18375,7 @@ snapshots:
'@babel/parser': 7.28.0
'@babel/template': 7.27.2
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
'@babel/types': 7.28.1
accepts: 1.3.8
chalk: 4.1.2
ci-info: 2.0.0
@@ -19010,8 +19002,6 @@ snapshots:
on-headers@1.0.2: {}
on-headers@1.1.0: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -20544,7 +20534,7 @@ snapshots:
source-map@0.6.1: {}
source-map@0.7.6: {}
source-map@0.7.4: {}
space-separated-tokens@2.0.2: {}
@@ -20881,7 +20871,7 @@ snapshots:
terser@5.43.1:
dependencies:
'@jridgewell/source-map': 0.3.11
'@jridgewell/source-map': 0.3.10
acorn: 8.15.0
commander: 2.20.3
source-map-support: 0.5.21
@@ -21308,6 +21298,12 @@ snapshots:
escalade: 3.1.2
picocolors: 1.0.1
update-browserslist-db@1.1.3(browserslist@4.24.3):
dependencies:
browserslist: 4.24.3
escalade: 3.2.0
picocolors: 1.1.1
update-browserslist-db@1.1.3(browserslist@4.25.1):
dependencies:
browserslist: 4.25.1
@@ -21598,7 +21594,7 @@ snapshots:
yaml@2.4.5: {}
yaml@2.8.1: {}
yaml@2.8.0: {}
yamljs@0.3.0:
dependencies:

View File

@@ -291,7 +291,7 @@ class PlexTvAPI extends ExternalAPI {
headers: {
'If-None-Match': cachedWatchlist?.etag,
},
baseURL: 'https://discover.provider.plex.tv',
baseURL: 'https://metadata.provider.plex.tv',
validateStatus: (status) => status < 400, // Allow HTTP 304 to return without error
}
);
@@ -315,7 +315,7 @@ class PlexTvAPI extends ExternalAPI {
const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{
baseURL: 'https://discover.provider.plex.tv',
baseURL: 'https://metadata.provider.plex.tv',
}
);

View File

@@ -1054,7 +1054,7 @@ class TheMovieDb extends ExternalAPI {
keywordId,
}: {
keywordId: number;
}): Promise<TmdbKeyword | null> {
}): Promise<TmdbKeyword> {
try {
const data = await this.get<TmdbKeyword>(
`/keyword/${keywordId}`,
@@ -1064,9 +1064,6 @@ class TheMovieDb extends ExternalAPI {
return data;
} catch (e) {
if (e.response?.status === 404) {
return null;
}
throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`);
}
}

View File

@@ -25,7 +25,6 @@ import imageproxy from '@server/routes/imageproxy';
import { appDataPermissions } from '@server/utils/appDataVolume';
import { getAppVersion } from '@server/utils/appVersion';
import createCustomProxyAgent from '@server/utils/customProxyAgent';
import { initializeDnsCache } from '@server/utils/dnsCache';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
import axios from 'axios';
@@ -81,14 +80,6 @@ app
axios.defaults.httpsAgent = new https.Agent({ family: 4 });
}
// Add DNS caching
if (settings.network.dnsCache) {
initializeDnsCache({
forceMinTtl: settings.network.dnsCache.forceMinTtl,
forceMaxTtl: settings.network.dnsCache.forceMaxTtl,
});
}
// Register HTTP proxy
if (settings.network.proxy.enabled) {
await createCustomProxyAgent(settings.network.proxy);

View File

@@ -61,39 +61,9 @@ export interface CacheItem {
};
}
export interface DNSAddresses {
ipv4: number;
ipv6: number;
}
export interface DNSRecord {
addresses: DNSAddresses;
activeAddress: string;
family: number;
age: number;
ttl: number;
networkErrors: number;
hits: number;
misses: number;
}
export interface DNSStats {
size: number;
maxSize: number;
hits: number;
misses: number;
failures: number;
ipv4Fallbacks: number;
hitRate: number;
}
export interface CacheResponse {
apiCaches: CacheItem[];
imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>;
dnsCache: {
entries: Record<string, DNSRecord>;
stats: DNSStats;
};
}
export interface StatusResponse {

View File

@@ -72,7 +72,6 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
const blacklistedTagsArr = blacklistedTags.split(',');
const pageLimit = settings.main.blacklistedTagsLimit;
const invalidKeywords = new Set<string>();
if (blacklistedTags.length === 0) {
return;
@@ -88,19 +87,6 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
// Iterate for each tag
for (const tag of blacklistedTagsArr) {
const keywordDetails = await tmdb.getKeywordDetails({
keywordId: Number(tag),
});
if (keywordDetails === null) {
logger.warn('Skipping invalid keyword in blacklisted tags', {
label: 'Blacklisted Tags Processor',
keywordId: tag,
});
invalidKeywords.add(tag);
continue;
}
let queryMax = pageLimit * SortOptionsIterable.length;
let fixedSortMode = false; // Set to true when the page limit allows for getting every page of tag
@@ -116,51 +102,24 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
throw new AbortTransaction();
}
try {
const response = await getDiscover({
page,
sortBy,
keywords: tag,
});
const response = await getDiscover({
page,
sortBy,
keywords: tag,
});
await this.processResults(response, tag, type, em);
await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS));
await this.processResults(response, tag, type, em);
await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS));
this.progress++;
if (page === 1 && response.total_pages <= queryMax) {
// We will finish the tag with less queries than expected, move progress accordingly
this.progress += queryMax - response.total_pages;
fixedSortMode = true;
queryMax = response.total_pages;
}
} catch (error) {
logger.error('Error processing keyword in blacklisted tags', {
label: 'Blacklisted Tags Processor',
keywordId: tag,
errorMessage: error.message,
});
this.progress++;
if (page === 1 && response.total_pages <= queryMax) {
// We will finish the tag with less queries than expected, move progress accordingly
this.progress += queryMax - response.total_pages;
fixedSortMode = true;
queryMax = response.total_pages;
}
}
}
}
if (invalidKeywords.size > 0) {
const currentTags = blacklistedTagsArr.filter(
(tag) => !invalidKeywords.has(tag)
);
const cleanedTags = currentTags.join(',');
if (cleanedTags !== blacklistedTags) {
settings.main.blacklistedTags = cleanedTags;
await settings.save();
logger.info('Cleaned up invalid keywords from settings', {
label: 'Blacklisted Tags Processor',
removedKeywords: Array.from(invalidKeywords),
newBlacklistedTags: cleanedTags,
});
}
}
}
private async processResults(

View File

@@ -100,6 +100,17 @@ interface Quota {
quotaDays?: number;
}
export interface ProxySettings {
enabled: boolean;
hostname: string;
port: number;
useSsl: boolean;
user: string;
password: string;
bypassFilter: string;
bypassLocalAddresses: boolean;
}
export interface MainSettings {
apiKey: string;
applicationTitle: string;
@@ -127,29 +138,11 @@ export interface MainSettings {
youtubeUrl: string;
}
export interface ProxySettings {
enabled: boolean;
hostname: string;
port: number;
useSsl: boolean;
user: string;
password: string;
bypassFilter: string;
bypassLocalAddresses: boolean;
}
export interface DnsCacheSettings {
enabled: boolean;
forceMinTtl?: number;
forceMaxTtl?: number;
}
export interface NetworkSettings {
csrfProtection: boolean;
forceIpv4First: boolean;
trustProxy: boolean;
proxy: ProxySettings;
dnsCache: DnsCacheSettings;
}
interface PublicSettings {
@@ -549,11 +542,6 @@ class Settings {
bypassFilter: '',
bypassLocalAddresses: true,
},
dnsCache: {
enabled: false,
forceMinTtl: 0,
forceMaxTtl: -1,
},
},
};
if (initialSettings) {

View File

@@ -128,15 +128,11 @@ discoverRoutes.get('/movies', async (req, res, next) => {
if (keywords) {
const splitKeywords = keywords.split(',');
const keywordResults = await Promise.all(
keywordData = await Promise.all(
splitKeywords.map(async (keywordId) => {
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
})
);
keywordData = keywordResults.filter(
(keyword): keyword is TmdbKeyword => keyword !== null
);
}
return res.status(200).json({
@@ -419,15 +415,11 @@ discoverRoutes.get('/tv', async (req, res, next) => {
if (keywords) {
const splitKeywords = keywords.split(',');
const keywordResults = await Promise.all(
keywordData = await Promise.all(
splitKeywords.map(async (keywordId) => {
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
})
);
keywordData = keywordResults.filter(
(keyword): keyword is TmdbKeyword => keyword !== null
);
}
return res.status(200).json({

View File

@@ -4,40 +4,27 @@ import { Router } from 'express';
const router = Router();
// Delay the initialization of ImageProxy instances until the proxy (if any) is properly configured
let _tmdbImageProxy: ImageProxy;
function initTmdbImageProxy() {
if (!_tmdbImageProxy) {
_tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
}
return _tmdbImageProxy;
}
let _tvdbImageProxy: ImageProxy;
function initTvdbImageProxy() {
if (!_tvdbImageProxy) {
_tvdbImageProxy = new ImageProxy('tvdb', 'https://artworks.thetvdb.com', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
}
return _tvdbImageProxy;
}
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
const tvdbImageProxy = new ImageProxy('tvdb', 'https://artworks.thetvdb.com', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
router.get('/:type/*', async (req, res) => {
const imagePath = req.path.replace(/^\/\w+/, '');
try {
let imageData;
if (req.params.type === 'tmdb') {
imageData = await initTmdbImageProxy().getImage(imagePath);
imageData = await tmdbImageProxy.getImage(imagePath);
} else if (req.params.type === 'tvdb') {
imageData = await initTvdbImageProxy().getImage(imagePath);
imageData = await tvdbImageProxy.getImage(imagePath);
} else {
logger.error('Unsupported image type', {
imagePath,

View File

@@ -197,10 +197,8 @@ mediaRoutes.delete(
const media = await mediaRepository.findOneOrFail({
where: { id: Number(req.params.id) },
});
const is4k = req.query.is4k === 'true';
const is4k = media.serviceUrl4k !== undefined;
const isMovie = media.mediaType === MediaType.MOVIE;
let serviceSettings;
if (isMovie) {
serviceSettings = settings.radarr.find(
@@ -227,7 +225,6 @@ mediaRoutes.delete(
);
}
}
if (!serviceSettings) {
logger.warn(
`There is no default ${
@@ -242,7 +239,6 @@ mediaRoutes.delete(
);
return;
}
let service;
if (isMovie) {
service = new RadarrAPI({

View File

@@ -28,7 +28,6 @@ import discoverSettingRoutes from '@server/routes/settings/discover';
import { ApiError } from '@server/types/error';
import { appDataPath } from '@server/utils/appDataVolume';
import { getAppVersion } from '@server/utils/appVersion';
import dnsCache from '@server/utils/dnsCache';
import { getHostname } from '@server/utils/getHostname';
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
@@ -756,19 +755,12 @@ settingsRoutes.get('/cache', async (_req, res) => {
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
const avatarImageCache = await ImageProxy.getImageStats('avatar');
const stats = dnsCache?.getStats();
const entries = dnsCache?.getCacheEntries();
return res.status(200).json({
apiCaches,
imageCache: {
tmdb: tmdbImageCache,
avatar: avatarImageCache,
},
dnsCache: {
stats,
entries,
},
});
});
@@ -786,20 +778,6 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
}
);
settingsRoutes.post<{ dnsEntry: string }>(
'/cache/dns/:dnsEntry/flush',
(req, res, next) => {
const dnsEntry = req.params.dnsEntry;
if (dnsCache) {
dnsCache.clear(dnsEntry);
return res.status(204).send();
}
next({ status: 404, message: 'Cache not found.' });
}
);
settingsRoutes.post(
'/initialize',
isAuthenticated(Permission.ADMIN),

View File

@@ -33,93 +33,52 @@ import { EventSubscriber } from 'typeorm';
export class MediaRequestSubscriber
implements EntitySubscriberInterface<MediaRequest>
{
private async notifyAvailableMovie(
entity: MediaRequest,
event?: UpdateEvent<MediaRequest>
) {
// Get fresh media state using event manager
let latestMedia: Media | null = null;
if (event?.manager) {
latestMedia = await event.manager.findOne(Media, {
where: { id: entity.media.id },
});
}
if (!latestMedia) {
const mediaRepository = getRepository(Media);
latestMedia = await mediaRepository.findOne({
where: { id: entity.media.id },
});
}
// Check availability using fresh media state
private async notifyAvailableMovie(entity: MediaRequest) {
if (
!latestMedia ||
latestMedia[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE
entity.media[entity.is4k ? 'status4k' : 'status'] ===
MediaStatus.AVAILABLE
) {
return;
}
const tmdb = new TheMovieDb();
const tmdb = new TheMovieDb();
try {
const movie = await tmdb.getMovie({
movieId: entity.media.tmdbId,
});
try {
const movie = await tmdb.getMovie({
movieId: entity.media.tmdbId,
});
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${entity.is4k ? '4K ' : ''}Movie Request Now Available`,
notifyAdmin: false,
notifySystem: true,
notifyUser: entity.requestedBy,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: latestMedia,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: entity,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${entity.is4k ? '4K ' : ''}Movie Request Now Available`,
notifyAdmin: false,
notifySystem: true,
notifyUser: entity.requestedBy,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: entity.media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: entity,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
}
}
private async notifyAvailableSeries(
entity: MediaRequest,
event?: UpdateEvent<MediaRequest>
) {
// Get fresh media state with seasons using event manager
let latestMedia: Media | null = null;
if (event?.manager) {
latestMedia = await event.manager.findOne(Media, {
where: { id: entity.media.id },
relations: { seasons: true },
});
}
if (!latestMedia) {
const mediaRepository = getRepository(Media);
latestMedia = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { seasons: true },
});
}
if (!latestMedia) {
return;
}
// Check availability using fresh media state
private async notifyAvailableSeries(entity: MediaRequest) {
// Find all seasons in the related media entity
// and see if they are available, then we can check
// if the request contains the same seasons
const requestedSeasons =
entity.seasons?.map((entitySeason) => entitySeason.seasonNumber) ?? [];
const availableSeasons = latestMedia.seasons.filter(
const availableSeasons = entity.media.seasons.filter(
(season) =>
season[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE &&
requestedSeasons.includes(season.seasonNumber)
@@ -128,46 +87,44 @@ export class MediaRequestSubscriber
availableSeasons.length > 0 &&
availableSeasons.length === requestedSeasons.length;
if (!isMediaAvailable) {
return;
}
if (isMediaAvailable) {
const tmdb = new TheMovieDb();
const tmdb = new TheMovieDb();
try {
const tv = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
try {
const tv = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${entity.is4k ? '4K ' : ''}Series Request Now Available`,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
notifyAdmin: false,
notifySystem: true,
notifyUser: entity.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media: latestMedia,
extra: [
{
name: 'Requested Seasons',
value: entity.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: entity,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${entity.is4k ? '4K ' : ''}Series Request Now Available`,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
notifyAdmin: false,
notifySystem: true,
notifyUser: entity.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media: entity.media,
extra: [
{
name: 'Requested Seasons',
value: entity.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: entity,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
}
}
@@ -825,10 +782,10 @@ export class MediaRequestSubscriber
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
this.notifyAvailableMovie(event.entity as MediaRequest, event);
this.notifyAvailableMovie(event.entity as MediaRequest);
}
if (event.entity.media.mediaType === MediaType.TV) {
this.notifyAvailableSeries(event.entity as MediaRequest, event);
this.notifyAvailableSeries(event.entity as MediaRequest);
}
}
}

View File

@@ -1,28 +0,0 @@
import logger from '@server/logger';
import { DnsCacheManager } from 'dns-caching';
let dnsCache: DnsCacheManager | undefined;
export function initializeDnsCache({
forceMinTtl,
forceMaxTtl,
}: {
forceMinTtl?: number;
forceMaxTtl?: number;
}) {
if (dnsCache) {
logger.warn('DNS Cache is already initialized', { label: 'DNS Cache' });
return;
}
logger.info('Initializing DNS Cache', { label: 'DNS Cache' });
dnsCache = new DnsCacheManager({
logger,
forceMinTtl: typeof forceMinTtl === 'number' ? forceMinTtl * 1000 : 0,
forceMaxTtl: typeof forceMaxTtl === 'number' ? forceMaxTtl * 1000 : -1,
});
dnsCache.initialize();
}
export default dnsCache;

View File

@@ -29,10 +29,14 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
const keywordIds = data.blacklistedTags.slice(1, -1).split(',');
Promise.all(
keywordIds.map(async (keywordId) => {
const { data } = await axios.get<Keyword | null>(
`/api/v1/keyword/${keywordId}`
);
return data?.name || `[Invalid: ${keywordId}]`;
try {
const { data } = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return data.name;
} catch (err) {
return '';
}
})
).then((keywords) => {
setTagNamesBlacklistedFor(keywords.join(', '));

View File

@@ -5,10 +5,7 @@ import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { ArrowDownIcon } from '@heroicons/react/24/solid';
import type {
TmdbKeyword,
TmdbKeywordSearchResponse,
} from '@server/api/themoviedb/interfaces';
import type { TmdbKeywordSearchResponse } from '@server/api/themoviedb/interfaces';
import type { Keyword } from '@server/models/common';
import axios from 'axios';
import { useFormikContext } from 'formik';
@@ -127,19 +124,15 @@ const ControlledKeywordSelector = ({
const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => {
const { data } = await axios.get<Keyword | null>(
const { data } = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return data;
})
);
const validKeywords: TmdbKeyword[] = keywords.filter(
(keyword): keyword is TmdbKeyword => keyword !== null
);
onChange(
validKeywords.map((keyword) => ({
keywords.map((keyword) => ({
label: keyword.name,
value: keyword.id,
}))

View File

@@ -77,19 +77,16 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
const keywords = await Promise.all(
slider.data.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword | null>(
const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return keyword.data;
})
);
const validKeywords: Keyword[] = keywords.filter(
(keyword): keyword is Keyword => keyword !== null
);
setDefaultDataValue(
validKeywords.map((keyword) => ({
keywords.map((keyword) => ({
label: keyword.name,
value: keyword.id,
}))

View File

@@ -118,11 +118,9 @@ const ManageSlideOver = ({
}
};
const deleteMediaFile = async (is4k = false) => {
const deleteMediaFile = async () => {
if (data.mediaInfo) {
await axios.delete(
`/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}`
);
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate();
onClose();
@@ -416,7 +414,7 @@ const ManageSlideOver = ({
isDefaultService() && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile(false)}
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
@@ -575,7 +573,7 @@ const ManageSlideOver = ({
{isDefaultService() && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile(true)}
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}

View File

@@ -343,9 +343,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const deleteMediaFile = async () => {
if (request.media) {
await axios.delete(
`/api/v1/media/${request.media.id}/file?is4k=${request.is4k}`
);
await axios.delete(`/api/v1/media/${request.media.id}/file`);
await axios.delete(`/api/v1/media/${request.media.id}`);
revalidateList();
}

View File

@@ -309,19 +309,16 @@ export const KeywordSelector = ({
const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword | null>(
const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return keyword.data;
})
);
const validKeywords: Keyword[] = keywords.filter(
(keyword): keyword is Keyword => keyword !== null
);
setDefaultDataValue(
validKeywords.map((keyword) => ({
keywords.map((keyword) => ({
label: keyword.name,
value: keyword.id,
}))

View File

@@ -113,16 +113,12 @@ const OverrideRuleTiles = ({
.flat()
.filter((keywordId) => keywordId)
.map(async (keywordId) => {
const response = await axios.get<Keyword | null>(
`/api/v1/keyword/${keywordId}`
);
return response.data;
const response = await axios.get(`/api/v1/keyword/${keywordId}`);
const keyword: Keyword = response.data;
return keyword;
})
);
const validKeywords: Keyword[] = keywords.filter(
(keyword): keyword is Keyword => keyword !== null
);
setKeywords(validKeywords);
setKeywords(keywords);
const allUsersFromRules = rules
.map((rule) => rule.users)
.filter((users) => users)

View File

@@ -22,7 +22,6 @@ import type {
import type { JobId } from '@server/lib/settings';
import axios from 'axios';
import cronstrue from 'cronstrue/i18n';
import { formatDuration, intervalToDuration } from 'date-fns';
import { Fragment, useReducer, useState } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { FormattedRelativeTime, useIntl } from 'react-intl';
@@ -56,26 +55,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
cacheksize: 'Key Size',
cachevsize: 'Value Size',
flushcache: 'Flush Cache',
dnsCache: 'DNS Cache',
dnsCacheDescription:
'Jellyseerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.',
dnscacheflushed: '{hostname} dns cache flushed.',
dnscachename: 'Hostname',
dnscacheactiveaddress: 'Active Address',
dnscachehits: 'Hits',
dnscachemisses: 'Misses',
dnscacheage: 'Age',
dnscachenetworkerrors: 'Network Errors',
flushdnscache: 'Flush DNS Cache',
dnsCacheGlobalStats: 'Global DNS Cache Stats',
dnsCacheGlobalStatsDescription:
'These stats are aggregated across all DNS cache entries.',
size: 'Size',
hits: 'Hits',
misses: 'Misses',
failures: 'Failures',
ipv4Fallbacks: 'IPv4 Fallbacks',
hitRate: 'Hit Rate',
unknownJob: 'Unknown Job',
'plex-recently-added-scan': 'Plex Recently Added Scan',
'plex-full-scan': 'Plex Full Library Scan',
@@ -263,18 +242,6 @@ const SettingsJobs = () => {
cacheRevalidate();
};
const flushDnsCache = async (hostname: string) => {
await axios.post(`/api/v1/settings/cache/dns/${hostname}/flush`);
addToast(
intl.formatMessage(messages.dnscacheflushed, { hostname: hostname }),
{
appearance: 'success',
autoDismiss: true,
}
);
cacheRevalidate();
};
const scheduleJob = async () => {
const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
@@ -318,18 +285,6 @@ const SettingsJobs = () => {
}
};
const formatAge = (milliseconds: number): string => {
const duration = intervalToDuration({
start: 0,
end: milliseconds,
});
return formatDuration(duration, {
format: ['minutes', 'seconds'],
zero: false,
});
};
return (
<>
<PageTitle
@@ -612,95 +567,6 @@ const SettingsJobs = () => {
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.dnscachename)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscacheactiveaddress)}
</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachehits)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachemisses)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscacheage)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscachenetworkerrors)}
</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{Object.entries(cacheData?.dnsCache.entries || {}).map(
([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD>{intl.formatNumber(data.networkErrors)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>{intl.formatMessage(messages.flushdnscache)}</span>
</Button>
</Table.TD>
</tr>
)
)}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
</Table.TH>
))}
</tr>
</thead>
<Table.TBody>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName, statValue]) => (
<Table.TD key={`dns-stat-${statName}`}>
{statName === 'hitRate'
? intl.formatNumber(statValue, {
style: 'percent',
maximumFractionDigits: 2,
})
: intl.formatNumber(statValue)}
</Table.TD>
))}
</tr>
</Table.TBody>
</Table>
</div>
<div className="break-words">
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
<p className="description">

View File

@@ -45,13 +45,6 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
forceIpv4First: 'Force IPv4 Resolution First',
forceIpv4FirstTip:
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
dnsCache: 'DNS Cache',
dnsCacheTip:
'Enable caching of DNS lookups to optimize performance and avoid making unnecessary API calls',
dnsCacheHoverTip:
'Do NOT enable this if you are experiencing issues with DNS lookups',
dnsCacheForceMinTtl: 'DNS Cache Minimum TTL',
dnsCacheForceMaxTtl: 'DNS Cache Maximum TTL',
});
const SettingsNetwork = () => {
@@ -97,9 +90,6 @@ const SettingsNetwork = () => {
initialValues={{
csrfProtection: data?.csrfProtection,
forceIpv4First: data?.forceIpv4First,
dnsCacheEnabled: data?.dnsCache.enabled,
dnsCacheForceMinTtl: data?.dnsCache.forceMinTtl,
dnsCacheForceMaxTtl: data?.dnsCache.forceMaxTtl,
trustProxy: data?.trustProxy,
proxyEnabled: data?.proxy?.enabled,
proxyHostname: data?.proxy?.hostname,
@@ -118,11 +108,6 @@ const SettingsNetwork = () => {
csrfProtection: values.csrfProtection,
forceIpv4First: values.forceIpv4First,
trustProxy: values.trustProxy,
dnsCache: {
enabled: values.dnsCacheEnabled,
forceMinTtl: values.dnsCacheForceMinTtl,
forceMaxTtl: values.dnsCacheForceMaxTtl,
},
proxy: {
enabled: values.proxyEnabled,
hostname: values.proxyHostname,
@@ -236,90 +221,6 @@ const SettingsNetwork = () => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="dnsCacheEnabled" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.dnsCache)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<SettingsBadge badgeType="experimental" className="mr-2" />
<span className="label-tip">
{intl.formatMessage(messages.dnsCacheTip)}
</span>
</label>
<div className="form-input-area">
<Tooltip
content={intl.formatMessage(messages.dnsCacheHoverTip)}
>
<Field
type="checkbox"
id="dnsCacheEnabled"
name="dnsCacheEnabled"
onChange={() => {
setFieldValue(
'dnsCacheEnabled',
!values.dnsCacheEnabled
);
}}
/>
</Tooltip>
</div>
</div>
{values.dnsCacheEnabled && (
<>
<div className="mr-2 ml-4">
<div className="form-row">
<label
htmlFor="dnsCacheForceMinTtl"
className="checkbox-label"
>
{intl.formatMessage(messages.dnsCacheForceMinTtl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl"
type="text"
/>
</div>
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="dnsCacheForceMaxTtl"
className="checkbox-label"
>
{intl.formatMessage(messages.dnsCacheForceMaxTtl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl"
type="text"
/>
</div>
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
</div>
</div>
</div>
</>
)}
<div className="form-row">
<label htmlFor="proxyEnabled" className="checkbox-label">
<span className="mr-2">

View File

@@ -873,17 +873,6 @@
"components.Settings.SettingsJobsCache.cachevsize": "Value Size",
"components.Settings.SettingsJobsCache.canceljob": "Cancel Job",
"components.Settings.SettingsJobsCache.command": "Command",
"components.Settings.SettingsJobsCache.dnsCache": "DNS Cache",
"components.Settings.SettingsJobsCache.dnsCacheDescription": "Jellyseerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Global DNS Cache Stats",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "These stats are aggregated across all DNS cache entries.",
"components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Active Address",
"components.Settings.SettingsJobsCache.dnscacheage": "Age",
"components.Settings.SettingsJobsCache.dnscacheflushed": "{hostname} dns cache flushed.",
"components.Settings.SettingsJobsCache.dnscachehits": "Hits",
"components.Settings.SettingsJobsCache.dnscachemisses": "Misses",
"components.Settings.SettingsJobsCache.dnscachename": "Hostname",
"components.Settings.SettingsJobsCache.dnscachenetworkerrors": "Network Errors",
"components.Settings.SettingsJobsCache.download-sync": "Download Sync",
"components.Settings.SettingsJobsCache.download-sync-reset": "Download Sync Reset",
"components.Settings.SettingsJobsCache.editJobSchedule": "Modify Job",
@@ -893,17 +882,12 @@
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
"components.Settings.SettingsJobsCache.failures": "Failures",
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
"components.Settings.SettingsJobsCache.flushdnscache": "Flush DNS Cache",
"components.Settings.SettingsJobsCache.hitRate": "Hit Rate",
"components.Settings.SettingsJobsCache.hits": "Hits",
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
"components.Settings.SettingsJobsCache.imagecache": "Image Cache",
"components.Settings.SettingsJobsCache.imagecacheDescription": "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>.",
"components.Settings.SettingsJobsCache.imagecachecount": "Images Cached",
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
"components.Settings.SettingsJobsCache.ipv4Fallbacks": "IPv4 Fallbacks",
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Full Library Scan",
"components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Jellyfin Recently Added Scan",
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
@@ -915,7 +899,6 @@
"components.Settings.SettingsJobsCache.jobsandcache": "Jobs & Cache",
"components.Settings.SettingsJobsCache.jobstarted": "{jobname} started.",
"components.Settings.SettingsJobsCache.jobtype": "Type",
"components.Settings.SettingsJobsCache.misses": "Misses",
"components.Settings.SettingsJobsCache.nextexecution": "Next Execution",
"components.Settings.SettingsJobsCache.plex-full-scan": "Plex Full Library Scan",
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex Recently Added Scan",
@@ -925,7 +908,6 @@
"components.Settings.SettingsJobsCache.process-blacklisted-tags": "Process Blacklisted Tags",
"components.Settings.SettingsJobsCache.radarr-scan": "Radarr Scan",
"components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SettingsJobsCache.size": "Size",
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
"components.Settings.SettingsJobsCache.usersavatars": "Users' Avatars",
@@ -987,9 +969,6 @@
"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.dnsCache": "DNS Cache",
"components.Settings.SettingsNetwork.dnsCacheHoverTip": "Do NOT enable this if you are experiencing issues with DNS lookups",
"components.Settings.SettingsNetwork.dnsCacheTip": "Enable caching of DNS lookups to optimize performance and avoid making unnecessary API calls",
"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",