diff --git a/.prettierignore b/.prettierignore index c2e778c1d..736c402cb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ dist/ config/ CHANGELOG.md pnpm-lock.yaml +cypress/config/settings.cypress.json # assets src/assets/ diff --git a/.prettierrc.js b/.prettierrc.js index 10da08eb1..25b39dd3b 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -21,5 +21,11 @@ module.exports = { rangeEnd: 0, // default: Infinity }, }, + { + files: 'cypress/config/settings.cypress.json', + options: { + rangeEnd: 0, + }, + }, ], }; diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index d10a107fd..e30dde861 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -6,7 +6,6 @@ "apiKey": "testkey", "applicationTitle": "Jellyseerr", "applicationUrl": "", - "csrfProtection": false, "cacheImages": false, "defaultPermissions": 32, "defaultQuotas": { @@ -180,5 +179,26 @@ "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 + } } } diff --git a/docs/using-jellyseerr/settings/dns-caching.md b/docs/using-jellyseerr/settings/dns-caching.md new file mode 100644 index 000000000..ad4e9597c --- /dev/null +++ b/docs/using-jellyseerr/settings/dns-caching.md @@ -0,0 +1,16 @@ +--- +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). diff --git a/docs/using-jellyseerr/settings/jobs&cache.md b/docs/using-jellyseerr/settings/jobs&cache.md index b68e6d911..ef8ca4871 100644 --- a/docs/using-jellyseerr/settings/jobs&cache.md +++ b/docs/using-jellyseerr/settings/jobs&cache.md @@ -1,6 +1,7 @@ --- title: Jobs & Cache description: Configure jobs and cache settings. +sidebar_position: 6 --- # Jobs & Cache diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 60f16e51c..4254ba43e 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -260,9 +260,51 @@ components: csrfProtection: type: boolean example: false + forceIpv4First: + type: boolean + example: false trustProxy: type: boolean - example: true + 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 PlexLibrary: type: object properties: @@ -2967,6 +3009,68 @@ 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: @@ -3006,6 +3110,21 @@ 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 diff --git a/package.json b/package.json index 52ed3feb5..b47b8f2dc 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07051e1a6..4012b0350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ 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) @@ -688,8 +691,8 @@ packages: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.2': + resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.24.7': @@ -1455,8 +1458,8 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} '@babel/template@7.24.7': @@ -1483,8 +1486,8 @@ packages: resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.1': - resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} '@codedependant/semantic-release-docker@5.1.0': @@ -1975,8 +1978,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.12': - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} @@ -1990,20 +1993,20 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.10': - resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -3229,8 +3232,8 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@swc/types@0.1.23': - resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@swc/types@0.1.24': + resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} '@tailwindcss/aspect-ratio@0.4.2': resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==} @@ -3391,8 +3394,8 @@ packages: '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@18.19.118': - resolution: {integrity: sha512-hIPK0hSrrcaoAu/gJMzN3QClXE4QdCdFvaenJ0JsjIbExP1JFFVH+RHcBt25c9n8bx5dkIfqKE+uw6BmBns7ug==} + '@types/node@18.19.122': + resolution: {integrity: sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==} '@types/node@20.5.1': resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} @@ -4087,11 +4090,6 @@ 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} @@ -4196,8 +4194,8 @@ packages: caniuse-lite@1.0.30001700: resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001734: + resolution: {integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==} caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -4426,8 +4424,8 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - compression@1.8.0: - resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==} + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} computed-style@0.1.4: @@ -4547,8 +4545,8 @@ packages: core-js-compat@3.37.1: resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} - core-js-compat@3.44.0: - resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} + core-js-compat@3.45.0: + resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==} core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -4866,6 +4864,9 @@ 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'} @@ -4937,8 +4938,8 @@ packages: electron-to-chromium@1.4.810: resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==} - electron-to-chromium@1.5.182: - resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==} + electron-to-chromium@1.5.200: + resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} email-templates@12.0.1: resolution: {integrity: sha512-849pjBFVUAWWTa3HqhDjxlXHaSWmxf4CZOlZ9iVkrSAbQ8YCYi+7KiKqt35L6F20WhSViWX7lmMjno6zBv2rNQ==} @@ -5506,8 +5507,8 @@ packages: flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - flow-parser@0.275.0: - resolution: {integrity: sha512-fHNwawoA2LM7FsxhU/1lTRGq9n6/Q8k861eHgN7GKtamYt9Qrxpg/ZSrev8o1WX7fQ2D3Gg3+uvYN15PmsG7Yw==} + flow-parser@0.278.0: + resolution: {integrity: sha512-9oUcYDHf9n+E/t0FXndgBqGbaUsGEcmWqIr1ldqCzTzctsJV5E/bHusOj4ThB72Ss2mqWpLFNz0+o2c1O8J6+A==} engines: {node: '>=0.4.0'} fn.name@1.1.0: @@ -6744,6 +6745,10 @@ 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==} @@ -7534,6 +7539,10 @@ 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==} @@ -8838,9 +8847,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -9893,8 +9902,8 @@ packages: engines: {node: '>= 14'} hasBin: true - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} hasBin: true @@ -10038,11 +10047,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.27.6 + '@babel/helpers': 7.28.2 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -10061,9 +10070,9 @@ snapshots: '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.24.7': @@ -10072,7 +10081,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: @@ -10093,7 +10102,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.24.3 + browserslist: 4.25.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -10199,7 +10208,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10213,7 +10222,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10252,7 +10261,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/helper-plugin-utils@7.24.7': {} @@ -10320,7 +10329,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10357,7 +10366,7 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -10366,10 +10375,10 @@ snapshots: '@babel/template': 7.24.7 '@babel/types': 7.24.7 - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.2': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/highlight@7.24.7': dependencies: @@ -10388,7 +10397,7 @@ snapshots: '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': dependencies: @@ -11080,7 +11089,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.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -11374,7 +11383,7 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} '@babel/template@7.24.7': dependencies: @@ -11386,7 +11395,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/traverse@7.24.7': dependencies: @@ -11410,7 +11419,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11426,7 +11435,7 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.28.1': + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -12091,10 +12100,10 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.12': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/gen-mapping@0.3.5': dependencies: @@ -12106,24 +12115,24 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.10': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping@0.3.29': + '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.9': dependencies: @@ -12871,7 +12880,7 @@ snapshots: semver: 7.7.1 strip-ansi: 5.2.0 wcwidth: 1.0.1 - yaml: 2.8.0 + yaml: 2.8.1 transitivePeerDependencies: - encoding @@ -12916,7 +12925,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.0 + compression: 1.8.1 connect: 3.7.0 errorhandler: 1.5.1 nocache: 3.0.4 @@ -13392,7 +13401,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.27.6 + '@babel/runtime': 7.28.2 '@types/react-reconciler': 0.26.7 '@types/webxr': 0.5.22 base64-js: 1.5.1 @@ -13547,7 +13556,7 @@ snapshots: '@rnx-kit/chromium-edge-launcher@1.0.0': dependencies: - '@types/node': 18.19.118 + '@types/node': 18.19.122 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -13810,7 +13819,7 @@ snapshots: '@swc/core@1.6.5(@swc/helpers@0.5.11)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.23 + '@swc/types': 0.1.24 optionalDependencies: '@swc/core-darwin-arm64': 1.6.5 '@swc/core-darwin-x64': 1.6.5 @@ -13835,7 +13844,7 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 - '@swc/types@0.1.23': + '@swc/types@0.1.24': dependencies: '@swc/counter': 0.1.3 @@ -14013,7 +14022,7 @@ snapshots: '@types/node@17.0.45': {} - '@types/node@18.19.118': + '@types/node@18.19.122': dependencies: undici-types: 5.26.5 @@ -14689,7 +14698,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.44.0 + core-js-compat: 3.45.0 transitivePeerDependencies: - supports-color @@ -14804,17 +14813,10 @@ 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.30001727 - electron-to-chromium: 1.5.182 + caniuse-lite: 1.0.30001734 + electron-to-chromium: 1.5.200 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -14945,7 +14947,7 @@ snapshots: caniuse-lite@1.0.30001700: {} - caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001734: {} caseless@0.12.0: {} @@ -15195,13 +15197,13 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.0: + compression@1.8.1: dependencies: bytes: 3.1.2 compressible: 2.0.18 debug: 2.6.9 negotiator: 0.6.4 - on-headers: 1.0.2 + on-headers: 1.1.0 safe-buffer: 5.2.1 vary: 1.1.2 transitivePeerDependencies: @@ -15332,7 +15334,7 @@ snapshots: dependencies: browserslist: 4.23.1 - core-js-compat@3.44.0: + core-js-compat@3.45.0: dependencies: browserslist: 4.25.1 @@ -15698,6 +15700,10 @@ 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 @@ -15782,7 +15788,7 @@ snapshots: electron-to-chromium@1.4.810: {} - electron-to-chromium@1.5.182: {} + electron-to-chromium@1.5.200: {} 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: @@ -16702,7 +16708,7 @@ snapshots: flow-enums-runtime@0.0.6: {} - flow-parser@0.275.0: {} + flow-parser@0.278.0: {} fn.name@1.1.0: {} @@ -17092,7 +17098,7 @@ snapshots: hermes-profile-transformer@0.0.6: dependencies: - source-map: 0.7.4 + source-map: 0.7.6 highlight.js@10.7.3: {} @@ -17685,7 +17691,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.275.0 + flow-parser: 0.278.0 graceful-fs: 4.2.11 micromatch: 4.0.8 neo-async: 2.6.2 @@ -18036,6 +18042,8 @@ snapshots: lru-cache@10.2.2: {} + lru-cache@11.1.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -18307,13 +18315,13 @@ snapshots: metro-runtime@0.80.12: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 flow-enums-runtime: 0.0.6 metro-source-map@0.80.12: dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.80.12 @@ -18352,7 +18360,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 metro: 0.80.12 metro-babel-transformer: 0.80.12 @@ -18375,7 +18383,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -19002,6 +19010,8 @@ snapshots: on-headers@1.0.2: {} + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -20534,7 +20544,7 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.4: {} + source-map@0.7.6: {} space-separated-tokens@2.0.2: {} @@ -20871,7 +20881,7 @@ snapshots: terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.10 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -21298,12 +21308,6 @@ 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 @@ -21594,7 +21598,7 @@ snapshots: yaml@2.4.5: {} - yaml@2.8.0: {} + yaml@2.8.1: {} yamljs@0.3.0: dependencies: diff --git a/server/index.ts b/server/index.ts index e4d46b8a1..24b7f2250 100644 --- a/server/index.ts +++ b/server/index.ts @@ -25,6 +25,7 @@ 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'; @@ -80,6 +81,14 @@ 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); diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 9393370dc..3fee84be8 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -61,9 +61,39 @@ 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; + stats: DNSStats; + }; } export interface StatusResponse { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 115f224f8..52272c74d 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -100,17 +100,6 @@ 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; @@ -138,11 +127,29 @@ 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 { @@ -542,6 +549,11 @@ class Settings { bypassFilter: '', bypassLocalAddresses: true, }, + dnsCache: { + enabled: false, + forceMinTtl: 0, + forceMaxTtl: -1, + }, }, }; if (initialSettings) { diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index d74d328f3..db31b937e 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -28,6 +28,7 @@ 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'; @@ -755,12 +756,19 @@ 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, + }, }); }); @@ -778,6 +786,20 @@ 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), diff --git a/server/utils/dnsCache.ts b/server/utils/dnsCache.ts new file mode 100644 index 000000000..253b9291a --- /dev/null +++ b/server/utils/dnsCache.ts @@ -0,0 +1,28 @@ +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; diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 0f951293a..5a6ebefea 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -22,6 +22,7 @@ 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'; @@ -55,6 +56,26 @@ 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', @@ -242,6 +263,18 @@ 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', '*', '*', '*', '*']; @@ -285,6 +318,18 @@ const SettingsJobs = () => { } }; + const formatAge = (milliseconds: number): string => { + const duration = intervalToDuration({ + start: 0, + end: milliseconds, + }); + + return formatDuration(duration, { + format: ['minutes', 'seconds'], + zero: false, + }); + }; + return ( <> { +
+

{intl.formatMessage(messages.dnsCache)}

+

+ {intl.formatMessage(messages.dnsCacheDescription)} +

+
+
+ + + + {intl.formatMessage(messages.dnscachename)} + + {intl.formatMessage(messages.dnscacheactiveaddress)} + + {intl.formatMessage(messages.dnscachehits)} + {intl.formatMessage(messages.dnscachemisses)} + {intl.formatMessage(messages.dnscacheage)} + + {intl.formatMessage(messages.dnscachenetworkerrors)} + + + + + + {Object.entries(cacheData?.dnsCache.entries || {}).map( + ([hostname, data]) => ( + + {hostname} + {data.activeAddress} + {intl.formatNumber(data.hits)} + {intl.formatNumber(data.misses)} + {formatAge(data.age)} + {intl.formatNumber(data.networkErrors)} + + + + + ) + )} + +
+
+
+

+ {intl.formatMessage(messages.dnsCacheGlobalStats)} +

+

+ {intl.formatMessage(messages.dnsCacheGlobalStatsDescription)} +

+
+
+ + + + {Object.entries(cacheData?.dnsCache.stats || {}) + .filter(([statName]) => statName !== 'maxSize') + .map(([statName]) => ( + + {messages[statName] + ? intl.formatMessage(messages[statName]) + : statName} + + ))} + + + + + {Object.entries(cacheData?.dnsCache.stats || {}) + .filter(([statName]) => statName !== 'maxSize') + .map(([statName, statValue]) => ( + + {statName === 'hitRate' + ? intl.formatNumber(statValue, { + style: 'percent', + maximumFractionDigits: 2, + }) + : intl.formatNumber(statValue)} + + ))} + + +
+

{intl.formatMessage(messages.imagecache)}

diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx index e02b7bfc6..f0ac1f13a 100644 --- a/src/components/Settings/SettingsNetwork/index.tsx +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -45,6 +45,13 @@ 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 = () => { @@ -90,6 +97,9 @@ 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, @@ -108,6 +118,11 @@ 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, @@ -221,6 +236,90 @@ const SettingsNetwork = () => { />

+
+ +
+ + { + setFieldValue( + 'dnsCacheEnabled', + !values.dnsCacheEnabled + ); + }} + /> + +
+
+ {values.dnsCacheEnabled && ( + <> +
+
+ +
+
+ +
+ {errors.dnsCacheForceMinTtl && + touched.dnsCacheForceMinTtl && + typeof errors.dnsCacheForceMinTtl === 'string' && ( +
+ {errors.dnsCacheForceMinTtl} +
+ )} +
+
+
+ +
+
+ +
+ {errors.dnsCacheForceMaxTtl && + touched.dnsCacheForceMaxTtl && + typeof errors.dnsCacheForceMaxTtl === 'string' && ( +
+ {errors.dnsCacheForceMaxTtl} +
+ )} +
+
+
+ + )}