From 5bb2d75fe1582a30c8f183afd8096f58421bfb83 Mon Sep 17 00:00:00 2001
From: sct
Date: Sat, 30 Jan 2021 06:09:13 +0000
Subject: [PATCH 01/66] build(deps): bump dependencies
---
package.json | 18 ++--
yarn.lock | 283 ++++++++++++++++++++++++++-------------------------
2 files changed, 154 insertions(+), 147 deletions(-)
diff --git a/package.json b/package.json
index b696336c..1f9acfe9 100644
--- a/package.json
+++ b/package.json
@@ -29,14 +29,14 @@
"csurf": "^1.11.0",
"email-templates": "^8.0.3",
"express": "^4.17.1",
- "express-openapi-validator": "^4.10.8",
+ "express-openapi-validator": "^4.10.9",
"express-session": "^1.17.1",
"formik": "^2.2.6",
"gravatar-url": "^3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "10.0.3",
- "node-schedule": "^1.3.2",
+ "node-schedule": "^1.3.3",
"nodemailer": "^6.4.17",
"nookies": "^2.5.2",
"plex-api": "^5.3.1",
@@ -46,7 +46,7 @@
"react-animate-height": "^2.0.23",
"react-dom": "17.0.1",
"react-intersection-observer": "^8.31.0",
- "react-intl": "^5.10.16",
+ "react-intl": "^5.11.0",
"react-markdown": "^5.0.3",
"react-spring": "^8.0.27",
"react-toast-notifications": "^2.4.0",
@@ -57,7 +57,7 @@
"secure-random-password": "^0.2.2",
"sqlite3": "^5.0.0",
"swagger-ui-express": "^4.1.6",
- "swr": "^0.4.0",
+ "swr": "^0.4.1",
"typeorm": "^0.2.30",
"uuid": "^8.3.2",
"winston": "^3.3.3",
@@ -81,7 +81,7 @@
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/csurf": "^1.11.0",
- "@types/email-templates": "^8.0.0",
+ "@types/email-templates": "^8.0.1",
"@types/express": "^4.17.11",
"@types/express-session": "^1.17.0",
"@types/lodash": "^4.14.168",
@@ -98,8 +98,8 @@
"@types/xml2js": "^0.4.8",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.11",
- "@typescript-eslint/eslint-plugin": "^4.14.0",
- "@typescript-eslint/parser": "^4.14.0",
+ "@typescript-eslint/eslint-plugin": "^4.14.1",
+ "@typescript-eslint/parser": "^4.14.1",
"autoprefixer": "^9",
"babel-plugin-react-intl": "^8.2.25",
"babel-plugin-react-intl-auto": "^3.3.0",
@@ -108,7 +108,7 @@
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.2.0",
- "eslint-plugin-formatjs": "^2.10.3",
+ "eslint-plugin-formatjs": "^2.11.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
@@ -120,7 +120,7 @@
"postcss": "^7",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1",
- "semantic-release": "^17.3.6",
+ "semantic-release": "^17.3.7",
"semantic-release-docker": "^2.2.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"ts-node": "^9.1.1",
diff --git a/yarn.lock b/yarn.lock
index 3254f9e5..d269381c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1358,10 +1358,10 @@
dependencies:
tslib "^2.0.1"
-"@formatjs/ecma402-abstract@1.5.1":
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.1.tgz#629324d2bfdc570ed210fec7700ce20bbd872bed"
- integrity sha512-io9XhgIpEbc6jSdn4QVnJeFaUzy6gS5fGiIRCUJ7QKqCNp69JS8EJPW8gCtvwz+JQtx2SJvhaMJbzz3rGkTXBA==
+"@formatjs/ecma402-abstract@1.5.2":
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.2.tgz#6c20c24f814ebf8e9dd46e34310a67895853a931"
+ integrity sha512-rscxoLyIwH2x+l15Z4eD580ioO3CkFVoWDLgDtgiOnWzDzpL5EigDRg9V4mINb8W6bQRT1xnCxiRwvw3bgvqrA==
dependencies:
tslib "^2.0.1"
@@ -1372,28 +1372,28 @@
dependencies:
tslib "^2.0.1"
-"@formatjs/intl-datetimeformat@3.2.4":
- version "3.2.4"
- resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-3.2.4.tgz#082df22e68b065b9bf297bfa25b6692640af2044"
- integrity sha512-gcwO+GitSavAixx7Q6qB8CQY8k4ioVSe2y6VaBiv7fMCCRMHjNzDRXXBe87Nikux4va2V25APPX7bR6+h9g4Zw==
+"@formatjs/intl-datetimeformat@3.2.6":
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-3.2.6.tgz#58c0902b576337cc1407e4f84ab37f3f6c073fa5"
+ integrity sha512-6tO95ajF+GOGQ4q6sBEayiytAkFcgXu2eWGvo8i24zmx51cbvVTrgc/+S6dccdsbYtvarD2LjLcY3I0RxZ09iw==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
+ "@formatjs/ecma402-abstract" "1.5.2"
tslib "^2.0.1"
-"@formatjs/intl-displaynames@4.0.4":
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-4.0.4.tgz#a3ef3d243a1cc6df51128ca3faa208969ab5fe57"
- integrity sha512-oNeLM0vZDFNZSqrz70XhxbMGtjfQ7T/UUcA9K4DvjWX6vmgCbpw5rdwEddhTotY3EmTyUJueK+14e2gIwfCbBA==
+"@formatjs/intl-displaynames@4.0.5":
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-4.0.5.tgz#2102a2bc984ce8391bf1216f6cde8d68d9f741ae"
+ integrity sha512-+01kv4Lqao245SrNW1uhYjzYlUgg8xLjxqSlbWrkAplwGBG9JNnmOze1Jmv3ls+cUe9KA0oifL6Yn28u7Gm78g==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
+ "@formatjs/ecma402-abstract" "1.5.2"
tslib "^2.0.1"
-"@formatjs/intl-listformat@5.0.4":
- version "5.0.4"
- resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-5.0.4.tgz#32b43257a4757ceab93d469c94f0fd067b302668"
- integrity sha512-0DQ2NF1PmO3+mvZp4V/SPNk7kUaLDcZR3eWbN8cGvSafWOrcv1iEcTXOd8ow8u9OA0gBTWwgPDcQFn7W0mU8kw==
+"@formatjs/intl-listformat@5.0.5":
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-5.0.5.tgz#a00b5b9c5dc140fa81cba8dea7356fe40e91fb01"
+ integrity sha512-YHmAlmUkrnoJvZtUnEuxC0N8dWPABRNDdSJksjKC1xjdpZrzHplsiY4ugw7eO56lLjsBQ1DPOAfH10iZDtmzxA==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
+ "@formatjs/ecma402-abstract" "1.5.2"
tslib "^2.0.1"
"@formatjs/intl-numberformat@^5.5.2":
@@ -1403,27 +1403,27 @@
dependencies:
"@formatjs/ecma402-abstract" "^1.2.1"
-"@formatjs/intl-relativetimeformat@8.0.3":
- version "8.0.3"
- resolved "https://registry.yarnpkg.com/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.0.3.tgz#614c681b64f90d7000f1bfddc86c1a9c3447ecc3"
- integrity sha512-OIobPtY5vtwe5IM0B0J3KmewYB/NTcbgiW9yRdWzMA1TeFSd8LfuficICYuzUZt25Kh/eIw4g37ArhS1WH/6Iw==
+"@formatjs/intl-relativetimeformat@8.0.4":
+ version "8.0.4"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.0.4.tgz#eca86236320802c7c7d0f7500573ebab679d607d"
+ integrity sha512-MUxsXa/cukBa4+8waUS7rQcVz0CKt7UufU9nXcUGh1FR85urqh42dz+3bM2klg51P5Mhove2mecQGeFwOhFYBQ==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
+ "@formatjs/ecma402-abstract" "1.5.2"
tslib "^2.0.1"
-"@formatjs/intl@1.6.1":
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.6.1.tgz#6f6de1650136feb48c475409e7d77769ae851934"
- integrity sha512-911RCkqyZuwbihUT98qBfVNsEmukdf5Y4DIVqAx+BGIyyPNUeS+JXpKx6M8awXx2L9UbeTHKKhjJbNzZlVFO+w==
+"@formatjs/intl@1.6.5":
+ version "1.6.5"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-1.6.5.tgz#a67c94113fcd58160bcf04f9aa9aefad7c95204b"
+ integrity sha512-uJ7ukBulcaN3nT5x5d7CkvzdtVYQNoEyp1Ob8N6zkibA08DMdGy1pqq5n3y3Z2GBemMUJ4qFgGSVww4Z5SXf2A==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
- "@formatjs/intl-datetimeformat" "3.2.4"
- "@formatjs/intl-displaynames" "4.0.4"
- "@formatjs/intl-listformat" "5.0.4"
- "@formatjs/intl-relativetimeformat" "8.0.3"
+ "@formatjs/ecma402-abstract" "1.5.2"
+ "@formatjs/intl-datetimeformat" "3.2.6"
+ "@formatjs/intl-displaynames" "4.0.5"
+ "@formatjs/intl-listformat" "5.0.5"
+ "@formatjs/intl-relativetimeformat" "8.0.4"
fast-memoize "^2.5.2"
- intl-messageformat "9.4.3"
- intl-messageformat-parser "6.1.3"
+ intl-messageformat "9.4.6"
+ intl-messageformat-parser "6.3.1"
tslib "^2.0.1"
"@formatjs/ts-transformer@2.13.0":
@@ -1435,12 +1435,12 @@
tslib "^2.0.1"
typescript "^4.0"
-"@formatjs/ts-transformer@3.0.1":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.0.1.tgz#4bfaab5f54ab9e014a0dc31e08462622b7ba0c6e"
- integrity sha512-8Flmp10252OxlFyLYlATBZwslfWpyR71f+wfBHsxZbwPE+LJI2knogOKSjaw+EN2b6pZ3jIVarekQJDVUkyAOQ==
+"@formatjs/ts-transformer@3.0.4":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.0.4.tgz#82979813749ddfdeb87fdc73c25ff18bb53b9d2e"
+ integrity sha512-x+0KRnA4ptfkM2W60tT5UaVOL39YEhVtvZc/FvysYDm6J76d/ucRd2qNJq6Iz+L0tygmCXURPttRQ1t3r5o79g==
dependencies:
- intl-messageformat-parser "6.1.3"
+ intl-messageformat-parser "6.3.1"
tslib "^2.0.1"
typescript "^4.0"
@@ -2025,10 +2025,10 @@
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
integrity sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==
-"@types/email-templates@^8.0.0":
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/@types/email-templates/-/email-templates-8.0.0.tgz#9bd4e328be02253d65609ff078181e98e3c2818a"
- integrity sha512-oPZQ9qkidtJCTWH3XKwfcqKdAuBrFZ6L+yVr7FQONivDm0g88MoKR0cCn13Hrt79R4r+FFAinELC22+6eXFa0Q==
+"@types/email-templates@^8.0.1":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@types/email-templates/-/email-templates-8.0.1.tgz#ed7f47b8aad12b17401d56e973589ab5049c536f"
+ integrity sha512-hc3nEX8VgeA6etrlK6Grx8wZ1QAvICfCYwTNLNRHUG1A/pT7wWivVNeKdOhmpIvkOY8qNc7LxqPRK3O9W+dDJg==
dependencies:
"@types/html-to-text" "*"
"@types/nodemailer" "*"
@@ -2047,7 +2047,7 @@
"@types/estree" "*"
"@types/json-schema" "*"
-"@types/estree@*", "@types/estree@^0.0.45":
+"@types/estree@*":
version "0.0.45"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
@@ -2160,6 +2160,13 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
+"@types/multer@^1.4.2":
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.5.tgz#db0557562307e9adb6661a9500c334cd7ddd0cd9"
+ integrity sha512-9b/0a8JyrR0r2nQhL73JR86obWL7cogfX12augvlrvcpciCo/hkvEsgu80Z4S2g2DHGVXHr8pUIi1VhqFJ8Ufw==
+ dependencies:
+ "@types/express" "*"
+
"@types/node-schedule@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-1.3.1.tgz#6785ea71b12b0b8899c3fce0650b2ef5a7ea9d1e"
@@ -2316,13 +2323,13 @@
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e"
integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g==
-"@typescript-eslint/eslint-plugin@^4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.0.tgz#92db8e7c357ed7d69632d6843ca70b71be3a721d"
- integrity sha512-IJ5e2W7uFNfg4qh9eHkHRUCbgZ8VKtGwD07kannJvM5t/GU8P8+24NX8gi3Hf5jST5oWPY8kyV1s/WtfiZ4+Ww==
+"@typescript-eslint/eslint-plugin@^4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.1.tgz#22dd301ce228aaab3416b14ead10b1db3e7d3180"
+ integrity sha512-5JriGbYhtqMS1kRcZTQxndz1lKMwwEXKbwZbkUZNnp6MJX0+OVXnG0kOlBZP4LUAxEyzu3cs+EXd/97MJXsGfw==
dependencies:
- "@typescript-eslint/experimental-utils" "4.14.0"
- "@typescript-eslint/scope-manager" "4.14.0"
+ "@typescript-eslint/experimental-utils" "4.14.1"
+ "@typescript-eslint/scope-manager" "4.14.1"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.15"
@@ -2330,53 +2337,53 @@
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/experimental-utils@4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.0.tgz#5aa7b006736634f588a69ee343ca959cd09988df"
- integrity sha512-6i6eAoiPlXMKRbXzvoQD5Yn9L7k9ezzGRvzC/x1V3650rUk3c3AOjQyGYyF9BDxQQDK2ElmKOZRD0CbtdkMzQQ==
+"@typescript-eslint/experimental-utils@4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.1.tgz#a5c945cb24dabb96747180e1cfc8487f8066f471"
+ integrity sha512-2CuHWOJwvpw0LofbyG5gvYjEyoJeSvVH2PnfUQSn0KQr4v8Dql2pr43ohmx4fdPQ/eVoTSFjTi/bsGEXl/zUUQ==
dependencies:
"@types/json-schema" "^7.0.3"
- "@typescript-eslint/scope-manager" "4.14.0"
- "@typescript-eslint/types" "4.14.0"
- "@typescript-eslint/typescript-estree" "4.14.0"
+ "@typescript-eslint/scope-manager" "4.14.1"
+ "@typescript-eslint/types" "4.14.1"
+ "@typescript-eslint/typescript-estree" "4.14.1"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
-"@typescript-eslint/parser@^4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.0.tgz#62d4cd2079d5c06683e9bfb200c758f292c4dee7"
- integrity sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg==
+"@typescript-eslint/parser@^4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.1.tgz#3bd6c24710cd557d8446625284bcc9c6d52817c6"
+ integrity sha512-mL3+gU18g9JPsHZuKMZ8Z0Ss9YP1S5xYZ7n68Z98GnPq02pYNQuRXL85b9GYhl6jpdvUc45Km7hAl71vybjUmw==
dependencies:
- "@typescript-eslint/scope-manager" "4.14.0"
- "@typescript-eslint/types" "4.14.0"
- "@typescript-eslint/typescript-estree" "4.14.0"
+ "@typescript-eslint/scope-manager" "4.14.1"
+ "@typescript-eslint/types" "4.14.1"
+ "@typescript-eslint/typescript-estree" "4.14.1"
debug "^4.1.1"
-"@typescript-eslint/scope-manager@4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.0.tgz#55a4743095d684e1f7b7180c4bac2a0a3727f517"
- integrity sha512-/J+LlRMdbPh4RdL4hfP1eCwHN5bAhFAGOTsvE6SxsrM/47XQiPSgF5MDgLyp/i9kbZV9Lx80DW0OpPkzL+uf8Q==
+"@typescript-eslint/scope-manager@4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.1.tgz#8444534254c6f370e9aa974f035ced7fe713ce02"
+ integrity sha512-F4bjJcSqXqHnC9JGUlnqSa3fC2YH5zTtmACS1Hk+WX/nFB0guuynVK5ev35D4XZbdKjulXBAQMyRr216kmxghw==
dependencies:
- "@typescript-eslint/types" "4.14.0"
- "@typescript-eslint/visitor-keys" "4.14.0"
+ "@typescript-eslint/types" "4.14.1"
+ "@typescript-eslint/visitor-keys" "4.14.1"
"@typescript-eslint/types@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==
-"@typescript-eslint/types@4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.0.tgz#d8a8202d9b58831d6fd9cee2ba12f8a5a5dd44b6"
- integrity sha512-VsQE4VvpldHrTFuVPY1ZnHn/Txw6cZGjL48e+iBxTi2ksa9DmebKjAeFmTVAYoSkTk7gjA7UqJ7pIsyifTsI4A==
+"@typescript-eslint/types@4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa"
+ integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w==
-"@typescript-eslint/typescript-estree@4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.0.tgz#4bcd67486e9acafc3d0c982b23a9ab8ac8911ed7"
- integrity sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag==
+"@typescript-eslint/typescript-estree@4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.1.tgz#20d3b8c8e3cdc8f764bdd5e5b0606dd83da6075b"
+ integrity sha512-M8+7MbzKC1PvJIA8kR2sSBnex8bsR5auatLCnVlNTJczmJgqRn8M+sAlQfkEq7M4IY3WmaNJ+LJjPVRrREVSHQ==
dependencies:
- "@typescript-eslint/types" "4.14.0"
- "@typescript-eslint/visitor-keys" "4.14.0"
+ "@typescript-eslint/types" "4.14.1"
+ "@typescript-eslint/visitor-keys" "4.14.1"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
@@ -2405,12 +2412,12 @@
dependencies:
eslint-visitor-keys "^1.1.0"
-"@typescript-eslint/visitor-keys@4.14.0":
- version "4.14.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.0.tgz#b1090d9d2955b044b2ea2904a22496849acbdf54"
- integrity sha512-MeHHzUyRI50DuiPgV9+LxcM52FCJFYjJiWHtXlbyC27b80mfOwKeiKI+MHOTEpcpfmoPFm/vvQS88bYIx6PZTA==
+"@typescript-eslint/visitor-keys@4.14.1":
+ version "4.14.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91"
+ integrity sha512-TAblbDXOI7bd0C/9PE1G+AFo7R5uc+ty1ArDoxmrC1ah61Hn6shURKy7gLdRb1qKJmjHkqu5Oq+e4Kt0jwf1IA==
dependencies:
- "@typescript-eslint/types" "4.14.0"
+ "@typescript-eslint/types" "4.14.1"
eslint-visitor-keys "^2.0.0"
"@webassemblyjs/ast@1.9.0":
@@ -4576,10 +4583,10 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
-cron-parser@^2.7.3:
- version "2.17.0"
- resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.17.0.tgz#5707421a7e0a73ee74675d1c032a2f14123f2cf8"
- integrity sha512-oTmzVEwlurRe51HqTm4afshVr8Rkxy9kFiWxh5e6SmrY2o9NDYU4S6SduanBZYXLgkLy0skA98y7/tztW/DmjQ==
+cron-parser@^2.18.0:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf"
+ integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==
dependencies:
is-nan "^1.3.0"
moment-timezone "^0.5.31"
@@ -5697,18 +5704,17 @@ eslint-config-prettier@^7.2.0:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9"
integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==
-eslint-plugin-formatjs@^2.10.3:
- version "2.10.3"
- resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-2.10.3.tgz#ebc0fdbfda1a5568ed27fef564ff60df3faae418"
- integrity sha512-mxbX7JKIUnKCn5+7O0GdGFrYNGj+gHbr8upJUumaAWCpCqPW54Zngg7X5qNtIPNvLZ68MAWNnJ5iEb5P3s9+RA==
+eslint-plugin-formatjs@^2.11.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-2.11.0.tgz#46dac11f9c0f96b72fad2da55812c25b9d864f0a"
+ integrity sha512-/HVmY343p44str15ETCejlPct25Yi1yZTEyv8MONZuUKCTG1YEeyQ+SuAC8y6Xpt/z5mFC0G5dXxz6/zc/uRJw==
dependencies:
- "@formatjs/ts-transformer" "3.0.1"
+ "@formatjs/ts-transformer" "3.0.4"
"@types/emoji-regex" "^8.0.0"
"@types/eslint" "^7.2.0"
- "@types/estree" "^0.0.45"
"@typescript-eslint/typescript-estree" "^3.6.0"
emoji-regex "^9.0.0"
- intl-messageformat-parser "6.1.3"
+ intl-messageformat-parser "6.3.1"
tslib "^2.0.1"
eslint-plugin-jsx-a11y@^6.4.1:
@@ -5996,11 +6002,12 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
-express-openapi-validator@^4.10.8:
- version "4.10.8"
- resolved "https://registry.yarnpkg.com/express-openapi-validator/-/express-openapi-validator-4.10.8.tgz#80f2e4c39d3c8e9203b2a7c79801bd573c9e2e9d"
- integrity sha512-PF40cfxOlPWgMBTquLP8v3JCP8HF6QMLl5i4LbTqncowZRF1gcQVShxSu7hRRSS2chkOQaLSYWYxQVt8ZTeBlA==
+express-openapi-validator@^4.10.9:
+ version "4.10.9"
+ resolved "https://registry.yarnpkg.com/express-openapi-validator/-/express-openapi-validator-4.10.9.tgz#cad485c12516cefde971135edef8c855c6facbf0"
+ integrity sha512-bsjNn9N3wzib7J3l9w1yOhvqz4lBX6ZAkaTBOjjmXVxCkkxr6VUkw0vgT28k183jR41aYYmKKJu4wUE7O3RlWQ==
dependencies:
+ "@types/multer" "^1.4.2"
ajv "^6.12.6"
content-type "^1.0.4"
json-schema-ref-parser "^9.0.6"
@@ -7385,12 +7392,12 @@ intl-messageformat-parser@6.1.2:
"@formatjs/ecma402-abstract" "1.5.0"
tslib "^2.0.1"
-intl-messageformat-parser@6.1.3:
- version "6.1.3"
- resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.1.3.tgz#c333850f66d686eca5c9d87eff1ad46f8721b64d"
- integrity sha512-rQTtrVTFy/Z6Lg0ieHkkhdFfi/47BKv1P9+wMWlKWaAxpdDP0FIsp2LRyLPpIVKTwUfL3xf26QT25d69cSkZgQ==
+intl-messageformat-parser@6.3.1:
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.3.1.tgz#da343ea34ed061db1597e46885d6f5a8febb68e4"
+ integrity sha512-/aK5zKeNfVO9OEGji6X8y11J0Xse7cQFVJo4YPJwQ/ts85O2J4QQsxXZBh5mMVHvHuKDDdsX+FY4/YTD9bwoNQ==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
+ "@formatjs/ecma402-abstract" "1.5.2"
tslib "^2.0.1"
intl-messageformat-parser@^5.3.7:
@@ -7400,13 +7407,13 @@ intl-messageformat-parser@^5.3.7:
dependencies:
"@formatjs/intl-numberformat" "^5.5.2"
-intl-messageformat@9.4.3:
- version "9.4.3"
- resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.4.3.tgz#c769923deced44b4c13ad35f84333a20c4f2bf38"
- integrity sha512-vTn8gCY5EvHYCYha22QcX4zMSYxkIT8r+3vXeUtvb2udicb7Z+0Ev9p/8hHzcvyrMJk0HPADnkNLVsx8EUfRkg==
+intl-messageformat@9.4.6:
+ version "9.4.6"
+ resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.4.6.tgz#dc447cd338023f7c1ad478bd8c1807691cd4f01b"
+ integrity sha512-7GSvbRcM2iuWGWoHrvqB/Yu0vvidj3eTbsjFGI9CeYxboPxlmmTGAXGJo9gbOqM5dw6zL0rFFWyMUS1b4yNPMw==
dependencies:
fast-memoize "^2.5.2"
- intl-messageformat-parser "6.1.3"
+ intl-messageformat-parser "6.3.1"
tslib "^2.0.1"
intl@^1.2.5:
@@ -9583,14 +9590,14 @@ node-releases@^1.1.65, node-releases@^1.1.67:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
-node-schedule@^1.3.2:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb"
- integrity sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw==
+node-schedule@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.3.tgz#f8e01c5fb9597f09ecf9c4c25d6938e5e7a06f48"
+ integrity sha512-uF9Ubn6luOPrcAYKfsXWimcJ1tPFtQ8I85wb4T3NgJQrXazEzojcFZVk46ZlLHby3eEJChgkV/0T689IsXh2Gw==
dependencies:
- cron-parser "^2.7.3"
+ cron-parser "^2.18.0"
long-timeout "0.1.1"
- sorted-array-functions "^1.0.0"
+ sorted-array-functions "^1.3.0"
nodemailer@6.4.16, nodemailer@^6.4.16:
version "6.4.16"
@@ -11586,21 +11593,21 @@ react-intersection-observer@^8.31.0:
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.31.0.tgz#0ed21aaf93c4c0475b22b0ccaba6169076d01605"
integrity sha512-XraIC/tkrD9JtrmVA7ypEN1QIpKc52mXBH1u/bz/aicRLo8QQEJQAMUTb8mz4B6dqpPwyzgjrr7Ljv/2ACDtqw==
-react-intl@^5.10.16:
- version "5.10.16"
- resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.10.16.tgz#2ccde74acd26cbe4f9e6e42765048a7f2f40e645"
- integrity sha512-Cp0p9MGGWYNsl3hamJcrRqKid2HZun4MsdIlHV9fdmYVoLOII1+YynH6UBBwLuDlrLi8JrSniH1G1pqUlwQZMw==
+react-intl@^5.11.0:
+ version "5.11.0"
+ resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.11.0.tgz#1606941ec402b9b566e6191f5ddb3efea967b31b"
+ integrity sha512-1ZOlKdRD1yKcm+TuuGpHrp7dGRrQeozH7n4/i5r8c6RM74ByO00bBAS1Ut4hJJcCqbMa7AqLD69kFe6Y7j46RA==
dependencies:
- "@formatjs/ecma402-abstract" "1.5.1"
- "@formatjs/intl" "1.6.1"
- "@formatjs/intl-displaynames" "4.0.4"
- "@formatjs/intl-listformat" "5.0.4"
- "@formatjs/intl-relativetimeformat" "8.0.3"
+ "@formatjs/ecma402-abstract" "1.5.2"
+ "@formatjs/intl" "1.6.5"
+ "@formatjs/intl-displaynames" "4.0.5"
+ "@formatjs/intl-listformat" "5.0.5"
+ "@formatjs/intl-relativetimeformat" "8.0.4"
"@types/hoist-non-react-statics" "^3.3.1"
fast-memoize "^2.5.2"
hoist-non-react-statics "^3.3.2"
- intl-messageformat "9.4.3"
- intl-messageformat-parser "6.1.3"
+ intl-messageformat "9.4.6"
+ intl-messageformat-parser "6.3.1"
shallow-equal "^1.2.1"
tslib "^2.0.1"
@@ -12348,10 +12355,10 @@ semantic-release-docker@^2.2.0:
"@semantic-release/error" "^2.1.0"
execa "^0.10.0"
-semantic-release@^17.3.6:
- version "17.3.6"
- resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-17.3.6.tgz#0390eb9c4d13e82dbf0f8e5700f196b599fa2a12"
- integrity sha512-zPAFxmnMtEVbN1Lzxz+CMXlt5a9txB/PRWaVq+oAC9Mppbax/vWXZ0kisHX92O+BjEBbsaFtISjz82E+2Ro9gQ==
+semantic-release@^17.3.7:
+ version "17.3.7"
+ resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-17.3.7.tgz#a54a8ccf443c7389706c9e3c52f2a9433e0581f7"
+ integrity sha512-mMWaxykq1+mZu7hvUjEdUhJXb+BvEJhcuy15hog3+8XWZqmxRfcdlW/aTBav8rWUZ16bEevWVc6qHwsq3aPGiQ==
dependencies:
"@semantic-release/commit-analyzer" "^8.0.0"
"@semantic-release/error" "^2.2.0"
@@ -12365,7 +12372,7 @@ semantic-release@^17.3.6:
execa "^5.0.0"
figures "^3.0.0"
find-versions "^4.0.0"
- get-stream "^5.0.0"
+ get-stream "^6.0.0"
git-log-parser "^1.2.0"
hook-std "^2.0.0"
hosted-git-info "^3.0.0"
@@ -12711,7 +12718,7 @@ sort-keys@^4.0.0:
dependencies:
is-plain-obj "^2.0.0"
-sorted-array-functions@^1.0.0:
+sorted-array-functions@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==
@@ -13272,10 +13279,10 @@ swagger-ui-express@^4.1.6:
dependencies:
swagger-ui-dist "^3.18.1"
-swr@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/swr/-/swr-0.4.0.tgz#e76da9f981fe6dee0e133289e9b582fc80d9c41d"
- integrity sha512-70qd1FHYHwIdYXW0jTpm5ktitzvPBCtyKz8ZzynWlY/rMqe4drYPgcl/H9Ipuh+Xv6ZW5viNx13ro8EKIWZcoQ==
+swr@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/swr/-/swr-0.4.1.tgz#4bb1f9e994a8f8d7a2e9220dbc7aef296bc4a673"
+ integrity sha512-/jQWPjCwy7rNbEJKpuObfbtJHtneTKhlzjy8VkuAEGg/kkYhZoKWVdGtIXrz9vuTGdy/grLVWpA6DfiNk9ECWg==
dependencies:
dequal "2.0.2"
From e34482bc3f9d83071437c82d93e809966fbdc93e Mon Sep 17 00:00:00 2001
From: sct
Date: Sat, 30 Jan 2021 06:31:34 +0000
Subject: [PATCH 02/66] ci(snapcraft): remove --unshallow argument from fetch
in Prepare step
---
.github/workflows/release.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3bd56884..92223b51 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -64,7 +64,7 @@ jobs:
- name: Prepare
id: prepare
run: |
- git fetch --prune --unshallow --tags
+ git fetch --prune --tags
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
echo ::set-output name=RELEASE::stable
else
From e2b800000d76c3dda9659d0d1a6ca3ffdcc17a4f Mon Sep 17 00:00:00 2001
From: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Date: Sat, 30 Jan 2021 10:48:47 -0500
Subject: [PATCH 03/66] docs(readme): remove PROXY env var and correct
grammar/inconsistencies (#788)
---
README.md | 21 ++++++++++-----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 458448bb..547e0420 100644
--- a/README.md
+++ b/README.md
@@ -20,17 +20,17 @@
-**Overseerr** is a free and open source software application for managing requests for your media library. It integrates with your existing services such as **Sonarr**, **Radarr** and **Plex**!
+**Overseerr** is a free and open source software application for managing requests for your media library. It integrates with your existing services such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)** and **[Plex](https://www.plex.tv/)**!
## Current Features
- Full Plex integration. Login and manage user access with Plex!
-- Integrates easily with your existing services. Currently Overseerr supports Sonarr and Radarr. More to come!
-- Syncs to your Plex library to know what titles you already have.
+- Easy integration with your existing services. Currently Overseerr supports Sonarr and Radarr. More to come!
+- Plex libraries sync to know what titles you already have.
- Complex request system allowing users to request individual seasons or movies in a friendly, easy to use UI.
-- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests.
-- Granular permission system
-- Mobile friendly design, for when you need to approve requests on the go!
+- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
+- Granular permission system.
+- Mobile-friendly design, for when you need to approve requests on the go!
## In Development
@@ -46,19 +46,18 @@
## Getting Started
-Check out our documentation for steps on how to install and run Overseerr:
+Check out our documentation for instructions on how to install and run Overseerr:
https://docs.overseerr.dev/getting-started/installation
## Running Overseerr
-Currently, Overseerr is only distributed through Docker images. If you have Docker, you can run Overseerr as per:
+Currently, Overseerr is primarily distributed as Docker images. If you have Docker, you can run Overseerr with:
```
docker run -d \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
- -e PROXY=
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
@@ -67,7 +66,7 @@ docker run -d \
After running Overseerr for the first time, configure it by visiting the web UI at http://[address]:5055 and completing the setup steps.
-⚠️ Overseerr is currently under very heavy, rapid development and things are likely to break often. We need all the help we can get to find bugs and get them fixed to hit a more stable release. If you would like to help test the bleeding edge, please use the image **sctx/overseerr:develop** instead! ⚠️
+⚠️ Overseerr is currently under very heavy, rapid development and things are likely to break often. We need all the help we can get to find bugs and get them fixed to hit a more stable release. If you would like to help test the bleeding edge, please use the `sctx/overseerr:develop` image instead! ⚠️
## Preview
@@ -78,7 +77,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
- Check out the [Overseerr Documentation](https://docs.overseerr.dev/) before asking for help. Your question might already be in the [FAQ](https://docs.overseerr.dev/support/faq).
- You can get support on [Discord](https://discord.gg/PkCWJSeCk7).
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions).
-- Bugs/Feature Requests can be opened via a [GitHub issue](https://github.com/sct/overseerr/issues).
+- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
## API Documentation
From b239598e64d33b78dc5d7972878840149aff360a Mon Sep 17 00:00:00 2001
From: Danshil Mungur
Date: Sun, 31 Jan 2021 04:50:15 +0400
Subject: [PATCH 04/66] feat(tv): show cast for the entire show instead of only
the last season (#778)
This uses TMDb's `aggregate_credits` instead of `credits` to get the show's cast for all seasons.
Fixes #775
---
server/api/themoviedb.ts | 15 +++++++++++++--
server/models/Tv.ts | 4 ++--
server/models/common.ts | 13 +++++++++++++
3 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/server/api/themoviedb.ts b/server/api/themoviedb.ts
index fddab1ba..47dca67a 100644
--- a/server/api/themoviedb.ts
+++ b/server/api/themoviedb.ts
@@ -126,6 +126,14 @@ export interface TmdbCreditCast {
profile_path?: string;
}
+export interface TmdbAggregateCreditCast extends TmdbCreditCast {
+ roles: {
+ credit_id: string;
+ character: string;
+ episode_count: number;
+ }[];
+}
+
export interface TmdbCreditCrew {
credit_id: string;
gender?: number;
@@ -293,8 +301,10 @@ export interface TmdbTvDetails {
type: string;
vote_average: number;
vote_count: number;
+ aggregate_credits: {
+ cast: TmdbAggregateCreditCast[];
+ };
credits: {
- cast: TmdbCreditCast[];
crew: TmdbCreditCrew[];
};
external_ids: TmdbExternalIds;
@@ -499,7 +509,8 @@ class TheMovieDb {
const response = await this.axios.get(`/tv/${tvId}`, {
params: {
language,
- append_to_response: 'credits,external_ids,keywords,videos',
+ append_to_response:
+ 'aggregate_credits,credits,external_ids,keywords,videos',
},
});
diff --git a/server/models/Tv.ts b/server/models/Tv.ts
index 5ff2f631..5f84315f 100644
--- a/server/models/Tv.ts
+++ b/server/models/Tv.ts
@@ -3,7 +3,7 @@ import {
ProductionCompany,
Cast,
Crew,
- mapCast,
+ mapAggregateCast,
mapCrew,
ExternalIds,
mapExternalIds,
@@ -193,7 +193,7 @@ export const mapTvDetails = (
: undefined,
posterPath: show.poster_path,
credits: {
- cast: show.credits.cast.map(mapCast),
+ cast: show.aggregate_credits.cast.map(mapAggregateCast),
crew: show.credits.crew.map(mapCrew),
},
externalIds: mapExternalIds(show.external_ids),
diff --git a/server/models/common.ts b/server/models/common.ts
index 733cf2df..dcd81934 100644
--- a/server/models/common.ts
+++ b/server/models/common.ts
@@ -1,5 +1,6 @@
import {
TmdbCreditCast,
+ TmdbAggregateCreditCast,
TmdbCreditCrew,
TmdbExternalIds,
TmdbVideo,
@@ -68,6 +69,18 @@ export const mapCast = (person: TmdbCreditCast): Cast => ({
profilePath: person.profile_path,
});
+export const mapAggregateCast = (person: TmdbAggregateCreditCast): Cast => ({
+ castId: person.cast_id,
+ // the first role is the one for which the actor appears the most as
+ character: person.roles[0].character,
+ creditId: person.roles[0].credit_id,
+ id: person.id,
+ name: person.name,
+ order: person.order,
+ gender: person.gender,
+ profilePath: person.profile_path,
+});
+
export const mapCrew = (person: TmdbCreditCrew): Crew => ({
creditId: person.credit_id,
department: person.department,
From 20289b5960a93545cdff9331a1a7b613f382e702 Mon Sep 17 00:00:00 2001
From: sct
Date: Sun, 31 Jan 2021 17:24:45 +0900
Subject: [PATCH 05/66] feat(cache): external API cache (#786)
---
package.json | 1 +
server/api/externalapi.ts | 106 ++
server/api/radarr.ts | 33 +-
server/api/rottentomatoes.ts | 57 +-
server/api/sonarr.ts | 33 +-
server/api/themoviedb.ts | 945 ------------------
server/api/themoviedb/constants.ts | 1 +
server/api/themoviedb/index.ts | 599 +++++++++++
server/api/themoviedb/interfaces.ts | 346 +++++++
server/entity/MediaRequest.ts | 3 +-
server/job/plexsync/index.ts | 5 +-
server/job/sonarrsync/index.ts | 3 +-
server/lib/cache.ts | 56 ++
server/models/Collection.ts | 2 +-
server/models/Movie.ts | 2 +-
server/models/Person.ts | 4 +-
server/models/Search.ts | 2 +-
server/models/Tv.ts | 4 +-
server/models/common.ts | 4 +-
server/utils/typeHelpers.ts | 2 +-
.../RequestModal/TvRequestModal.tsx | 2 +-
src/components/TvDetails/index.tsx | 2 +-
yarn.lock | 12 +
23 files changed, 1210 insertions(+), 1014 deletions(-)
create mode 100644 server/api/externalapi.ts
delete mode 100644 server/api/themoviedb.ts
create mode 100644 server/api/themoviedb/constants.ts
create mode 100644 server/api/themoviedb/index.ts
create mode 100644 server/api/themoviedb/interfaces.ts
create mode 100644 server/lib/cache.ts
diff --git a/package.json b/package.json
index 1f9acfe9..47e4455d 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "10.0.3",
+ "node-cache": "^5.1.2",
"node-schedule": "^1.3.3",
"nodemailer": "^6.4.17",
"nookies": "^2.5.2",
diff --git a/server/api/externalapi.ts b/server/api/externalapi.ts
new file mode 100644
index 00000000..6ca4c1fd
--- /dev/null
+++ b/server/api/externalapi.ts
@@ -0,0 +1,106 @@
+import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
+import NodeCache from 'node-cache';
+import logger from '../logger';
+
+// 5 minute default TTL (in seconds)
+const DEFAULT_TTL = 300;
+
+// 10 seconds default rolling buffer (in ms)
+const DEFAULT_ROLLING_BUFFER = 10000;
+
+interface ExternalAPIOptions {
+ nodeCache?: NodeCache;
+ headers?: Record;
+}
+
+class ExternalAPI {
+ protected axios: AxiosInstance;
+ private baseUrl: string;
+ private cache?: NodeCache;
+
+ constructor(
+ baseUrl: string,
+ params: Record,
+ options: ExternalAPIOptions = {}
+ ) {
+ this.axios = axios.create({
+ baseURL: baseUrl,
+ params,
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ ...options.headers,
+ },
+ });
+ this.baseUrl = baseUrl;
+ this.cache = options.nodeCache;
+ }
+
+ protected async get(
+ endpoint: string,
+ config?: AxiosRequestConfig,
+ ttl?: number
+ ): Promise {
+ const cacheKey = this.serializeCacheKey(endpoint, config?.params);
+ const cachedItem = this.cache?.get(cacheKey);
+ if (cachedItem) {
+ return cachedItem;
+ }
+
+ const response = await this.axios.get(endpoint, config);
+
+ if (this.cache) {
+ this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
+ }
+
+ return response.data;
+ }
+
+ protected async getRolling(
+ endpoint: string,
+ config?: AxiosRequestConfig,
+ ttl?: number
+ ): Promise {
+ const cacheKey = this.serializeCacheKey(endpoint, config?.params);
+ const cachedItem = this.cache?.get(cacheKey);
+
+ if (cachedItem) {
+ const keyTtl = this.cache?.getTtl(cacheKey) ?? 0;
+ logger.debug(`Loaded item from cache: ${cacheKey}`, {
+ keyTtl,
+ });
+
+ // If the item has passed our rolling check, fetch again in background
+ if (
+ keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
+ Date.now() - DEFAULT_ROLLING_BUFFER
+ ) {
+ this.axios.get(endpoint, config).then((response) => {
+ this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
+ });
+ }
+ return cachedItem;
+ }
+
+ const response = await this.axios.get(endpoint, config);
+
+ if (this.cache) {
+ this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
+ }
+
+ return response.data;
+ }
+
+ private serializeCacheKey(
+ endpoint: string,
+ params?: Record
+ ) {
+ if (!params) {
+ return `${this.baseUrl}${endpoint}`;
+ }
+
+ return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
+ }
+}
+
+export default ExternalAPI;
diff --git a/server/api/radarr.ts b/server/api/radarr.ts
index ec0c7956..8e8488d0 100644
--- a/server/api/radarr.ts
+++ b/server/api/radarr.ts
@@ -1,6 +1,7 @@
-import Axios, { AxiosInstance } from 'axios';
+import cacheManager from '../lib/cache';
import { RadarrSettings } from '../lib/settings';
import logger from '../logger';
+import ExternalAPI from './externalapi';
interface RadarrMovieOptions {
title: string;
@@ -73,21 +74,23 @@ interface QueueResponse {
records: QueueItem[];
}
-class RadarrAPI {
+class RadarrAPI extends ExternalAPI {
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
}
- private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
- this.axios = Axios.create({
- baseURL: url,
- params: {
+ super(
+ url,
+ {
apikey: apiKey,
},
- });
+ {
+ nodeCache: cacheManager.getCache('radarr').data,
+ }
+ );
}
public getMovies = async (): Promise => {
@@ -238,9 +241,13 @@ class RadarrAPI {
public getProfiles = async (): Promise => {
try {
- const response = await this.axios.get(`/profile`);
+ const data = await this.getRolling(
+ `/profile`,
+ undefined,
+ 3600
+ );
- return response.data;
+ return data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
}
@@ -248,9 +255,13 @@ class RadarrAPI {
public getRootFolders = async (): Promise => {
try {
- const response = await this.axios.get(`/rootfolder`);
+ const data = await this.getRolling(
+ `/rootfolder`,
+ undefined,
+ 3600
+ );
- return response.data;
+ return data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
}
diff --git a/server/api/rottentomatoes.ts b/server/api/rottentomatoes.ts
index cc3a562a..e83d5572 100644
--- a/server/api/rottentomatoes.ts
+++ b/server/api/rottentomatoes.ts
@@ -1,4 +1,5 @@
-import axios, { AxiosInstance } from 'axios';
+import cacheManager from '../lib/cache';
+import ExternalAPI from './externalapi';
interface RTMovieOldSearchResult {
id: number;
@@ -55,17 +56,19 @@ export interface RTRating {
* Unfortunately, we need to do it by searching for the movie name, so it's
* not always accurate.
*/
-class RottenTomatoes {
- private axios: AxiosInstance;
-
+class RottenTomatoes extends ExternalAPI {
constructor() {
- this.axios = axios.create({
- baseURL: 'https://www.rottentomatoes.com/api/private',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- },
- });
+ super(
+ 'https://www.rottentomatoes.com/api/private',
+ {},
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ nodeCache: cacheManager.getCache('rt').data,
+ }
+ );
}
/**
@@ -85,33 +88,30 @@ class RottenTomatoes {
year: number
): Promise {
try {
- const response = await this.axios.get(
- '/v1.0/movies',
- {
- params: { q: name },
- }
- );
+ const data = await this.get('/v1.0/movies', {
+ params: { q: name },
+ });
// First, attempt to match exact name and year
- let movie = response.data.movies.find(
+ let movie = data.movies.find(
(movie) => movie.year === year && movie.title === name
);
// If we don't find a movie, try to match partial name and year
if (!movie) {
- movie = response.data.movies.find(
+ movie = data.movies.find(
(movie) => movie.year === year && movie.title.includes(name)
);
}
// If we still dont find a movie, try to match just on year
if (!movie) {
- movie = response.data.movies.find((movie) => movie.year === year);
+ movie = data.movies.find((movie) => movie.year === year);
}
// One last try, try exact name match only
if (!movie) {
- movie = response.data.movies.find((movie) => movie.title === name);
+ movie = data.movies.find((movie) => movie.title === name);
}
if (!movie) {
@@ -139,19 +139,14 @@ class RottenTomatoes {
year?: number
): Promise {
try {
- const response = await this.axios.get(
- '/v2.0/search/',
- {
- params: { q: name, limit: 10 },
- }
- );
+ const data = await this.get('/v2.0/search/', {
+ params: { q: name, limit: 10 },
+ });
- let tvshow: RTTvSearchResult | undefined = response.data.tvSeries[0];
+ let tvshow: RTTvSearchResult | undefined = data.tvSeries[0];
if (year) {
- tvshow = response.data.tvSeries.find(
- (series) => series.startYear === year
- );
+ tvshow = data.tvSeries.find((series) => series.startYear === year);
}
if (!tvshow) {
diff --git a/server/api/sonarr.ts b/server/api/sonarr.ts
index 29c6b431..681cb1f3 100644
--- a/server/api/sonarr.ts
+++ b/server/api/sonarr.ts
@@ -1,6 +1,7 @@
-import Axios, { AxiosInstance } from 'axios';
+import cacheManager from '../lib/cache';
import { SonarrSettings } from '../lib/settings';
import logger from '../logger';
+import ExternalAPI from './externalapi';
interface SonarrSeason {
seasonNumber: number;
@@ -119,21 +120,23 @@ interface AddSeriesOptions {
searchNow?: boolean;
}
-class SonarrAPI {
+class SonarrAPI extends ExternalAPI {
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
}
- private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
- this.axios = Axios.create({
- baseURL: url,
- params: {
+ super(
+ url,
+ {
apikey: apiKey,
},
- });
+ {
+ nodeCache: cacheManager.getCache('sonarr').data,
+ }
+ );
}
public async getSeries(): Promise {
@@ -280,9 +283,13 @@ class SonarrAPI {
public async getProfiles(): Promise {
try {
- const response = await this.axios.get('/profile');
+ const data = await this.getRolling(
+ '/profile',
+ undefined,
+ 3600
+ );
- return response.data;
+ return data;
} catch (e) {
logger.error('Something went wrong while retrieving Sonarr profiles.', {
label: 'Sonarr API',
@@ -294,9 +301,13 @@ class SonarrAPI {
public async getRootFolders(): Promise {
try {
- const response = await this.axios.get('/rootfolder');
+ const data = await this.getRolling(
+ '/rootfolder',
+ undefined,
+ 3600
+ );
- return response.data;
+ return data;
} catch (e) {
logger.error(
'Something went wrong while retrieving Sonarr root folders.',
diff --git a/server/api/themoviedb.ts b/server/api/themoviedb.ts
deleted file mode 100644
index 47dca67a..00000000
--- a/server/api/themoviedb.ts
+++ /dev/null
@@ -1,945 +0,0 @@
-import axios, { AxiosInstance } from 'axios';
-
-export const ANIME_KEYWORD_ID = 210024;
-
-interface SearchOptions {
- query: string;
- page?: number;
- includeAdult?: boolean;
- language?: string;
-}
-
-interface DiscoverMovieOptions {
- page?: number;
- includeAdult?: boolean;
- language?: string;
- sortBy?:
- | 'popularity.asc'
- | 'popularity.desc'
- | 'release_date.asc'
- | 'release_date.desc'
- | 'revenue.asc'
- | 'revenue.desc'
- | 'primary_release_date.asc'
- | 'primary_release_date.desc'
- | 'original_title.asc'
- | 'original_title.desc'
- | 'vote_average.asc'
- | 'vote_average.desc'
- | 'vote_count.asc'
- | 'vote_count.desc';
-}
-
-interface DiscoverTvOptions {
- page?: number;
- language?: string;
- sortBy?:
- | 'popularity.asc'
- | 'popularity.desc'
- | 'vote_average.asc'
- | 'vote_average.desc'
- | 'vote_count.asc'
- | 'vote_count.desc'
- | 'first_air_date.asc'
- | 'first_air_date.desc';
-}
-
-interface TmdbMediaResult {
- id: number;
- media_type: string;
- popularity: number;
- poster_path?: string;
- backdrop_path?: string;
- vote_count: number;
- vote_average: number;
- genre_ids: number[];
- overview: string;
- original_language: string;
-}
-
-export interface TmdbMovieResult extends TmdbMediaResult {
- media_type: 'movie';
- title: string;
- original_title: string;
- release_date: string;
- adult: boolean;
- video: boolean;
-}
-
-export interface TmdbTvResult extends TmdbMediaResult {
- media_type: 'tv';
- name: string;
- original_name: string;
- origin_country: string[];
- first_air_date: string;
-}
-
-export interface TmdbPersonResult {
- id: number;
- name: string;
- popularity: number;
- profile_path?: string;
- adult: boolean;
- media_type: 'person';
- known_for: (TmdbMovieResult | TmdbTvResult)[];
-}
-
-interface TmdbPaginatedResponse {
- page: number;
- total_results: number;
- total_pages: number;
-}
-
-interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
- results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
-}
-
-interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
- results: TmdbMovieResult[];
-}
-
-interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
- results: TmdbTvResult[];
-}
-
-interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
- dates: {
- maximum: string;
- minimum: string;
- };
- results: TmdbMovieResult[];
-}
-
-interface TmdbExternalIdResponse {
- movie_results: TmdbMovieResult[];
- tv_results: TmdbTvResult[];
-}
-
-export interface TmdbCreditCast {
- cast_id: number;
- character: string;
- credit_id: string;
- gender?: number;
- id: number;
- name: string;
- order: number;
- profile_path?: string;
-}
-
-export interface TmdbAggregateCreditCast extends TmdbCreditCast {
- roles: {
- credit_id: string;
- character: string;
- episode_count: number;
- }[];
-}
-
-export interface TmdbCreditCrew {
- credit_id: string;
- gender?: number;
- id: number;
- name: string;
- profile_path?: string;
- job: string;
- department: string;
-}
-
-export interface TmdbExternalIds {
- imdb_id?: string;
- freebase_mid?: string;
- freebase_id?: string;
- tvdb_id?: number;
- tvrage_id?: string;
- facebook_id?: string;
- instagram_id?: string;
- twitter_id?: string;
-}
-
-export interface TmdbMovieDetails {
- id: number;
- imdb_id?: string;
- adult: boolean;
- backdrop_path?: string;
- poster_path?: string;
- budget: number;
- genres: {
- id: number;
- name: string;
- }[];
- homepage?: string;
- original_language: string;
- original_title: string;
- overview?: string;
- popularity: number;
- production_companies: {
- id: number;
- name: string;
- logo_path?: string;
- origin_country: string;
- }[];
- production_countries: {
- iso_3166_1: string;
- name: string;
- }[];
- release_date: string;
- revenue: number;
- runtime?: number;
- spoken_languages: {
- iso_639_1: string;
- name: string;
- }[];
- status: string;
- tagline?: string;
- title: string;
- video: boolean;
- vote_average: number;
- vote_count: number;
- credits: {
- cast: TmdbCreditCast[];
- crew: TmdbCreditCrew[];
- };
- belongs_to_collection?: {
- id: number;
- name: string;
- poster_path?: string;
- backdrop_path?: string;
- };
- external_ids: TmdbExternalIds;
- videos: TmdbVideoResult;
-}
-
-export interface TmdbVideo {
- id: string;
- key: string;
- name: string;
- site: 'YouTube';
- size: number;
- type:
- | 'Clip'
- | 'Teaser'
- | 'Trailer'
- | 'Featurette'
- | 'Opening Credits'
- | 'Behind the Scenes'
- | 'Bloopers';
-}
-
-export interface TmdbTvEpisodeResult {
- id: number;
- air_date: string;
- episode_number: number;
- name: string;
- overview: string;
- production_code: string;
- season_number: number;
- show_id: number;
- still_path: string;
- vote_average: number;
- vote_cuont: number;
-}
-
-export interface TmdbTvSeasonResult {
- id: number;
- air_date: string;
- episode_count: number;
- name: string;
- overview: string;
- poster_path?: string;
- season_number: number;
-}
-
-export interface TmdbTvDetails {
- id: number;
- backdrop_path?: string;
- created_by: {
- id: number;
- credit_id: string;
- name: string;
- gender: number;
- profile_path?: string;
- }[];
- episode_run_time: number[];
- first_air_date: string;
- genres: {
- id: number;
- name: string;
- }[];
- homepage: string;
- in_production: boolean;
- languages: string[];
- last_air_date: string;
- last_episode_to_air?: TmdbTvEpisodeResult;
- name: string;
- next_episode_to_air?: TmdbTvEpisodeResult;
- networks: {
- id: number;
- name: string;
- logo_path: string;
- origin_country: string;
- }[];
- number_of_episodes: number;
- number_of_seasons: number;
- origin_country: string[];
- original_language: string;
- original_name: string;
- overview: string;
- popularity: number;
- poster_path?: string;
- production_companies: {
- id: number;
- logo_path?: string;
- name: string;
- origin_country: string;
- }[];
- spoken_languages: {
- english_name: string;
- iso_639_1: string;
- name: string;
- }[];
- seasons: TmdbTvSeasonResult[];
- status: string;
- type: string;
- vote_average: number;
- vote_count: number;
- aggregate_credits: {
- cast: TmdbAggregateCreditCast[];
- };
- credits: {
- crew: TmdbCreditCrew[];
- };
- external_ids: TmdbExternalIds;
- keywords: {
- results: TmdbKeyword[];
- };
- videos: TmdbVideoResult;
-}
-
-export interface TmdbVideoResult {
- results: TmdbVideo[];
-}
-
-export interface TmdbKeyword {
- id: number;
- name: string;
-}
-
-export interface TmdbPersonDetail {
- id: number;
- name: string;
- deathday: string;
- known_for_department: string;
- also_known_as?: string[];
- gender: number;
- biography: string;
- popularity: string;
- place_of_birth?: string;
- profile_path?: string;
- adult: boolean;
- imdb_id?: string;
- homepage?: string;
-}
-
-export interface TmdbPersonCredit {
- id: number;
- original_language: string;
- episode_count: number;
- overview: string;
- origin_country: string[];
- original_name: string;
- vote_count: number;
- name: string;
- media_type?: string;
- popularity: number;
- credit_id: string;
- backdrop_path?: string;
- first_air_date: string;
- vote_average: number;
- genre_ids?: number[];
- poster_path?: string;
- original_title: string;
- video?: boolean;
- title: string;
- adult: boolean;
- release_date: string;
-}
-export interface TmdbPersonCreditCast extends TmdbPersonCredit {
- character: string;
-}
-
-export interface TmdbPersonCreditCrew extends TmdbPersonCredit {
- department: string;
- job: string;
-}
-
-export interface TmdbPersonCombinedCredits {
- id: number;
- cast: TmdbPersonCreditCast[];
- crew: TmdbPersonCreditCrew[];
-}
-
-export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
- episodes: TmdbTvEpisodeResult[];
- external_ids: TmdbExternalIds;
-}
-
-export interface TmdbCollection {
- id: number;
- name: string;
- overview?: string;
- poster_path?: string;
- backdrop_path?: string;
- parts: TmdbMovieResult[];
-}
-
-class TheMovieDb {
- private apiKey = 'db55323b8d3e4154498498a75642b381';
- private axios: AxiosInstance;
-
- constructor() {
- this.axios = axios.create({
- baseURL: 'https://api.themoviedb.org/3',
- params: {
- api_key: this.apiKey,
- },
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- },
- });
- }
-
- public searchMulti = async ({
- query,
- page = 1,
- includeAdult = false,
- language = 'en-US',
- }: SearchOptions): Promise => {
- try {
- const response = await this.axios.get('/search/multi', {
- params: { query, page, include_adult: includeAdult, language },
- });
-
- return response.data;
- } catch (e) {
- return {
- page: 1,
- results: [],
- total_pages: 1,
- total_results: 0,
- };
- }
- };
-
- public getPerson = async ({
- personId,
- language = 'en-US',
- }: {
- personId: number;
- language?: string;
- }): Promise => {
- try {
- const response = await this.axios.get(
- `/person/${personId}`,
- {
- params: { language },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
- }
- };
-
- public getPersonCombinedCredits = async ({
- personId,
- language = 'en-US',
- }: {
- personId: number;
- language?: string;
- }): Promise => {
- try {
- const response = await this.axios.get(
- `/person/${personId}/combined_credits`,
- {
- params: { language },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(
- `[TMDB] Failed to fetch person combined credits: ${e.message}`
- );
- }
- };
-
- public getMovie = async ({
- movieId,
- language = 'en-US',
- }: {
- movieId: number;
- language?: string;
- }): Promise => {
- try {
- const response = await this.axios.get(
- `/movie/${movieId}`,
- {
- params: {
- language,
- append_to_response: 'credits,external_ids,videos',
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
- }
- };
-
- public getTvShow = async ({
- tvId,
- language = 'en-US',
- }: {
- tvId: number;
- language?: string;
- }): Promise => {
- try {
- const response = await this.axios.get(`/tv/${tvId}`, {
- params: {
- language,
- append_to_response:
- 'aggregate_credits,credits,external_ids,keywords,videos',
- },
- });
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
- }
- };
-
- public getTvSeason = async ({
- tvId,
- seasonNumber,
- language,
- }: {
- tvId: number;
- seasonNumber: number;
- language?: string;
- }): Promise => {
- try {
- const response = await this.axios.get(
- `/tv/${tvId}/season/${seasonNumber}`,
- {
- params: {
- language,
- append_to_response: 'external_ids',
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
- }
- };
-
- public async getMovieRecommendations({
- movieId,
- page = 1,
- language = 'en-US',
- }: {
- movieId: number;
- page?: number;
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/movie/${movieId}/recommendations`,
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
- }
- }
-
- public async getMovieSimilar({
- movieId,
- page = 1,
- language = 'en-US',
- }: {
- movieId: number;
- page?: number;
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/movie/${movieId}/similar`,
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
- }
- }
-
- public async getMoviesByKeyword({
- keywordId,
- page = 1,
- language = 'en-US',
- }: {
- keywordId: number;
- page?: number;
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/keyword/${keywordId}/movies`,
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
- }
- }
-
- public async getTvRecommendations({
- tvId,
- page = 1,
- language = 'en-US',
- }: {
- tvId: number;
- page?: number;
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/tv/${tvId}/recommendations`,
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(
- `[TMDB] Failed to fetch tv recommendations: ${e.message}`
- );
- }
- }
-
- public async getTvSimilar({
- tvId,
- page = 1,
- language = 'en-US',
- }: {
- tvId: number;
- page?: number;
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/tv/${tvId}/similar`,
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch tv similar: ${e.message}`);
- }
- }
-
- public getDiscoverMovies = async ({
- sortBy = 'popularity.desc',
- page = 1,
- includeAdult = false,
- language = 'en-US',
- }: DiscoverMovieOptions = {}): Promise => {
- try {
- const response = await this.axios.get(
- '/discover/movie',
- {
- params: {
- sort_by: sortBy,
- page,
- include_adult: includeAdult,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
- }
- };
-
- public getDiscoverTv = async ({
- sortBy = 'popularity.desc',
- page = 1,
- language = 'en-US',
- }: DiscoverTvOptions = {}): Promise => {
- try {
- const response = await this.axios.get(
- '/discover/tv',
- {
- params: {
- sort_by: sortBy,
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch discover tv: ${e.message}`);
- }
- };
-
- public getUpcomingMovies = async ({
- page = 1,
- language = 'en-US',
- }: {
- page: number;
- language: string;
- }): Promise => {
- try {
- const response = await this.axios.get(
- '/movie/upcoming',
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
- }
- };
-
- public getAllTrending = async ({
- page = 1,
- timeWindow = 'day',
- language = 'en-US',
- }: {
- page?: number;
- timeWindow?: 'day' | 'week';
- language?: string;
- } = {}): Promise => {
- try {
- const response = await this.axios.get(
- `/trending/all/${timeWindow}`,
- {
- params: {
- page,
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
- }
- };
-
- public getMovieTrending = async ({
- page = 1,
- timeWindow = 'day',
- }: {
- page?: number;
- timeWindow?: 'day' | 'week';
- } = {}): Promise => {
- try {
- const response = await this.axios.get(
- `/trending/movie/${timeWindow}`,
- {
- params: {
- page,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
- }
- };
-
- public getTvTrending = async ({
- page = 1,
- timeWindow = 'day',
- }: {
- page?: number;
- timeWindow?: 'day' | 'week';
- } = {}): Promise => {
- try {
- const response = await this.axios.get(
- `/trending/tv/${timeWindow}`,
- {
- params: {
- page,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
- }
- };
-
- public async getByExternalId({
- externalId,
- type,
- language = 'en-US',
- }:
- | {
- externalId: string;
- type: 'imdb';
- language?: string;
- }
- | {
- externalId: number;
- type: 'tvdb';
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/find/${externalId}`,
- {
- params: {
- external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
- }
- }
-
- public async getMovieByImdbId({
- imdbId,
- language = 'en-US',
- }: {
- imdbId: string;
- language?: string;
- }): Promise {
- try {
- const extResponse = await this.getByExternalId({
- externalId: imdbId,
- type: 'imdb',
- });
-
- if (extResponse.movie_results[0]) {
- const movie = await this.getMovie({
- movieId: extResponse.movie_results[0].id,
- language,
- });
-
- return movie;
- }
-
- throw new Error(
- '[TMDB] Failed to find a title with the provided IMDB id'
- );
- } catch (e) {
- throw new Error(
- `[TMDB] Failed to get movie by external imdb ID: ${e.message}`
- );
- }
- }
-
- public async getShowByTvdbId({
- tvdbId,
- language = 'en-US',
- }: {
- tvdbId: number;
- language?: string;
- }): Promise {
- try {
- const extResponse = await this.getByExternalId({
- externalId: tvdbId,
- type: 'tvdb',
- });
-
- if (extResponse.tv_results[0]) {
- const tvshow = await this.getTvShow({
- tvId: extResponse.tv_results[0].id,
- language,
- });
-
- return tvshow;
- }
-
- throw new Error(
- `[TMDB] Failed to find a TV show with the provided TVDB ID: ${tvdbId}`
- );
- } catch (e) {
- throw new Error(
- `[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
- );
- }
- }
-
- public async getCollection({
- collectionId,
- language = 'en-US',
- }: {
- collectionId: number;
- language?: string;
- }): Promise {
- try {
- const response = await this.axios.get(
- `/collection/${collectionId}`,
- {
- params: {
- language,
- },
- }
- );
-
- return response.data;
- } catch (e) {
- throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
- }
- }
-}
-
-export default TheMovieDb;
diff --git a/server/api/themoviedb/constants.ts b/server/api/themoviedb/constants.ts
new file mode 100644
index 00000000..be475f72
--- /dev/null
+++ b/server/api/themoviedb/constants.ts
@@ -0,0 +1 @@
+export const ANIME_KEYWORD_ID = 210024;
diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts
new file mode 100644
index 00000000..bf716385
--- /dev/null
+++ b/server/api/themoviedb/index.ts
@@ -0,0 +1,599 @@
+import cacheManager from '../../lib/cache';
+import ExternalAPI from '../externalapi';
+import {
+ TmdbCollection,
+ TmdbExternalIdResponse,
+ TmdbMovieDetails,
+ TmdbPersonCombinedCredits,
+ TmdbPersonDetail,
+ TmdbSearchMovieResponse,
+ TmdbSearchMultiResponse,
+ TmdbSearchTvResponse,
+ TmdbSeasonWithEpisodes,
+ TmdbTvDetails,
+ TmdbUpcomingMoviesResponse,
+} from './interfaces';
+
+interface SearchOptions {
+ query: string;
+ page?: number;
+ includeAdult?: boolean;
+ language?: string;
+}
+
+interface DiscoverMovieOptions {
+ page?: number;
+ includeAdult?: boolean;
+ language?: string;
+ sortBy?:
+ | 'popularity.asc'
+ | 'popularity.desc'
+ | 'release_date.asc'
+ | 'release_date.desc'
+ | 'revenue.asc'
+ | 'revenue.desc'
+ | 'primary_release_date.asc'
+ | 'primary_release_date.desc'
+ | 'original_title.asc'
+ | 'original_title.desc'
+ | 'vote_average.asc'
+ | 'vote_average.desc'
+ | 'vote_count.asc'
+ | 'vote_count.desc';
+}
+
+interface DiscoverTvOptions {
+ page?: number;
+ language?: string;
+ sortBy?:
+ | 'popularity.asc'
+ | 'popularity.desc'
+ | 'vote_average.asc'
+ | 'vote_average.desc'
+ | 'vote_count.asc'
+ | 'vote_count.desc'
+ | 'first_air_date.asc'
+ | 'first_air_date.desc';
+}
+
+class TheMovieDb extends ExternalAPI {
+ constructor() {
+ super(
+ 'https://api.themoviedb.org/3',
+ {
+ api_key: 'db55323b8d3e4154498498a75642b381',
+ },
+ {
+ nodeCache: cacheManager.getCache('tmdb').data,
+ }
+ );
+ }
+
+ public searchMulti = async ({
+ query,
+ page = 1,
+ includeAdult = false,
+ language = 'en',
+ }: SearchOptions): Promise => {
+ try {
+ const data = await this.get('/search/multi', {
+ params: { query, page, include_adult: includeAdult, language },
+ });
+
+ return data;
+ } catch (e) {
+ return {
+ page: 1,
+ results: [],
+ total_pages: 1,
+ total_results: 0,
+ };
+ }
+ };
+
+ public getPerson = async ({
+ personId,
+ language = 'en',
+ }: {
+ personId: number;
+ language?: string;
+ }): Promise => {
+ try {
+ const data = await this.get(`/person/${personId}`, {
+ params: { language },
+ });
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
+ }
+ };
+
+ public getPersonCombinedCredits = async ({
+ personId,
+ language = 'en',
+ }: {
+ personId: number;
+ language?: string;
+ }): Promise => {
+ try {
+ const data = await this.get(
+ `/person/${personId}/combined_credits`,
+ {
+ params: { language },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(
+ `[TMDB] Failed to fetch person combined credits: ${e.message}`
+ );
+ }
+ };
+
+ public getMovie = async ({
+ movieId,
+ language = 'en',
+ }: {
+ movieId: number;
+ language?: string;
+ }): Promise => {
+ try {
+ const data = await this.get(
+ `/movie/${movieId}`,
+ {
+ params: {
+ language,
+ append_to_response: 'credits,external_ids,videos',
+ },
+ },
+ 900
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
+ }
+ };
+
+ public getTvShow = async ({
+ tvId,
+ language = 'en',
+ }: {
+ tvId: number;
+ language?: string;
+ }): Promise => {
+ try {
+ const data = await this.get(
+ `/tv/${tvId}`,
+ {
+ params: {
+ language,
+ append_to_response:
+ 'aggregate_credits,credits,external_ids,keywords,videos',
+ },
+ },
+ 900
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
+ }
+ };
+
+ public getTvSeason = async ({
+ tvId,
+ seasonNumber,
+ language,
+ }: {
+ tvId: number;
+ seasonNumber: number;
+ language?: string;
+ }): Promise => {
+ try {
+ const data = await this.get(
+ `/tv/${tvId}/season/${seasonNumber}`,
+ {
+ params: {
+ language,
+ append_to_response: 'external_ids',
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
+ }
+ };
+
+ public async getMovieRecommendations({
+ movieId,
+ page = 1,
+ language = 'en',
+ }: {
+ movieId: number;
+ page?: number;
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(
+ `/movie/${movieId}/recommendations`,
+ {
+ params: {
+ page,
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
+ }
+ }
+
+ public async getMovieSimilar({
+ movieId,
+ page = 1,
+ language = 'en',
+ }: {
+ movieId: number;
+ page?: number;
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(
+ `/movie/${movieId}/similar`,
+ {
+ params: {
+ page,
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
+ }
+ }
+
+ public async getMoviesByKeyword({
+ keywordId,
+ page = 1,
+ language = 'en',
+ }: {
+ keywordId: number;
+ page?: number;
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(
+ `/keyword/${keywordId}/movies`,
+ {
+ params: {
+ page,
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
+ }
+ }
+
+ public async getTvRecommendations({
+ tvId,
+ page = 1,
+ language = 'en',
+ }: {
+ tvId: number;
+ page?: number;
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(
+ `/tv/${tvId}/recommendations`,
+ {
+ params: {
+ page,
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(
+ `[TMDB] Failed to fetch tv recommendations: ${e.message}`
+ );
+ }
+ }
+
+ public async getTvSimilar({
+ tvId,
+ page = 1,
+ language = 'en',
+ }: {
+ tvId: number;
+ page?: number;
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(`/tv/${tvId}/similar`, {
+ params: {
+ page,
+ language,
+ },
+ });
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch tv similar: ${e.message}`);
+ }
+ }
+
+ public getDiscoverMovies = async ({
+ sortBy = 'popularity.desc',
+ page = 1,
+ includeAdult = false,
+ language = 'en',
+ }: DiscoverMovieOptions = {}): Promise => {
+ try {
+ const data = await this.get('/discover/movie', {
+ params: {
+ sort_by: sortBy,
+ page,
+ include_adult: includeAdult,
+ language,
+ },
+ });
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
+ }
+ };
+
+ public getDiscoverTv = async ({
+ sortBy = 'popularity.desc',
+ page = 1,
+ language = 'en',
+ }: DiscoverTvOptions = {}): Promise => {
+ try {
+ const data = await this.get('/discover/tv', {
+ params: {
+ sort_by: sortBy,
+ page,
+ language,
+ },
+ });
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch discover tv: ${e.message}`);
+ }
+ };
+
+ public getUpcomingMovies = async ({
+ page = 1,
+ language = 'en',
+ }: {
+ page: number;
+ language: string;
+ }): Promise => {
+ try {
+ const data = await this.get(
+ '/movie/upcoming',
+ {
+ params: {
+ page,
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
+ }
+ };
+
+ public getAllTrending = async ({
+ page = 1,
+ timeWindow = 'day',
+ language = 'en',
+ }: {
+ page?: number;
+ timeWindow?: 'day' | 'week';
+ language?: string;
+ } = {}): Promise => {
+ try {
+ const data = await this.get(
+ `/trending/all/${timeWindow}`,
+ {
+ params: {
+ page,
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
+ }
+ };
+
+ public getMovieTrending = async ({
+ page = 1,
+ timeWindow = 'day',
+ }: {
+ page?: number;
+ timeWindow?: 'day' | 'week';
+ } = {}): Promise => {
+ try {
+ const data = await this.get(
+ `/trending/movie/${timeWindow}`,
+ {
+ params: {
+ page,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
+ }
+ };
+
+ public getTvTrending = async ({
+ page = 1,
+ timeWindow = 'day',
+ }: {
+ page?: number;
+ timeWindow?: 'day' | 'week';
+ } = {}): Promise => {
+ try {
+ const data = await this.get(
+ `/trending/tv/${timeWindow}`,
+ {
+ params: {
+ page,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
+ }
+ };
+
+ public async getByExternalId({
+ externalId,
+ type,
+ language = 'en',
+ }:
+ | {
+ externalId: string;
+ type: 'imdb';
+ language?: string;
+ }
+ | {
+ externalId: number;
+ type: 'tvdb';
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(
+ `/find/${externalId}`,
+ {
+ params: {
+ external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
+ }
+ }
+
+ public async getMovieByImdbId({
+ imdbId,
+ language = 'en',
+ }: {
+ imdbId: string;
+ language?: string;
+ }): Promise {
+ try {
+ const extResponse = await this.getByExternalId({
+ externalId: imdbId,
+ type: 'imdb',
+ });
+
+ if (extResponse.movie_results[0]) {
+ const movie = await this.getMovie({
+ movieId: extResponse.movie_results[0].id,
+ language,
+ });
+
+ return movie;
+ }
+
+ throw new Error(
+ '[TMDB] Failed to find a title with the provided IMDB id'
+ );
+ } catch (e) {
+ throw new Error(
+ `[TMDB] Failed to get movie by external imdb ID: ${e.message}`
+ );
+ }
+ }
+
+ public async getShowByTvdbId({
+ tvdbId,
+ language = 'en',
+ }: {
+ tvdbId: number;
+ language?: string;
+ }): Promise {
+ try {
+ const extResponse = await this.getByExternalId({
+ externalId: tvdbId,
+ type: 'tvdb',
+ });
+
+ if (extResponse.tv_results[0]) {
+ const tvshow = await this.getTvShow({
+ tvId: extResponse.tv_results[0].id,
+ language,
+ });
+
+ return tvshow;
+ }
+
+ throw new Error(
+ `[TMDB] Failed to find a TV show with the provided TVDB ID: ${tvdbId}`
+ );
+ } catch (e) {
+ throw new Error(
+ `[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
+ );
+ }
+ }
+
+ public async getCollection({
+ collectionId,
+ language = 'en',
+ }: {
+ collectionId: number;
+ language?: string;
+ }): Promise {
+ try {
+ const data = await this.get(
+ `/collection/${collectionId}`,
+ {
+ params: {
+ language,
+ },
+ }
+ );
+
+ return data;
+ } catch (e) {
+ throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
+ }
+ }
+}
+
+export default TheMovieDb;
diff --git a/server/api/themoviedb/interfaces.ts b/server/api/themoviedb/interfaces.ts
new file mode 100644
index 00000000..63b0ba9a
--- /dev/null
+++ b/server/api/themoviedb/interfaces.ts
@@ -0,0 +1,346 @@
+interface TmdbMediaResult {
+ id: number;
+ media_type: string;
+ popularity: number;
+ poster_path?: string;
+ backdrop_path?: string;
+ vote_count: number;
+ vote_average: number;
+ genre_ids: number[];
+ overview: string;
+ original_language: string;
+}
+
+export interface TmdbMovieResult extends TmdbMediaResult {
+ media_type: 'movie';
+ title: string;
+ original_title: string;
+ release_date: string;
+ adult: boolean;
+ video: boolean;
+}
+
+export interface TmdbTvResult extends TmdbMediaResult {
+ media_type: 'tv';
+ name: string;
+ original_name: string;
+ origin_country: string[];
+ first_air_date: string;
+}
+
+export interface TmdbPersonResult {
+ id: number;
+ name: string;
+ popularity: number;
+ profile_path?: string;
+ adult: boolean;
+ media_type: 'person';
+ known_for: (TmdbMovieResult | TmdbTvResult)[];
+}
+
+interface TmdbPaginatedResponse {
+ page: number;
+ total_results: number;
+ total_pages: number;
+}
+
+export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
+ results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
+}
+
+export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
+ results: TmdbMovieResult[];
+}
+
+export interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
+ results: TmdbTvResult[];
+}
+
+export interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
+ dates: {
+ maximum: string;
+ minimum: string;
+ };
+ results: TmdbMovieResult[];
+}
+
+export interface TmdbExternalIdResponse {
+ movie_results: TmdbMovieResult[];
+ tv_results: TmdbTvResult[];
+}
+
+export interface TmdbCreditCast {
+ cast_id: number;
+ character: string;
+ credit_id: string;
+ gender?: number;
+ id: number;
+ name: string;
+ order: number;
+ profile_path?: string;
+}
+
+export interface TmdbAggregateCreditCast extends TmdbCreditCast {
+ roles: {
+ credit_id: string;
+ character: string;
+ episode_count: number;
+ }[];
+}
+
+export interface TmdbCreditCrew {
+ credit_id: string;
+ gender?: number;
+ id: number;
+ name: string;
+ profile_path?: string;
+ job: string;
+ department: string;
+}
+
+export interface TmdbExternalIds {
+ imdb_id?: string;
+ freebase_mid?: string;
+ freebase_id?: string;
+ tvdb_id?: number;
+ tvrage_id?: string;
+ facebook_id?: string;
+ instagram_id?: string;
+ twitter_id?: string;
+}
+
+export interface TmdbMovieDetails {
+ id: number;
+ imdb_id?: string;
+ adult: boolean;
+ backdrop_path?: string;
+ poster_path?: string;
+ budget: number;
+ genres: {
+ id: number;
+ name: string;
+ }[];
+ homepage?: string;
+ original_language: string;
+ original_title: string;
+ overview?: string;
+ popularity: number;
+ production_companies: {
+ id: number;
+ name: string;
+ logo_path?: string;
+ origin_country: string;
+ }[];
+ production_countries: {
+ iso_3166_1: string;
+ name: string;
+ }[];
+ release_date: string;
+ revenue: number;
+ runtime?: number;
+ spoken_languages: {
+ iso_639_1: string;
+ name: string;
+ }[];
+ status: string;
+ tagline?: string;
+ title: string;
+ video: boolean;
+ vote_average: number;
+ vote_count: number;
+ credits: {
+ cast: TmdbCreditCast[];
+ crew: TmdbCreditCrew[];
+ };
+ belongs_to_collection?: {
+ id: number;
+ name: string;
+ poster_path?: string;
+ backdrop_path?: string;
+ };
+ external_ids: TmdbExternalIds;
+ videos: TmdbVideoResult;
+}
+
+export interface TmdbVideo {
+ id: string;
+ key: string;
+ name: string;
+ site: 'YouTube';
+ size: number;
+ type:
+ | 'Clip'
+ | 'Teaser'
+ | 'Trailer'
+ | 'Featurette'
+ | 'Opening Credits'
+ | 'Behind the Scenes'
+ | 'Bloopers';
+}
+
+export interface TmdbTvEpisodeResult {
+ id: number;
+ air_date: string;
+ episode_number: number;
+ name: string;
+ overview: string;
+ production_code: string;
+ season_number: number;
+ show_id: number;
+ still_path: string;
+ vote_average: number;
+ vote_cuont: number;
+}
+
+export interface TmdbTvSeasonResult {
+ id: number;
+ air_date: string;
+ episode_count: number;
+ name: string;
+ overview: string;
+ poster_path?: string;
+ season_number: number;
+}
+
+export interface TmdbTvDetails {
+ id: number;
+ backdrop_path?: string;
+ created_by: {
+ id: number;
+ credit_id: string;
+ name: string;
+ gender: number;
+ profile_path?: string;
+ }[];
+ episode_run_time: number[];
+ first_air_date: string;
+ genres: {
+ id: number;
+ name: string;
+ }[];
+ homepage: string;
+ in_production: boolean;
+ languages: string[];
+ last_air_date: string;
+ last_episode_to_air?: TmdbTvEpisodeResult;
+ name: string;
+ next_episode_to_air?: TmdbTvEpisodeResult;
+ networks: {
+ id: number;
+ name: string;
+ logo_path: string;
+ origin_country: string;
+ }[];
+ number_of_episodes: number;
+ number_of_seasons: number;
+ origin_country: string[];
+ original_language: string;
+ original_name: string;
+ overview: string;
+ popularity: number;
+ poster_path?: string;
+ production_companies: {
+ id: number;
+ logo_path?: string;
+ name: string;
+ origin_country: string;
+ }[];
+ spoken_languages: {
+ english_name: string;
+ iso_639_1: string;
+ name: string;
+ }[];
+ seasons: TmdbTvSeasonResult[];
+ status: string;
+ type: string;
+ vote_average: number;
+ vote_count: number;
+ aggregate_credits: {
+ cast: TmdbAggregateCreditCast[];
+ };
+ credits: {
+ crew: TmdbCreditCrew[];
+ };
+ external_ids: TmdbExternalIds;
+ keywords: {
+ results: TmdbKeyword[];
+ };
+ videos: TmdbVideoResult;
+}
+
+export interface TmdbVideoResult {
+ results: TmdbVideo[];
+}
+
+export interface TmdbKeyword {
+ id: number;
+ name: string;
+}
+
+export interface TmdbPersonDetail {
+ id: number;
+ name: string;
+ deathday: string;
+ known_for_department: string;
+ also_known_as?: string[];
+ gender: number;
+ biography: string;
+ popularity: string;
+ place_of_birth?: string;
+ profile_path?: string;
+ adult: boolean;
+ imdb_id?: string;
+ homepage?: string;
+}
+
+export interface TmdbPersonCredit {
+ id: number;
+ original_language: string;
+ episode_count: number;
+ overview: string;
+ origin_country: string[];
+ original_name: string;
+ vote_count: number;
+ name: string;
+ media_type?: string;
+ popularity: number;
+ credit_id: string;
+ backdrop_path?: string;
+ first_air_date: string;
+ vote_average: number;
+ genre_ids?: number[];
+ poster_path?: string;
+ original_title: string;
+ video?: boolean;
+ title: string;
+ adult: boolean;
+ release_date: string;
+}
+export interface TmdbPersonCreditCast extends TmdbPersonCredit {
+ character: string;
+}
+
+export interface TmdbPersonCreditCrew extends TmdbPersonCredit {
+ department: string;
+ job: string;
+}
+
+export interface TmdbPersonCombinedCredits {
+ id: number;
+ cast: TmdbPersonCreditCast[];
+ crew: TmdbPersonCreditCrew[];
+}
+
+export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
+ episodes: TmdbTvEpisodeResult[];
+ external_ids: TmdbExternalIds;
+}
+
+export interface TmdbCollection {
+ id: number;
+ name: string;
+ overview?: string;
+ poster_path?: string;
+ backdrop_path?: string;
+ parts: TmdbMovieResult[];
+}
diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index 1ba74961..3c090035 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -15,7 +15,8 @@ import { User } from './User';
import Media from './Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
-import TheMovieDb, { ANIME_KEYWORD_ID } from '../api/themoviedb';
+import TheMovieDb from '../api/themoviedb';
+import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
import RadarrAPI from '../api/radarr';
import logger from '../logger';
import SeasonRequest from './SeasonRequest';
diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts
index cab4f5ef..3ed5870d 100644
--- a/server/job/plexsync/index.ts
+++ b/server/job/plexsync/index.ts
@@ -1,10 +1,11 @@
import { getRepository } from 'typeorm';
import { User } from '../../entity/User';
import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../api/plexapi';
-import TheMovieDb, {
+import TheMovieDb from '../../api/themoviedb';
+import {
TmdbMovieDetails,
TmdbTvDetails,
-} from '../../api/themoviedb';
+} from '../../api/themoviedb/interfaces';
import Media from '../../entity/Media';
import { MediaStatus, MediaType } from '../../constants/media';
import logger from '../../logger';
diff --git a/server/job/sonarrsync/index.ts b/server/job/sonarrsync/index.ts
index 6ef45254..1fdbac0a 100644
--- a/server/job/sonarrsync/index.ts
+++ b/server/job/sonarrsync/index.ts
@@ -2,7 +2,8 @@ import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import { v4 as uuid } from 'uuid';
import SonarrAPI, { SonarrSeries } from '../../api/sonarr';
-import TheMovieDb, { TmdbTvDetails } from '../../api/themoviedb';
+import TheMovieDb from '../../api/themoviedb';
+import { TmdbTvDetails } from '../../api/themoviedb/interfaces';
import { MediaStatus, MediaType } from '../../constants/media';
import Media from '../../entity/Media';
import Season from '../../entity/Season';
diff --git a/server/lib/cache.ts b/server/lib/cache.ts
new file mode 100644
index 00000000..5624527f
--- /dev/null
+++ b/server/lib/cache.ts
@@ -0,0 +1,56 @@
+import NodeCache from 'node-cache';
+
+type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
+
+interface Cache {
+ id: AvailableCacheIds;
+ data: NodeCache;
+}
+
+const DEFAULT_TTL = 300;
+const DEFAULT_CHECK_PERIOD = 120;
+
+class CacheManager {
+ private availableCaches: Record = {
+ tmdb: {
+ id: 'tmdb',
+ data: new NodeCache({
+ stdTTL: DEFAULT_TTL,
+ checkperiod: DEFAULT_CHECK_PERIOD,
+ }),
+ },
+ radarr: {
+ id: 'radarr',
+ data: new NodeCache({
+ stdTTL: DEFAULT_TTL,
+ checkperiod: DEFAULT_CHECK_PERIOD,
+ }),
+ },
+ sonarr: {
+ id: 'sonarr',
+ data: new NodeCache({
+ stdTTL: DEFAULT_TTL,
+ checkperiod: DEFAULT_CHECK_PERIOD,
+ }),
+ },
+ rt: {
+ id: 'rt',
+ data: new NodeCache({
+ stdTTL: 21600, // 12 hours TTL
+ checkperiod: 60 * 30, // 30 minutes check period
+ }),
+ },
+ };
+
+ public getCache(id: AvailableCacheIds): Cache {
+ return this.availableCaches[id];
+ }
+
+ public getAllCaches(): Record {
+ return this.availableCaches;
+ }
+}
+
+const cacheManager = new CacheManager();
+
+export default cacheManager;
diff --git a/server/models/Collection.ts b/server/models/Collection.ts
index 64709502..48112849 100644
--- a/server/models/Collection.ts
+++ b/server/models/Collection.ts
@@ -1,4 +1,4 @@
-import { TmdbCollection } from '../api/themoviedb';
+import type { TmdbCollection } from '../api/themoviedb/interfaces';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import { mapMovieResult, MovieResult } from './Search';
diff --git a/server/models/Movie.ts b/server/models/Movie.ts
index c8639613..bfeb95ac 100644
--- a/server/models/Movie.ts
+++ b/server/models/Movie.ts
@@ -1,4 +1,4 @@
-import { TmdbMovieDetails } from '../api/themoviedb';
+import type { TmdbMovieDetails } from '../api/themoviedb/interfaces';
import {
ProductionCompany,
Genre,
diff --git a/server/models/Person.ts b/server/models/Person.ts
index 575e40cc..522a8e5e 100644
--- a/server/models/Person.ts
+++ b/server/models/Person.ts
@@ -1,8 +1,8 @@
-import {
+import type {
TmdbPersonCreditCast,
TmdbPersonCreditCrew,
TmdbPersonDetail,
-} from '../api/themoviedb';
+} from '../api/themoviedb/interfaces';
import Media from '../entity/Media';
export interface PersonDetail {
diff --git a/server/models/Search.ts b/server/models/Search.ts
index 7d347207..0dab4e58 100644
--- a/server/models/Search.ts
+++ b/server/models/Search.ts
@@ -2,7 +2,7 @@ import type {
TmdbMovieResult,
TmdbPersonResult,
TmdbTvResult,
-} from '../api/themoviedb';
+} from '../api/themoviedb/interfaces';
import { MediaType as MainMediaType } from '../constants/media';
import Media from '../entity/Media';
diff --git a/server/models/Tv.ts b/server/models/Tv.ts
index 5f84315f..420dca28 100644
--- a/server/models/Tv.ts
+++ b/server/models/Tv.ts
@@ -10,12 +10,12 @@ import {
Keyword,
mapVideos,
} from './common';
-import {
+import type {
TmdbTvEpisodeResult,
TmdbTvSeasonResult,
TmdbTvDetails,
TmdbSeasonWithEpisodes,
-} from '../api/themoviedb';
+} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import { Video } from './Movie';
diff --git a/server/models/common.ts b/server/models/common.ts
index dcd81934..d26cf637 100644
--- a/server/models/common.ts
+++ b/server/models/common.ts
@@ -1,11 +1,11 @@
-import {
+import type {
TmdbCreditCast,
TmdbAggregateCreditCast,
TmdbCreditCrew,
TmdbExternalIds,
TmdbVideo,
TmdbVideoResult,
-} from '../api/themoviedb';
+} from '../api/themoviedb/interfaces';
import { Video } from '../models/Movie';
diff --git a/server/utils/typeHelpers.ts b/server/utils/typeHelpers.ts
index e7d76d78..ca12ddf4 100644
--- a/server/utils/typeHelpers.ts
+++ b/server/utils/typeHelpers.ts
@@ -2,7 +2,7 @@ import type {
TmdbMovieResult,
TmdbTvResult,
TmdbPersonResult,
-} from '../api/themoviedb';
+} from '../api/themoviedb/interfaces';
export const isMovie = (
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx
index 3a331d89..172abdd9 100644
--- a/src/components/RequestModal/TvRequestModal.tsx
+++ b/src/components/RequestModal/TvRequestModal.tsx
@@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import useSWR from 'swr';
import { useToasts } from 'react-toast-notifications';
-import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb';
+import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants';
import axios from 'axios';
import {
MediaStatus,
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx
index c998e7b4..16977793 100644
--- a/src/components/TvDetails/index.tsx
+++ b/src/components/TvDetails/index.tsx
@@ -28,7 +28,7 @@ import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes';
import Head from 'next/head';
-import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb';
+import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants';
import ExternalLinkBlock from '../ExternalLinkBlock';
import { sortCrewPriority } from '../../utils/creditHelpers';
import { Crew } from '../../../server/models/common';
diff --git a/yarn.lock b/yarn.lock
index d269381c..445f1d33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4015,6 +4015,11 @@ clone-response@^1.0.2:
dependencies:
mimic-response "^1.0.0"
+clone@2.x:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+ integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
@@ -9469,6 +9474,13 @@ node-addon-api@^3.0.2:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
+node-cache@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
+ integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
+ dependencies:
+ clone "2.x"
+
node-emoji@^1.10.0, node-emoji@^1.8.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da"
From 3c5ae360fd179d794a78cc918fe97a09216ca6b2 Mon Sep 17 00:00:00 2001
From: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Date: Sun, 31 Jan 2021 04:51:33 -0500
Subject: [PATCH 06/66] fix(lang): translate language names & change zh-Hant
language code to zh-TW (#793)
---
.../Layout/LanguagePicker/index.tsx | 62 +++++++++----------
src/context/LanguageContext.tsx | 10 +--
src/pages/_app.tsx | 22 +++----
3 files changed, 47 insertions(+), 47 deletions(-)
diff --git a/src/components/Layout/LanguagePicker/index.tsx b/src/components/Layout/LanguagePicker/index.tsx
index c3677c67..db7a5c33 100644
--- a/src/components/Layout/LanguagePicker/index.tsx
+++ b/src/components/Layout/LanguagePicker/index.tsx
@@ -17,61 +17,61 @@ type AvailableLanguageObject = Record<
>;
const availableLanguages: AvailableLanguageObject = {
+ de: {
+ code: 'de',
+ display: 'Deutsch',
+ },
en: {
code: 'en',
display: 'English',
},
- ja: {
- code: 'ja',
- display: 'Japanese',
+ es: {
+ code: 'es',
+ display: 'Español',
},
fr: {
code: 'fr',
display: 'Français',
},
- 'nb-NO': {
- code: 'nb-NO',
- display: 'Norwegian Bokmål',
- },
- de: {
- code: 'de',
- display: 'German',
- },
- ru: {
- code: 'ru',
- display: 'Russian',
+ it: {
+ code: 'it',
+ display: 'Italiano',
},
nl: {
code: 'nl',
display: 'Nederlands',
},
- es: {
- code: 'es',
- display: 'Spanish',
- },
- it: {
- code: 'it',
- display: 'Italian',
+ 'nb-NO': {
+ code: 'nb-NO',
+ display: 'Norsk Bokmål',
},
'pt-BR': {
code: 'pt-BR',
- display: 'Portuguese (Brazil)',
+ display: 'Português (Brasil)',
},
'pt-PT': {
code: 'pt-PT',
- display: 'Portuguese (Portugal)',
- },
- sr: {
- code: 'sr',
- display: 'Serbian',
+ display: 'Português (Portugal)',
},
sv: {
code: 'sv',
- display: 'Swedish',
+ display: 'Svenska',
},
- 'zh-Hant': {
- code: 'zh-Hant',
- display: 'Chinese (Traditional)',
+ ru: {
+ code: 'ru',
+ display: 'pусский',
+ },
+ sr: {
+ code: 'sr',
+ display: 'српски језик',
+ },
+ ja: {
+ code: 'ja',
+ display: '日本語',
+ },
+ 'zh-TW': {
+ code: 'zh-TW',
+ display: '中文(臺灣)',
},
};
diff --git a/src/context/LanguageContext.tsx b/src/context/LanguageContext.tsx
index a4c95873..d55797ab 100644
--- a/src/context/LanguageContext.tsx
+++ b/src/context/LanguageContext.tsx
@@ -1,20 +1,20 @@
import React, { ReactNode } from 'react';
export type AvailableLocales =
+ | 'de'
| 'en'
+ | 'es'
+ | 'it'
| 'ja'
| 'fr'
| 'nb-NO'
- | 'de'
- | 'ru'
| 'nl'
- | 'es'
- | 'it'
| 'pt-BR'
| 'pt-PT'
+ | 'ru'
| 'sr'
| 'sv'
- | 'zh-Hant';
+ | 'zh-TW';
interface LanguageContextProps {
locale: AvailableLocales;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 2009f560..86f54615 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -20,31 +20,31 @@ import { SettingsProvider } from '../context/SettingsContext';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadLocaleData = (locale: AvailableLocales): Promise => {
switch (locale) {
- case 'ja':
- return import('../i18n/locale/ja.json');
- case 'fr':
- return import('../i18n/locale/fr.json');
- case 'nb-NO':
- return import('../i18n/locale/nb_NO.json');
case 'de':
return import('../i18n/locale/de.json');
- case 'ru':
- return import('../i18n/locale/ru.json');
- case 'nl':
- return import('../i18n/locale/nl.json');
case 'es':
return import('../i18n/locale/es.json');
+ case 'fr':
+ return import('../i18n/locale/fr.json');
case 'it':
return import('../i18n/locale/it.json');
+ case 'ja':
+ return import('../i18n/locale/ja.json');
+ case 'nb-NO':
+ return import('../i18n/locale/nb_NO.json');
+ case 'nl':
+ return import('../i18n/locale/nl.json');
case 'pt-BR':
return import('../i18n/locale/pt_BR.json');
case 'pt-PT':
return import('../i18n/locale/pt_PT.json');
+ case 'ru':
+ return import('../i18n/locale/ru.json');
case 'sr':
return import('../i18n/locale/sr.json');
case 'sv':
return import('../i18n/locale/sv.json');
- case 'zh-Hant':
+ case 'zh-TW':
return import('../i18n/locale/zh_Hant.json');
default:
return import('../i18n/locale/en.json');
From 996bd9f14ed0f56767892c169b071be4f0f628d0 Mon Sep 17 00:00:00 2001
From: sct
Date: Sun, 31 Jan 2021 13:11:12 +0000
Subject: [PATCH 07/66] feat(cache): add cache table and flush cache option to
settings
also increases tmdb cache times to about 6 hours (12 hours for detail requests)
---
overseerr-api.yml | 50 +++++
server/api/themoviedb/index.ts | 4 +-
server/interfaces/api/settingsInterfaces.ts | 12 ++
server/lib/cache.ts | 72 ++++---
server/routes/settings/index.ts | 27 +++
.../RequestModal/AdvancedRequester/index.tsx | 13 +-
src/components/Settings/SettingsJobs.tsx | 118 -----------
.../Settings/SettingsJobsCache/index.tsx | 198 ++++++++++++++++++
src/components/Settings/SettingsLayout.tsx | 6 +-
src/i18n/locale/en.json | 28 ++-
src/pages/settings/jobs.tsx | 2 +-
src/utils/numberHelpers.ts | 11 +
12 files changed, 363 insertions(+), 178 deletions(-)
delete mode 100644 src/components/Settings/SettingsJobs.tsx
create mode 100644 src/components/Settings/SettingsJobsCache/index.tsx
create mode 100644 src/utils/numberHelpers.ts
diff --git a/overseerr-api.yml b/overseerr-api.yml
index 209b5ac2..56ae1916 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -2024,6 +2024,56 @@ paths:
running:
type: boolean
example: false
+ /settings/cache:
+ get:
+ summary: Get a list of active caches
+ description: Retrieves a list of all active caches and their current stats.
+ tags:
+ - settings
+ responses:
+ '200':
+ description: Caches returned
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ example: cache-id
+ name:
+ type: string
+ example: cache name
+ stats:
+ type: object
+ properties:
+ hits:
+ type: number
+ misses:
+ type: number
+ keys:
+ type: number
+ ksize:
+ type: number
+ vsize:
+ type: number
+ /settings/cache/{cacheId}/flush:
+ get:
+ summary: Flush a specific cache
+ description: Flushes all data from the cache ID provided
+ tags:
+ - settings
+ parameters:
+ - in: path
+ name: cacheId
+ required: true
+ schema:
+ type: string
+ responses:
+ '204':
+ description: 'Flushed cache'
/settings/notifications:
get:
summary: Return notification settings
diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts
index bf716385..be1a629e 100644
--- a/server/api/themoviedb/index.ts
+++ b/server/api/themoviedb/index.ts
@@ -148,7 +148,7 @@ class TheMovieDb extends ExternalAPI {
append_to_response: 'credits,external_ids,videos',
},
},
- 900
+ 43200
);
return data;
@@ -174,7 +174,7 @@ class TheMovieDb extends ExternalAPI {
'aggregate_credits,credits,external_ids,keywords,videos',
},
},
- 900
+ 43200
);
return data;
diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts
index 52be4c4c..41abaf91 100644
--- a/server/interfaces/api/settingsInterfaces.ts
+++ b/server/interfaces/api/settingsInterfaces.ts
@@ -11,3 +11,15 @@ export interface PublicSettingsResponse {
series4kEnabled: boolean;
hideAvailable: boolean;
}
+
+export interface CacheItem {
+ id: string;
+ name: string;
+ stats: {
+ hits: number;
+ misses: number;
+ keys: number;
+ ksize: number;
+ vsize: number;
+ };
+}
diff --git a/server/lib/cache.ts b/server/lib/cache.ts
index 5624527f..aaf3bd44 100644
--- a/server/lib/cache.ts
+++ b/server/lib/cache.ts
@@ -1,45 +1,49 @@
import NodeCache from 'node-cache';
-type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
-
-interface Cache {
- id: AvailableCacheIds;
- data: NodeCache;
-}
+export type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
const DEFAULT_TTL = 300;
const DEFAULT_CHECK_PERIOD = 120;
+class Cache {
+ public id: AvailableCacheIds;
+ public data: NodeCache;
+ public name: string;
+
+ constructor(
+ id: AvailableCacheIds,
+ name: string,
+ options: { stdTtl?: number; checkPeriod?: number } = {}
+ ) {
+ this.id = id;
+ this.name = name;
+ this.data = new NodeCache({
+ stdTTL: options.stdTtl ?? DEFAULT_TTL,
+ checkperiod: options.checkPeriod ?? DEFAULT_CHECK_PERIOD,
+ });
+ }
+
+ public getStats() {
+ return this.data.getStats();
+ }
+
+ public flush(): void {
+ this.data.flushAll();
+ }
+}
+
class CacheManager {
private availableCaches: Record = {
- tmdb: {
- id: 'tmdb',
- data: new NodeCache({
- stdTTL: DEFAULT_TTL,
- checkperiod: DEFAULT_CHECK_PERIOD,
- }),
- },
- radarr: {
- id: 'radarr',
- data: new NodeCache({
- stdTTL: DEFAULT_TTL,
- checkperiod: DEFAULT_CHECK_PERIOD,
- }),
- },
- sonarr: {
- id: 'sonarr',
- data: new NodeCache({
- stdTTL: DEFAULT_TTL,
- checkperiod: DEFAULT_CHECK_PERIOD,
- }),
- },
- rt: {
- id: 'rt',
- data: new NodeCache({
- stdTTL: 21600, // 12 hours TTL
- checkperiod: 60 * 30, // 30 minutes check period
- }),
- },
+ tmdb: new Cache('tmdb', 'TMDb API', {
+ stdTtl: 21600,
+ checkPeriod: 60 * 30,
+ }),
+ radarr: new Cache('radarr', 'Radarr API'),
+ sonarr: new Cache('sonarr', 'Sonarr API'),
+ rt: new Cache('rt', 'Rotten Tomatoes API', {
+ stdTtl: 43200,
+ checkPeriod: 60 * 30,
+ }),
};
public getCache(id: AvailableCacheIds): Cache {
diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts
index 1d87c12e..61dabe21 100644
--- a/server/routes/settings/index.ts
+++ b/server/routes/settings/index.ts
@@ -16,6 +16,7 @@ import { SettingsAboutResponse } from '../../interfaces/api/settingsInterfaces';
import notificationRoutes from './notifications';
import sonarrRoutes from './sonarr';
import radarrRoutes from './radarr';
+import cacheManager, { AvailableCacheIds } from '../../lib/cache';
const settingsRoutes = Router();
@@ -273,6 +274,32 @@ settingsRoutes.get<{ jobId: string }>(
}
);
+settingsRoutes.get('/cache', (req, res) => {
+ const caches = cacheManager.getAllCaches();
+
+ return res.status(200).json(
+ Object.values(caches).map((cache) => ({
+ id: cache.id,
+ name: cache.name,
+ stats: cache.getStats(),
+ }))
+ );
+});
+
+settingsRoutes.get<{ cacheId: AvailableCacheIds }>(
+ '/cache/:cacheId/flush',
+ (req, res, next) => {
+ const cache = cacheManager.getCache(req.params.cacheId);
+
+ if (cache) {
+ cache.flush();
+ return res.status(204).send();
+ }
+
+ next({ status: 404, message: 'Cache does not exist.' });
+ }
+);
+
settingsRoutes.get(
'/initialize',
isAuthenticated(Permission.ADMIN),
diff --git a/src/components/RequestModal/AdvancedRequester/index.tsx b/src/components/RequestModal/AdvancedRequester/index.tsx
index 70f59eb9..0d5a4dea 100644
--- a/src/components/RequestModal/AdvancedRequester/index.tsx
+++ b/src/components/RequestModal/AdvancedRequester/index.tsx
@@ -7,18 +7,7 @@ import type {
ServiceCommonServerWithDetails,
} from '../../../../server/interfaces/api/serviceInterfaces';
import { defineMessages, useIntl } from 'react-intl';
-
-const formatBytes = (bytes: number, decimals = 2) => {
- if (bytes === 0) return '0 Bytes';
-
- const k = 1024;
- const dm = decimals < 0 ? 0 : decimals;
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
-
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
-};
+import { formatBytes } from '../../../utils/numberHelpers';
const messages = defineMessages({
advancedoptions: 'Advanced Options',
diff --git a/src/components/Settings/SettingsJobs.tsx b/src/components/Settings/SettingsJobs.tsx
deleted file mode 100644
index bf891fc5..00000000
--- a/src/components/Settings/SettingsJobs.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import React from 'react';
-import useSWR from 'swr';
-import LoadingSpinner from '../Common/LoadingSpinner';
-import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
-import Button from '../Common/Button';
-import Table from '../Common/Table';
-import Spinner from '../../assets/spinner.svg';
-import axios from 'axios';
-import { useToasts } from 'react-toast-notifications';
-import Badge from '../Common/Badge';
-
-const messages = defineMessages({
- jobname: 'Job Name',
- jobtype: 'Type',
- nextexecution: 'Next Execution',
- runnow: 'Run Now',
- canceljob: 'Cancel Job',
- jobstarted: '{jobname} started.',
- jobcancelled: '{jobname} cancelled.',
-});
-
-interface Job {
- id: string;
- name: string;
- type: 'process' | 'command';
- nextExecutionTime: string;
- running: boolean;
-}
-
-const SettingsJobs: React.FC = () => {
- const intl = useIntl();
- const { addToast } = useToasts();
- const { data, error, revalidate } = useSWR('/api/v1/settings/jobs', {
- refreshInterval: 5000,
- });
-
- if (!data && !error) {
- return ;
- }
-
- const runJob = async (job: Job) => {
- await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
- addToast(
- intl.formatMessage(messages.jobstarted, {
- jobname: job.name,
- }),
- {
- appearance: 'success',
- autoDismiss: true,
- }
- );
- revalidate();
- };
-
- const cancelJob = async (job: Job) => {
- await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
- addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
- appearance: 'error',
- autoDismiss: true,
- });
- revalidate();
- };
-
- return (
-
-
- {intl.formatMessage(messages.jobname)}
- {intl.formatMessage(messages.jobtype)}
- {intl.formatMessage(messages.nextexecution)}
-
-
-
- {data?.map((job) => (
-
-
-
- {job.running && }
- {job.name}
-
-
-
-
- {job.type}
-
-
-
-
-
-
-
-
- {job.running ? (
-
- ) : (
-
- )}
-
-
- ))}
-
-
- );
-};
-
-export default SettingsJobs;
diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx
new file mode 100644
index 00000000..72493b2b
--- /dev/null
+++ b/src/components/Settings/SettingsJobsCache/index.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import useSWR from 'swr';
+import LoadingSpinner from '../../Common/LoadingSpinner';
+import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
+import Button from '../../Common/Button';
+import Table from '../../Common/Table';
+import Spinner from '../../../assets/spinner.svg';
+import axios from 'axios';
+import { useToasts } from 'react-toast-notifications';
+import Badge from '../../Common/Badge';
+import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
+import { formatBytes } from '../../../utils/numberHelpers';
+
+const messages = defineMessages({
+ jobs: 'Jobs',
+ jobsDescription:
+ 'Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
+ jobname: 'Job Name',
+ jobtype: 'Type',
+ nextexecution: 'Next Execution',
+ runnow: 'Run Now',
+ canceljob: 'Cancel Job',
+ jobstarted: '{jobname} started.',
+ jobcancelled: '{jobname} cancelled.',
+ cache: 'Cache',
+ cacheDescription:
+ 'Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
+ cacheflushed: '{cachename} cache flushed.',
+ cachename: 'Cache Name',
+ cachehits: 'Hits',
+ cachemisses: 'Misses',
+ cachekeys: 'Total Keys',
+ cacheksize: 'Key Size',
+ cachevsize: 'Value Size',
+ flushcache: 'Flush Cache',
+});
+
+interface Job {
+ id: string;
+ name: string;
+ type: 'process' | 'command';
+ nextExecutionTime: string;
+ running: boolean;
+}
+
+const SettingsJobs: React.FC = () => {
+ const intl = useIntl();
+ const { addToast } = useToasts();
+ const { data, error, revalidate } = useSWR('/api/v1/settings/jobs', {
+ refreshInterval: 5000,
+ });
+ const { data: cacheData, revalidate: cacheRevalidate } = useSWR(
+ '/api/v1/settings/cache',
+ {
+ refreshInterval: 10000,
+ }
+ );
+
+ if (!data && !error) {
+ return ;
+ }
+
+ const runJob = async (job: Job) => {
+ await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
+ addToast(
+ intl.formatMessage(messages.jobstarted, {
+ jobname: job.name,
+ }),
+ {
+ appearance: 'success',
+ autoDismiss: true,
+ }
+ );
+ revalidate();
+ };
+
+ const cancelJob = async (job: Job) => {
+ await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
+ addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
+ appearance: 'error',
+ autoDismiss: true,
+ });
+ revalidate();
+ };
+
+ const flushCache = async (cache: CacheItem) => {
+ await axios.get(`/api/v1/settings/cache/${cache.id}/flush`);
+ addToast(
+ intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
+ {
+ appearance: 'success',
+ autoDismiss: true,
+ }
+ );
+ cacheRevalidate();
+ };
+
+ return (
+ <>
+
+
+ {intl.formatMessage(messages.jobs)}
+
+
+ {intl.formatMessage(messages.jobsDescription)}
+
+
+
+
+ {intl.formatMessage(messages.jobname)}
+ {intl.formatMessage(messages.jobtype)}
+ {intl.formatMessage(messages.nextexecution)}
+
+
+
+ {data?.map((job) => (
+
+
+
+ {job.running && }
+ {job.name}
+
+
+
+
+ {job.type}
+
+
+
+
+
+
+
+
+ {job.running ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+
+ {intl.formatMessage(messages.cache)}
+
+
+ {intl.formatMessage(messages.cacheDescription)}
+
+
+
+
+ {intl.formatMessage(messages.cachename)}
+ {intl.formatMessage(messages.cachehits)}
+ {intl.formatMessage(messages.cachemisses)}
+ {intl.formatMessage(messages.cachekeys)}
+ {intl.formatMessage(messages.cacheksize)}
+ {intl.formatMessage(messages.cachevsize)}
+
+
+
+ {cacheData?.map((cache) => (
+
+ {cache.name}
+ {cache.stats.hits}
+ {cache.stats.misses}
+ {cache.stats.keys}
+ {formatBytes(cache.stats.ksize)}
+ {formatBytes(cache.stats.vsize)}
+
+
+
+
+ ))}
+
+
+ >
+ );
+};
+
+export default SettingsJobs;
diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx
index 13bc1801..8ce65432 100644
--- a/src/components/Settings/SettingsLayout.tsx
+++ b/src/components/Settings/SettingsLayout.tsx
@@ -9,7 +9,7 @@ const messages = defineMessages({
menuServices: 'Services',
menuNotifications: 'Notifications',
menuLogs: 'Logs',
- menuJobs: 'Jobs',
+ menuJobs: 'Jobs & Cache',
menuAbout: 'About',
});
@@ -106,7 +106,7 @@ const SettingsLayout: React.FC = ({ children }) => {
)?.route
}
aria-label="Selected tab"
- className="bg-gray-800 text-white mt-1 rounded-md form-select block w-full pl-3 pr-10 py-2 text-base leading-6 border-gray-700 focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5 transition ease-in-out duration-150"
+ className="block w-full py-2 pl-3 pr-10 mt-1 text-base leading-6 text-white transition duration-150 ease-in-out bg-gray-800 border-gray-700 rounded-md form-select focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5"
>
{settingsRoutes.map((route, index) => (
{
-
>
From a3b00c3458b868506d4158fb24f0369fa5daefc5 Mon Sep 17 00:00:00 2001
From: sct
Date: Mon, 1 Feb 2021 12:25:31 +0000
Subject: [PATCH 20/66] fix(frontend): set 4k status on RequestItem when
request is for 4k
---
src/components/RequestList/RequestItem/index.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx
index 435a6eb8..67e4b9a0 100644
--- a/src/components/RequestList/RequestItem/index.tsx
+++ b/src/components/RequestList/RequestItem/index.tsx
@@ -201,6 +201,7 @@ const RequestItem: React.FC = ({
] ?? []
).length > 0
}
+ is4k={requestData.is4k}
/>
)}
From 3052f12c91b3ce86128324e3698fff61bbce3f2a Mon Sep 17 00:00:00 2001
From: sct
Date: Tue, 2 Feb 2021 00:45:21 +0000
Subject: [PATCH 21/66] fix(api): filter out adult content from combined
credits
---
server/routes/person.ts | 32 ++++++++++++++++++--------------
1 file changed, 18 insertions(+), 14 deletions(-)
diff --git a/server/routes/person.ts b/server/routes/person.ts
index add0b0f1..7b8d90c4 100644
--- a/server/routes/person.ts
+++ b/server/routes/person.ts
@@ -42,24 +42,28 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
);
return res.status(200).json({
- cast: combinedCredits.cast.map((result) =>
- mapCastCredits(
- result,
- castMedia.find(
- (med) =>
- med.tmdbId === result.id && med.mediaType === result.media_type
+ cast: combinedCredits.cast
+ .map((result) =>
+ mapCastCredits(
+ result,
+ castMedia.find(
+ (med) =>
+ med.tmdbId === result.id && med.mediaType === result.media_type
+ )
)
)
- ),
- crew: combinedCredits.crew.map((result) =>
- mapCrewCredits(
- result,
- crewMedia.find(
- (med) =>
- med.tmdbId === result.id && med.mediaType === result.media_type
+ .filter((item) => !item.adult),
+ crew: combinedCredits.crew
+ .map((result) =>
+ mapCrewCredits(
+ result,
+ crewMedia.find(
+ (med) =>
+ med.tmdbId === result.id && med.mediaType === result.media_type
+ )
)
)
- ),
+ .filter((item) => !item.adult),
id: combinedCredits.id,
});
});
From bfe25d9755598bd40d92c0074082bbaf626dfa55 Mon Sep 17 00:00:00 2001
From: sct
Date: Tue, 2 Feb 2021 01:16:05 +0000
Subject: [PATCH 22/66] docs(api): deploy api docs to github pages [skip ci]
---
.github/workflows/deploy_docs.yml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 .github/workflows/deploy_docs.yml
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
new file mode 100644
index 00000000..b0aad89d
--- /dev/null
+++ b/.github/workflows/deploy_docs.yml
@@ -0,0 +1,22 @@
+name: Deploy API Docs
+
+on:
+ push:
+ branches:
+ - develop
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Generate Swagger UI
+ uses: Legion2/swagger-ui-action@v1
+ with:
+ output: swagger-ui
+ spec-file: overseerr-api.yml
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: swagger-ui
From e34fbf72fda34d69b9f25563fa81f88b3c20912a Mon Sep 17 00:00:00 2001
From: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Date: Tue, 2 Feb 2021 02:20:05 -0500
Subject: [PATCH 23/66] fix(ui): uniform-size checkboxes, vertically-aligned
form labels, and fixes for other UI imperfections/inconsistencies (#737)
---
src/components/Common/Alert/index.tsx | 2 +-
src/components/Common/Header/index.tsx | 8 +-
src/components/Common/List/index.tsx | 12 +-
src/components/Common/Modal/index.tsx | 2 +-
src/components/Common/Table/index.tsx | 2 +-
src/components/Discover/DiscoverMovies.tsx | 8 +-
src/components/Discover/DiscoverTv.tsx | 8 +-
src/components/Discover/Trending.tsx | 8 +-
src/components/Discover/Upcoming.tsx | 8 +-
.../Layout/LanguagePicker/index.tsx | 6 +-
src/components/Layout/Sidebar/index.tsx | 4 +-
src/components/Layout/index.tsx | 4 +-
src/components/Login/LocalLogin.tsx | 20 +-
.../MovieDetails/MovieCast/index.tsx | 20 +-
.../MovieDetails/MovieCrew/index.tsx | 20 +-
.../MovieDetails/MovieRecommendations.tsx | 24 +-
src/components/MovieDetails/MovieSimilar.tsx | 24 +-
src/components/MovieDetails/index.tsx | 80 +--
.../NotificationType/index.tsx | 5 +-
src/components/PermissionOption/index.tsx | 15 +-
.../RequestList/RequestItem/index.tsx | 28 +-
src/components/RequestList/index.tsx | 18 +-
.../RequestModal/AdvancedRequester/index.tsx | 8 +-
.../RequestModal/TvRequestModal.tsx | 4 +-
src/components/Search/index.tsx | 4 +-
src/components/Settings/CopyButton.tsx | 2 +-
.../Notifications/NotificationsDiscord.tsx | 63 +-
.../Notifications/NotificationsEmail.tsx | 149 ++---
.../NotificationsPushover/index.tsx | 79 +--
.../NotificationsSlack/index.tsx | 65 +-
.../Notifications/NotificationsTelegram.tsx | 77 +--
.../NotificationsWebhook/index.tsx | 72 +--
src/components/Settings/RadarrModal/index.tsx | 183 ++----
.../Settings/SettingsAbout/Releases/index.tsx | 70 +--
.../Settings/SettingsAbout/index.tsx | 8 +-
.../Settings/SettingsJobsCache/index.tsx | 181 +++---
src/components/Settings/SettingsLayout.tsx | 1 -
src/components/Settings/SettingsMain.tsx | 143 ++---
.../Settings/SettingsNotifications.tsx | 39 +-
src/components/Settings/SettingsPlex.tsx | 586 +++++++++---------
src/components/Settings/SettingsServices.tsx | 27 +-
src/components/Settings/SonarrModal/index.tsx | 198 ++----
src/components/Setup/LoginWithPlex.tsx | 4 +-
src/components/Setup/index.tsx | 4 +-
src/components/TvDetails/TvCast/index.tsx | 22 +-
src/components/TvDetails/TvCrew/index.tsx | 20 +-
.../TvDetails/TvRecommendations.tsx | 24 +-
src/components/TvDetails/TvSimilar.tsx | 24 +-
src/components/TvDetails/index.tsx | 22 +-
src/components/UserEdit/index.tsx | 193 +++---
src/components/UserList/BulkEditModal.tsx | 35 +-
src/components/UserList/index.tsx | 50 +-
src/i18n/globalMessages.ts | 1 +
src/i18n/locale/en.json | 16 +-
src/styles/globals.css | 72 +++
55 files changed, 1230 insertions(+), 1542 deletions(-)
diff --git a/src/components/Common/Alert/index.tsx b/src/components/Common/Alert/index.tsx
index 15d63eae..1b4bd854 100644
--- a/src/components/Common/Alert/index.tsx
+++ b/src/components/Common/Alert/index.tsx
@@ -77,7 +77,7 @@ const Alert: React.FC = ({ title, children, type }) => {
}
return (
-
+
{design.svg}
diff --git a/src/components/Common/Header/index.tsx b/src/components/Common/Header/index.tsx
index 3c743a26..77dbd159 100644
--- a/src/components/Common/Header/index.tsx
+++ b/src/components/Common/Header/index.tsx
@@ -11,14 +11,14 @@ const Header: React.FC
= ({
subtext,
}) => {
return (
-
+
-
-
+
+
{children}
- {subtext && {subtext}
}
+ {subtext && {subtext}
}
);
diff --git a/src/components/Common/List/index.tsx b/src/components/Common/List/index.tsx
index 89ba88e1..1f723c65 100644
--- a/src/components/Common/List/index.tsx
+++ b/src/components/Common/List/index.tsx
@@ -8,8 +8,8 @@ interface ListItemProps {
const ListItem: React.FC
= ({ title, children }) => {
return (
-
{title}
-
+ {title}
+
{children}
@@ -25,12 +25,10 @@ const List: React.FC = ({ title, subTitle, children }) => {
return (
<>
-
{title}
- {subTitle && (
-
{subTitle}
- )}
+
{title}
+ {subTitle &&
{subTitle}
}
-
+
>
diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx
index 18b6c074..77abc646 100644
--- a/src/components/Common/Modal/index.tsx
+++ b/src/components/Common/Modal/index.tsx
@@ -112,7 +112,7 @@ const Modal: React.FC
= ({
)}
{title && (
diff --git a/src/components/Common/Table/index.tsx b/src/components/Common/Table/index.tsx
index f9f1040f..dd15b9e6 100644
--- a/src/components/Common/Table/index.tsx
+++ b/src/components/Common/Table/index.tsx
@@ -3,7 +3,7 @@ import { withProperties } from '../../../utils/typeHelpers';
const TBody: React.FC = ({ children }) => {
return (
-
{children}
+
{children}
);
};
diff --git a/src/components/Discover/DiscoverMovies.tsx b/src/components/Discover/DiscoverMovies.tsx
index 8491aa30..243edf7a 100644
--- a/src/components/Discover/DiscoverMovies.tsx
+++ b/src/components/Discover/DiscoverMovies.tsx
@@ -68,9 +68,11 @@ const DiscoverMovies: React.FC = () => {
return (
<>
-
+
+
+
{
return (
<>
-
+
+
+
{
return (
<>
-
+
+
+
{
return (
<>
-
+
+
+
{
leaveTo="transform opacity-0 scale-95"
>
-
+
-
+
{router.pathname === '/' && hasPermission(Permission.ADMIN) && (
-
+
{loginError && (
-
{loginError}
+
{loginError}
)}
-
+
)}
diff --git a/src/components/NotificationTypeSelector/NotificationType/index.tsx b/src/components/NotificationTypeSelector/NotificationType/index.tsx
index 71b2a849..85224717 100644
--- a/src/components/NotificationTypeSelector/NotificationType/index.tsx
+++ b/src/components/NotificationTypeSelector/NotificationType/index.tsx
@@ -23,12 +23,11 @@ const NotificationType: React.FC
= ({
: ''
}`}
>
-
+
= ({
}
/>
-
+
diff --git a/src/components/PermissionOption/index.tsx b/src/components/PermissionOption/index.tsx
index f8c0677b..b6de4e39 100644
--- a/src/components/PermissionOption/index.tsx
+++ b/src/components/PermissionOption/index.tsx
@@ -41,12 +41,11 @@ const PermissionOption: React.FC
= ({
: ''
}`}
>
-
+
= ({
}
/>
-
-
{(option.children ?? []).map((child) => (
-
+
= ({
if (!title && !error) {
return (
-
+
|
);
@@ -110,14 +110,14 @@ const RequestItem: React.FC = ({
if (!title || !requestData) {
return (
-
+
|
);
}
return (
-
+
= ({
{requestData.modifiedBy ? (
- {requestData.modifiedBy.displayName}
- (
-
- )
+ {requestData.modifiedBy.displayName}
+
+ (
+
+ )
+
) : (
N/A
diff --git a/src/components/RequestList/index.tsx b/src/components/RequestList/index.tsx
index 7b682ac5..1934e594 100644
--- a/src/components/RequestList/index.tsx
+++ b/src/components/RequestList/index.tsx
@@ -56,7 +56,7 @@ const RequestList: React.FC = () => {
<>
{intl.formatMessage(messages.requests)}
-
+