Merge pull request #18 from psyko-gh/better-errors

Various updates
This commit is contained in:
psyko-gh
2024-08-30 23:05:40 +02:00
committed by GitHub
35 changed files with 724 additions and 392 deletions

40
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,40 @@
# Troubleshooting
### Error while connecting to Overseerr
The application is not able to connect to Overseerr/Jellyseerr.
---
**Request failed with status code 404**
Most likely your `urlApi` is not configured properly.
You can test it by opening it in a browser. It should display something like:
```json
{
"api": "Overseerr API",
"version": "1.0"
}
```
For proper configuration, please refer to [the Configuration page](configuration.md)
---
**Request failed with status code 403**
Your credentials are not correct.
Make sure you're using the email of your Overseerr user in the `overseerr/user` field of `settings.yaml` or in your environment variable.
For proper configuration, please refer to [the Configuration page](configuration.md)
---
Should you encounter another issue, please open a [Github issue](https://github.com/psyko-gh/overcrawlrr/issues) including any element that can be helpful.
!!! warning Be careful about posting logs
Overcrawlrr logs can display the URL your are using for Overseerr/Jellyseerr/Plex. Make sure to redact them if you don't want to make them public

View File

@@ -30,6 +30,8 @@ nav:
- Rules:
- rulesets.md
- predicates.md
- Troubleshooting:
- troubleshooting.md
- Additional:
- additional.md
markdown_extensions:

60
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@types/js-yaml": "^4.0.5",
"ajv": "^8.17.1",
"axios": "^1.4.0",
"better-ajv-errors": "^1.2.0",
"cookie": "^0.5.0",
"cors": "^2.8.5",
"croner": "^6.0.6",
@@ -29,6 +30,7 @@
"@commitlint/cli": "^19.4.0",
"@commitlint/config-conventional": "^19.2.2",
"@eslint/js": "^9.9.0",
"@semantic-release/github": "^10.1.7",
"@types/cookie": "^0.5.1",
"@types/cors": "^2.8.13",
"@types/eslint__js": "^8.42.3",
@@ -65,7 +67,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.24.7",
"picocolors": "^1.0.0"
@@ -233,7 +234,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -264,7 +264,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
@@ -279,7 +278,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -291,7 +289,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -305,7 +302,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@@ -313,14 +309,12 @@
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
@@ -329,7 +323,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -338,7 +331,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -1964,6 +1956,14 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/momoa": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz",
"integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==",
"engines": {
"node": ">=10.10.0"
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
@@ -3801,7 +3801,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -4004,6 +4003,24 @@
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
"dev": true
},
"node_modules/better-ajv-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-1.2.0.tgz",
"integrity": "sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==",
"dependencies": {
"@babel/code-frame": "^7.16.0",
"@humanwhocodes/momoa": "^2.0.2",
"chalk": "^4.1.2",
"jsonpointer": "^5.0.0",
"leven": "^3.1.0 < 4"
},
"engines": {
"node": ">= 12.13.0"
},
"peerDependencies": {
"ajv": "4.11.8 - 8"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -4193,7 +4210,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -4421,7 +4437,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -6335,7 +6350,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -7613,8 +7627,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -7701,6 +7714,14 @@
"node >= 0.2.0"
]
},
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/JSONStream": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
@@ -7744,7 +7765,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -11213,8 +11233,7 @@
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -12577,7 +12596,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},

View File

@@ -46,6 +46,7 @@
"@types/js-yaml": "^4.0.5",
"ajv": "^8.17.1",
"axios": "^1.4.0",
"better-ajv-errors": "^1.2.0",
"cookie": "^0.5.0",
"cors": "^2.8.5",
"croner": "^6.0.6",

View File

@@ -92,226 +92,151 @@
"predicate": {
"$id": "#/definitions/predicate",
"type": "object",
"oneOf": [
{ "$ref": "#/definitions/orPredicate" },
{ "$ref": "#/definitions/andPredicate" },
{ "$ref": "#/definitions/notPredicate" },
{ "$ref": "#/definitions/adultPredicate" },
{ "$ref": "#/definitions/agePredicate" },
{ "$ref": "#/definitions/scorePredicate" },
{ "$ref": "#/definitions/genrePredicate" },
{ "$ref": "#/definitions/keywordPredicate" },
{ "$ref": "#/definitions/watchProviderPredicate" },
{ "$ref": "#/definitions/voteCountPredicate" },
{ "$ref": "#/definitions/castPredicate" },
{ "$ref": "#/definitions/crewPredicate" },
{ "$ref": "#/definitions/releasedPredicate" },
{ "$ref": "#/definitions/runtimePredicate" },
{ "$ref": "#/definitions/productionCompanyPredicate" },
{ "$ref": "#/definitions/originalLanguagePredicate" },
{ "$ref": "#/definitions/statusPredicate" }
]
"properties": {
"or": { "$ref": "#/definitions/orPredicate" },
"and": { "$ref": "#/definitions/andPredicate" },
"not": { "$ref": "#/definitions/notPredicate" },
"adult": { "$ref": "#/definitions/adultPredicate" },
"age": { "$ref": "#/definitions/agePredicate" },
"score": { "$ref": "#/definitions/scorePredicate" },
"genre": { "$ref": "#/definitions/genrePredicate" },
"keyword": { "$ref": "#/definitions/keywordPredicate" },
"watchProviders": { "$ref": "#/definitions/watchProvidersPredicate" },
"voteCount": { "$ref": "#/definitions/voteCountPredicate" },
"cast": { "$ref": "#/definitions/castPredicate" },
"crew": { "$ref": "#/definitions/crewPredicate" },
"released": { "$ref": "#/definitions/releasedPredicate" },
"runtime": { "$ref": "#/definitions/runtimePredicate" },
"productionCompany": { "$ref": "#/definitions/productionCompanyPredicate" },
"originalLanguage": { "$ref": "#/definitions/originalLanguagePredicate" },
"status": { "$ref": "#/definitions/statusPredicate" }
},
"additionalProperties": false
},
"orPredicate": {
"$id": "#/definitions/orPredicate",
"type": "object",
"required": ["or"],
"properties": {
"or": { "type": "array", "items": { "$ref": "#/definitions/predicate" }}
}
"type": "array",
"items": { "$ref": "#/definitions/predicate" }
},
"andPredicate": {
"$id": "#/definitions/andPredicate",
"type": "object",
"required": ["and"],
"properties": {
"and": { "type": "array", "items": { "$ref": "#/definitions/predicate" }}
}
"type": "array",
"items": { "$ref": "#/definitions/predicate" }
},
"notPredicate": {
"$id": "#/definitions/notPredicate",
"type": "object",
"required": ["not"],
"properties": {
"not": { "type": "array", "items": { "$ref": "#/definitions/predicate" }}
}
"type": "array",
"items": { "$ref": "#/definitions/predicate" }
},
"adultPredicate": {
"$id": "#/definitions/adultPredicate",
"type": "object",
"required": ["adult"],
"properties": {
"adult": { "type": "string", "enum": ["yes", "no"]}
}
"type": "string",
"enum": ["yes", "no"]
},
"agePredicate": {
"$id": "#/definitions/agePredicate",
"type": "object",
"required": ["age"],
"properties": {
"age": { "type": "string" }
}
"type": "string"
},
"releasedPredicate": {
"$id": "#/definitions/releasedPredicate",
"type": "object",
"required": ["released"],
"properties": {
"released": { "type": "string", "enum": ["yes", "no"]}
}
"type": "string",
"enum": ["yes", "no"]
},
"scorePredicate": {
"$id": "#/definitions/scorePredicate",
"type": "object",
"required": ["score"],
"properties": {
"score": { "type": "string" }
}
"type": "string"
},
"voteCountPredicate": {
"$id": "#/definitions/voteCountPredicate",
"type": "object",
"required": ["voteCount"],
"properties": {
"voteCount": { "type": "string" }
}
"type": "string"
},
"genrePredicate": {
"$id": "#/definitions/genrePredicate",
"type": "object",
"required": ["genre"],
"properties": {
"genre": {
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
},
"keywordPredicate": {
"$id": "#/definitions/keywordPredicate",
"type": "object",
"required": ["keyword"],
"properties": {
"keyword": {
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
},
"watchProviderPredicate": {
"$id": "#/definitions/watchProviderPredicate",
"watchProvidersPredicate": {
"$id": "#/definitions/watchProvidersPredicate",
"type": "object",
"required": ["watchProviders"],
"properties": {
"watchProviders": {
"type": "object",
"properties": {
"region": { "type": "string"},
"names": { "type": "array", "items": { "type": "string" }}
}
}
"region": { "type": "string"},
"names": { "type": "array", "items": { "type": "string" }}
}
},
"castPredicate": {
"$id": "#/definitions/castPredicate",
"type": "object",
"required": ["cast"],
"properties": {
"cast": {
"oneOf": [
{ "type": "array", "items": { "type": "string" }},
{
"type": "object",
"properties": {
"voice": { "type": "string", "enum": ["include", "exclude"]},
"names": { "type": "array", "items": {"type": "string" }}
}
}
]
"oneOf": [
{ "type": "array", "items": { "type": "string" }},
{
"type": "object",
"properties": {
"voice": { "type": "string", "enum": ["include", "exclude"]},
"names": { "type": "array", "items": {"type": "string" }}
}
}
}
]
},
"crewPredicate": {
"$id": "#/definitions/crewPredicate",
"type": "object",
"required": ["crew"],
"properties": {
"crew": {
"oneOf": [
{ "type": "array", "items": { "type": "string" }},
{
"type": "object",
"properties": {
"job": { "type": "string"},
"names": { "type": "array", "items": {"type": "string" }}
}
}
]
"oneOf": [
{ "type": "array", "items": { "type": "string" }},
{
"type": "object",
"properties": {
"job": { "type": "string"},
"names": { "type": "array", "items": {"type": "string" }}
}
}
}
]
},
"productionCompanyPredicate": {
"$id": "#/definitions/productionCompanyPredicate",
"type": "object",
"required": ["productionCompany"],
"properties": {
"productionCompany": { "type": "array", "items": {"type": "string" }}
}
"type": "array",
"items": {"type": "string" }
},
"runtimePredicate": {
"$id": "#/definitions/runtimePredicate",
"type": "object",
"required": ["runtime"],
"properties": {
"runtime": { "type": "string" }
}
"type": "string"
},
"originalLanguagePredicate": {
"$id": "#/definitions/originalLanguagePredicate",
"type": "object",
"required": ["originalLanguage"],
"properties": {
"originalLanguage": {
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
},
"statusPredicate": {
"$id": "#/definitions/statusPredicate",
"type": "object",
"required": ["status"],
"properties": {
"status": {
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" }}
]
}
}
}

View File

@@ -15,7 +15,7 @@ interface HttpApiOptions {
class HttpApi {
protected axios: AxiosInstance;
private cookie: string;
private baseUrl: string;
protected baseUrl: string;
private cache?: NodeCache;
constructor(baseUrl: string, params: Record<string, unknown>, options: HttpApiOptions = {}, debug: boolean = false) {

View File

@@ -2,7 +2,7 @@ import HttpApi from '@core/api/httpApi';
import { getSettings } from '@core/lib/settings';
import logger from '@core/log';
import { AuthResponse, MediaRequest, MovieDetails, MovieResult, OverseerrRequestsResult, OverseerrResult } from '@core/api/overseerr/interfaces';
import { success } from '@core/lib/utils';
import { getErrorMessage, success } from '@core/lib/utils';
class OverseerrApi extends HttpApi {
private overseerrUser?: string;
@@ -16,15 +16,13 @@ class OverseerrApi extends HttpApi {
public test = async () => {
try {
logger.info('Testing overseerr connection...');
logger.info('Testing overseerr connection using:');
logger.info(` - url: ${this.baseUrl}`);
logger.info(` - user: ${this.overseerrUser}`);
await this.auth();
logger.info(success(' Overseerr connection successful'));
} catch (e: unknown) {
if (e instanceof Error) {
logger.error(`${e.message}`);
return false;
}
logger.error('Error while testing Overseerr connection. Verify URL and credentials: ', e);
logger.error(`Error while testing Overseerr connection. Verify URL and credentials: ${getErrorMessage(e)}`);
}
};

View File

@@ -4,7 +4,7 @@ import logger from '@core/log';
import { Parser } from 'xml2js';
import type { AxiosRequestConfig } from 'axios';
import { PlexSectionDirectory, PlexSectionsResponse, PlexVideo, PlexVideosResponse } from '@core/api/plex/interfaces';
import { isFulfilled, success } from '@core/lib/utils';
import { getErrorMessage, isFulfilled, success } from '@core/lib/utils';
class PlexApi extends HttpApi {
private xmlParser;
@@ -49,15 +49,12 @@ class PlexApi extends HttpApi {
public test = async () => {
try {
logger.info('Testing Plex connection...');
logger.info('Testing Plex connection using:');
logger.info(` - url: ${this.baseUrl}`);
await this.get('/');
logger.info(success(' Plex connection successful'));
} catch (e: unknown) {
if (e instanceof Error) {
logger.error(`${e.message}`);
return false;
}
logger.error('Error while testing Plex connection. Verify URL and credentials: ', e);
logger.error(`Error while testing Plex connection. Verify URL and credentials: ${getErrorMessage(e)}`);
}
};

View File

@@ -54,5 +54,5 @@ const corsOptions = {
app.use(cors(corsOptions));
app.use('/api', apiRouter);
app.listen(port, () => {
return logger.info(`Server is listening at http://localhost:${port} `);
return logger.info(`Overcrawlrr is listening at http://localhost:${port} `);
});

View File

@@ -35,6 +35,7 @@ const jobDefinitionsGetter = (settings: Settings) => [
const jobs: Cron[] = [];
export function registerCrons(settings: Settings) {
logger.info('Configuring jobs...');
jobs.forEach((j) => j.stop());
jobDefinitionsGetter(settings).forEach((jobDefinition: JobDefinition) => {
if (!jobDefinition.isEnabled()) {

View File

@@ -0,0 +1,97 @@
import { fromHumanReadableScore } from '@core/lib/rules/predicate/score';
import { fromHumanReadableNumber } from '@core/lib/rules/predicate/number';
import { fromHumanReadableDuration } from '@core/lib/rules/predicate/time';
import { fromHumanReadableBoolean } from '@core/lib/rules/predicate/boolean';
describe('fromHumanReadableNumber', () => {
it('should match', async () => {
expect(fromHumanReadableNumber('above 7')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableNumber('above 7.0')).toEqual({ threshold: 7.0, operator: 'gt' });
expect(fromHumanReadableNumber('greater than 47')).toEqual({ threshold: 47, operator: 'gt' });
expect(fromHumanReadableNumber('greater than 5768.5')).toEqual({ threshold: 5768.5, operator: 'gt' });
expect(fromHumanReadableNumber('below 123')).toEqual({ threshold: 123, operator: 'lt' });
expect(fromHumanReadableNumber('below 32.25')).toEqual({ threshold: 32.25, operator: 'lt' });
expect(fromHumanReadableNumber('under 47')).toEqual({ threshold: 47, operator: 'lt' });
expect(fromHumanReadableNumber('under 78.9')).toEqual({ threshold: 78.9, operator: 'lt' });
expect(fromHumanReadableNumber('less than 1234')).toEqual({ threshold: 1234, operator: 'lt' });
expect(fromHumanReadableNumber('less than 45.86')).toEqual({ threshold: 45.86, operator: 'lt' });
});
});
describe('fromHumanReadableScore', () => {
it('should match', async () => {
expect(fromHumanReadableScore('above 7')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableScore('above 7.0')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableScore('above 7.05')).toEqual({ threshold: 7.05, operator: 'gt' });
expect(fromHumanReadableScore('above 7/10')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableScore('above 7/100')).toEqual({ threshold: 0.7, operator: 'gt' });
expect(fromHumanReadableScore('above 77.0')).toEqual({ threshold: 77, operator: 'gt' });
expect(fromHumanReadableScore('greater than 7')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableScore('greater than 7.0')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableScore('greater than 7.05')).toEqual({ threshold: 7.05, operator: 'gt' });
expect(fromHumanReadableScore('greater than 7/10')).toEqual({ threshold: 7, operator: 'gt' });
expect(fromHumanReadableScore('greater than 77.0')).toEqual({ threshold: 77, operator: 'gt' });
expect(fromHumanReadableScore('below 4')).toEqual({ threshold: 4, operator: 'lt' });
expect(fromHumanReadableScore('below 5.0')).toEqual({ threshold: 5.0, operator: 'lt' });
expect(fromHumanReadableScore('below 5.3')).toEqual({ threshold: 5.3, operator: 'lt' });
expect(fromHumanReadableScore('below 85/100')).toEqual({ threshold: 8.5, operator: 'lt' });
expect(fromHumanReadableScore('below 77.0')).toEqual({ threshold: 77, operator: 'lt' });
// Invalid format. Using numerator
expect(fromHumanReadableScore('below 85/10')).toEqual({ threshold: 85, operator: 'lt' });
});
});
describe('fromHumanReadableDuration', () => {
it('should match', async () => {
expect(fromHumanReadableDuration('less than 1 minute')).toEqual({ threshold: 60, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 minutes')).toEqual({ threshold: 60, operator: 'lt' });
expect(fromHumanReadableDuration('less than 100 minute')).toEqual({ threshold: 100 * 60, operator: 'lt' });
expect(fromHumanReadableDuration('more than 1 minute')).toEqual({ threshold: 60, operator: 'gt' });
expect(fromHumanReadableDuration('more than 1 minutes')).toEqual({ threshold: 60, operator: 'gt' });
expect(fromHumanReadableDuration('more than 100 minute')).toEqual({ threshold: 100 * 60, operator: 'gt' });
expect(fromHumanReadableDuration('less than 1 hour')).toEqual({ threshold: 60 * 60, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 hours')).toEqual({ threshold: 60 * 60, operator: 'lt' });
expect(fromHumanReadableDuration('more than 2.5 hours')).toEqual({ threshold: 150 * 60, operator: 'gt' });
expect(fromHumanReadableDuration('less than 2.5 hour')).toEqual({ threshold: 150 * 60, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 day')).toEqual({ threshold: 60 * 60 * 24, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 days')).toEqual({ threshold: 60 * 60 * 24, operator: 'lt' });
expect(fromHumanReadableDuration('more than 2.5 days')).toEqual({ threshold: 60 * 60 * 60, operator: 'gt' });
expect(fromHumanReadableDuration('less than 2.5 day')).toEqual({ threshold: 60 * 60 * 60, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 week')).toEqual({ threshold: 60 * 60 * 24 * 7, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 weeks')).toEqual({ threshold: 60 * 60 * 24 * 7, operator: 'lt' });
expect(fromHumanReadableDuration('more than 2.5 weeks')).toEqual({ threshold: 60 * 60 * 24 * (7 * 2.5), operator: 'gt' });
expect(fromHumanReadableDuration('less than 2.5 week')).toEqual({ threshold: 60 * 60 * 24 * (7 * 2.5), operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 month')).toEqual({ threshold: 60 * 60 * 24 * 30, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 months')).toEqual({ threshold: 60 * 60 * 24 * 30, operator: 'lt' });
expect(fromHumanReadableDuration('more than 2.5 months')).toEqual({ threshold: 60 * 60 * 24 * 75, operator: 'gt' });
expect(fromHumanReadableDuration('less than 2.5 month')).toEqual({ threshold: 60 * 60 * 24 * 75, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 year')).toEqual({ threshold: 60 * 60 * 24 * 365, operator: 'lt' });
expect(fromHumanReadableDuration('less than 1 years')).toEqual({ threshold: 60 * 60 * 24 * 365, operator: 'lt' });
expect(fromHumanReadableDuration('more than 2.5 years')).toEqual({ threshold: 60 * 60 * 24 * (365 * 2.5), operator: 'gt' });
expect(fromHumanReadableDuration('less than 2.5 year')).toEqual({ threshold: 60 * 60 * 24 * (365 * 2.5), operator: 'lt' });
});
});
describe('fromHumanReadableBoolean', () => {
it('should match', async () => {
expect(fromHumanReadableBoolean('yes')).toEqual(true);
expect(fromHumanReadableBoolean('true')).toEqual(true);
expect(fromHumanReadableBoolean('no')).toEqual(false);
expect(fromHumanReadableBoolean('false')).toEqual(false);
expect(fromHumanReadableBoolean('abc')).toEqual(false);
});
});

View File

@@ -0,0 +1,201 @@
import { PredicateFactory } from '@core/lib/rules/factory';
import yaml from 'js-yaml';
import { PredicateOption } from '@core/lib/rules/interfaces';
import { AdultPredicate } from '@core/lib/rules/predicate/adult';
import { AgePredicate } from '@core/lib/rules/predicate/age';
import { CastPredicate } from '@core/lib/rules/predicate/cast';
import { CrewPredicate } from '@core/lib/rules/predicate/crew';
import { GenrePredicate } from '@core/lib/rules/predicate/genre';
import { KeywordPredicate } from '@core/lib/rules/predicate/keywords';
import { OriginalLanguagePredicate } from '@core/lib/rules/predicate/originalLanguage';
import { ProductionCompanyPredicate } from '@core/lib/rules/predicate/productionCompany';
import { ReleasedPredicate } from '@core/lib/rules/predicate/released';
import { RuntimePredicate } from '@core/lib/rules/predicate/runtime';
import { ScorePredicate } from '@core/lib/rules/predicate/score';
import { StatusPredicate } from '@core/lib/rules/predicate/status';
import { VoteCountPredicate } from '@core/lib/rules/predicate/voteCount';
import { WatchprovidersPredicate } from '@core/lib/rules/predicate/watchproviders';
const from = (data: string) => {
const options = yaml.load(data) as PredicateOption;
const [head] = PredicateFactory.buildPredicates([options]);
return head;
};
const expectPredicate = (data: string) => {
return expect(from(data));
};
describe('AdultPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`adult: yes`).toEqual(new AdultPredicate({ value: true }));
expectPredicate(`adult: true`).toEqual(new AdultPredicate({ value: true }));
expectPredicate(`adult: no`).toEqual(new AdultPredicate({ value: false }));
expectPredicate(`adult: false`).toEqual(new AdultPredicate({ value: false }));
});
});
describe('AgePredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`age: less than 1 minute`).toEqual(new AgePredicate({ operator: 'lt', threshold: 60 }));
expectPredicate(`age: less than 1 minutes`).toEqual(new AgePredicate({ operator: 'lt', threshold: 60 }));
expectPredicate(`age: more than 1 minute`).toEqual(new AgePredicate({ operator: 'gt', threshold: 60 }));
expectPredicate(`age: more than 1 minutes`).toEqual(new AgePredicate({ operator: 'gt', threshold: 60 }));
});
});
describe('CastPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
cast:
- Korben Dallas
- Dumbo
`).toEqual(new CastPredicate({ terms: ['Korben Dallas', 'Dumbo'], excludeVoice: false }));
expectPredicate(`
cast:
voice: exclude
names:
- Korben Dallas
- Dumbo
`).toEqual(new CastPredicate({ terms: ['Korben Dallas', 'Dumbo'], excludeVoice: true }));
});
});
describe('CrewPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
crew:
- Korben Dallas
- Dumbo
`).toEqual(new CrewPredicate({ terms: ['Korben Dallas', 'Dumbo'], job: undefined }));
expectPredicate(`
crew:
job: dummy
names:
- Korben Dallas
- Dumbo
`).toEqual(new CrewPredicate({ terms: ['Korben Dallas', 'Dumbo'], job: 'dummy' }));
});
});
describe('GenrePredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
genre: Comedy
`).toEqual(new GenrePredicate({ terms: ['Comedy'] }));
expectPredicate(`
genre:
- Comedy
- Romance
`).toEqual(new GenrePredicate({ terms: ['Comedy', 'Romance'] }));
});
});
describe('KeywordPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
keyword: Comedy
`).toEqual(new KeywordPredicate({ terms: ['Comedy'] }));
expectPredicate(`
keyword:
- space
- futuristic
`).toEqual(new GenrePredicate({ terms: ['space', 'futuristic'] }));
});
});
describe('OriginalLanguagePredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
originalLanguage: en
`).toEqual(new OriginalLanguagePredicate({ terms: ['en'] }));
expectPredicate(`
originalLanguage:
- de
- en
`).toEqual(new OriginalLanguagePredicate({ terms: ['de', 'en'] }));
});
});
describe('ProductionCompanyPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
productionCompany: Warner Bros.
`).toEqual(new ProductionCompanyPredicate({ terms: ['Warner Bros.'] }));
expectPredicate(`
productionCompany:
- Warner Bros.
- 20th Century Fox
`).toEqual(new ProductionCompanyPredicate({ terms: ['Warner Bros.', '20th Century Fox'] }));
});
});
describe('ReleasedPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`released: yes`).toEqual(new ReleasedPredicate({ value: true }));
expectPredicate(`released: true`).toEqual(new ReleasedPredicate({ value: true }));
expectPredicate(`released: no`).toEqual(new ReleasedPredicate({ value: false }));
expectPredicate(`released: false`).toEqual(new ReleasedPredicate({ value: false }));
});
});
describe('RuntimePredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`runtime: less than 1 hour`).toEqual(new RuntimePredicate({ operator: 'lt', threshold: 60 * 60 }));
expectPredicate(`runtime: less than 1 hours`).toEqual(new RuntimePredicate({ operator: 'lt', threshold: 60 * 60 }));
expectPredicate(`runtime: more than 3 hour`).toEqual(new RuntimePredicate({ operator: 'gt', threshold: 3 * 60 * 60 }));
expectPredicate(`runtime: more than 3 hours`).toEqual(new RuntimePredicate({ operator: 'gt', threshold: 3 * 60 * 60 }));
});
});
describe('ScorePredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`score: above 8`).toEqual(new ScorePredicate({ operator: 'gt', threshold: 8 }));
expectPredicate(`score: above 8.4`).toEqual(new ScorePredicate({ operator: 'gt', threshold: 8.4 }));
expectPredicate(`score: above 85/100`).toEqual(new ScorePredicate({ operator: 'gt', threshold: 8.5 }));
expectPredicate(`score: below 8`).toEqual(new ScorePredicate({ operator: 'lt', threshold: 8 }));
expectPredicate(`score: below 8.4`).toEqual(new ScorePredicate({ operator: 'lt', threshold: 8.4 }));
expectPredicate(`score: below 85/100`).toEqual(new ScorePredicate({ operator: 'lt', threshold: 8.5 }));
});
});
describe('StatusPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
status: status 1
`).toEqual(new StatusPredicate({ terms: ['status 1'] }));
expectPredicate(`
status:
- status 1
- status 2
`).toEqual(new StatusPredicate({ terms: ['status 1', 'status 2'] }));
});
});
describe('VoteCountPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`voteCount: greater than 1000`).toEqual(new VoteCountPredicate({ operator: 'gt', threshold: 1000 }));
expectPredicate(`voteCount: above 84`).toEqual(new ScorePredicate({ operator: 'gt', threshold: 84 }));
expectPredicate(`voteCount: above 84.32`).toEqual(new ScorePredicate({ operator: 'gt', threshold: 84.32 }));
expectPredicate(`voteCount: below 8`).toEqual(new ScorePredicate({ operator: 'lt', threshold: 8 }));
expectPredicate(`voteCount: under 85`).toEqual(new ScorePredicate({ operator: 'lt', threshold: 85 }));
expectPredicate(`voteCount: less than 100`).toEqual(new ScorePredicate({ operator: 'lt', threshold: 100 }));
});
});
describe('WatchProvidersPredicateBuilder', () => {
it('should match', async () => {
expectPredicate(`
watchProviders:
region: us
names:
- Platform 1
- Platform 2
`).toEqual(new WatchprovidersPredicate({ terms: ['Platform 1', 'Platform 2'], region: 'us' }));
});
});

View File

@@ -14,6 +14,7 @@ import { AdultPredicate } from '@core/lib/rules/predicate/adult';
import { RuntimePredicate } from '@core/lib/rules/predicate/runtime';
import { OriginalLanguagePredicate } from '@core/lib/rules/predicate/originalLanguage';
import { StatusPredicate } from '@core/lib/rules/predicate/status';
import { fromHumanReadableDuration } from '@core/lib/rules/predicate/time';
const movie = movieJson as MovieDetails;
const testRule = (predicate: Predicate | Predicate[]) => new Rule('test rule', Array.isArray(predicate) ? predicate : [predicate], 'accept');
@@ -87,29 +88,29 @@ describe('orPredicate', () => {
it('should not match when vote count below 8554 or above 8556', async () => {
{
const rule = testRule(new VoteCountPredicate({ voteCount: 'less than 8554' }));
const rule = testRule(new VoteCountPredicate({ operator: 'lt', threshold: 8554 }));
assertRuleDoesntMatch(rule, movie);
}
{
const rule = testRule(new VoteCountPredicate({ voteCount: 'above 8556' }));
const rule = testRule(new VoteCountPredicate({ operator: 'gt', threshold: 8556 }));
assertRuleDoesntMatch(rule, movie);
}
});
it('should match when vote count above 8555', async () => {
{
const rule = testRule(new VoteCountPredicate({ voteCount: 'above 8554' }));
const rule = testRule(new VoteCountPredicate({ operator: 'gt', threshold: 8554 }));
assertRuleMatches(rule, movie);
}
{
const rule = testRule(new VoteCountPredicate({ voteCount: 'less than 8556' }));
const rule = testRule(new VoteCountPredicate({ operator: 'lt', threshold: 8556 }));
assertRuleMatches(rule, movie);
}
});
it('should match genres', async () => {
{
const rule = testRule(new GenrePredicate({ genre: 'science-fiction' }));
const rule = testRule(new GenrePredicate({ terms: ['science-fiction'] }));
assertRuleMatches(rule, movie);
}
{
@@ -117,7 +118,7 @@ describe('orPredicate', () => {
'simple',
[
new GenrePredicate({
genre: ['science-fiction', 'comédie'],
terms: ['science-fiction', 'comédie'],
}),
],
'accept'
@@ -125,7 +126,7 @@ describe('orPredicate', () => {
assertRuleMatches(rule, movie);
}
{
const rule = testRule(new GenrePredicate({ genre: 'comédie' }));
const rule = testRule(new GenrePredicate({ terms: ['comédie'] }));
assertRuleDoesntMatch(rule, movie);
}
});
@@ -133,54 +134,48 @@ describe('orPredicate', () => {
describe('agePredicate', () => {
it('should match', async () => {
const rule = testRule(new AgePredicate({ age: 'more than 1 year' }));
const rule = testRule(new AgePredicate(fromHumanReadableDuration('more than 1 year')));
assertRuleMatches(rule, movie);
});
});
describe('castPredicate', () => {
it('should match', async () => {
const rule = testRule(new CastPredicate({ cast: ['Sigourney Weaver'] }));
const rule = testRule(new CastPredicate({ terms: ['Sigourney Weaver'], excludeVoice: false }));
assertRuleMatches(rule, movie);
});
it('should match despite different case', async () => {
const rule = testRule(new CastPredicate({ cast: ['SiGoUrnEy weAVEr'] }));
const rule = testRule(new CastPredicate({ terms: ['SiGoUrnEy weAVEr'], excludeVoice: false }));
assertRuleMatches(rule, movie);
});
it('should match if at least one cast match', async () => {
const rule = testRule([new CastPredicate({ cast: ['Sigourney Weaver', 'Jessica Alba'] })]);
const rule = testRule([new CastPredicate({ terms: ['Sigourney Weaver', 'Jessica Alba'], excludeVoice: false })]);
assertRuleMatches(rule, movie);
});
it('should not match', async () => {
const rule = testRule(new CastPredicate({ cast: ['Jessica Alba'] }));
const rule = testRule(new CastPredicate({ terms: ['Jessica Alba'], excludeVoice: false }));
assertRuleDoesntMatch(rule, movie);
});
it('should match with voice field', async () => {
const ruleIncludeVoice = testRule(
new CastPredicate({
cast: {
voice: 'include',
names: ['Bob Sherman'],
},
terms: ['Bob Sherman'],
excludeVoice: false,
})
);
const ruleExcludeVoice = testRule(
new CastPredicate({
cast: {
voice: 'exclude',
names: ['Bob Sherman'],
},
terms: ['Bob Sherman'],
excludeVoice: true,
})
);
const ruleExcludeVoice2 = testRule(
new CastPredicate({
cast: {
voice: 'exclude',
names: ['Bob Sherman', 'Sigourney Weaver'],
},
terms: ['Bob Sherman', 'Sigourney Weaver'],
excludeVoice: true,
})
);
assertRuleMatches(ruleIncludeVoice, movie);
@@ -191,21 +186,22 @@ describe('castPredicate', () => {
describe('crewPredicate', () => {
it('should match', async () => {
const rule = testRule(new CrewPredicate({ crew: ['James Cameron'] }));
const rule = testRule(new CrewPredicate({ terms: ['James Cameron'] }));
assertRuleMatches(rule, movie);
});
it('should match with a job', async () => {
const rule = testRule(
new CrewPredicate({
crew: { job: 'director', names: ['James Cameron'] },
job: 'director',
terms: ['James Cameron'],
})
);
assertRuleMatches(rule, movie);
});
it('should match despite different case', async () => {
const rule = testRule(new CrewPredicate({ crew: ['JaMEs CamerOn'] }));
const rule = testRule(new CrewPredicate({ terms: ['JaMEs CamerOn'] }));
assertRuleMatches(rule, movie);
});
@@ -213,10 +209,8 @@ describe('crewPredicate', () => {
it('should match if at least one crew match', async () => {
const rule = testRule(
new CrewPredicate({
crew: {
job: 'Art Direction',
names: ['Bert Davey', 'Ken Court'],
},
job: 'Art Direction',
terms: ['Bert Davey', 'Ken Court'],
})
);
@@ -224,14 +218,15 @@ describe('crewPredicate', () => {
});
it('should not match', async () => {
const rule = testRule(new CrewPredicate({ crew: ['Jessica Alba'] }));
const rule = testRule(new CrewPredicate({ terms: ['Jessica Alba'] }));
assertRuleDoesntMatch(rule, movie);
});
it('should not match when person found in another job', async () => {
const rule = testRule(
new CrewPredicate({
crew: { job: 'director', names: ['Peter Lamont'] },
job: 'director',
terms: ['Peter Lamont'],
})
);
@@ -240,20 +235,14 @@ describe('crewPredicate', () => {
});
describe('keywordPredicate', () => {
it('should match string', async () => {
const rule = testRule(new KeywordPredicate({ keyword: 'space travel' }));
assertRuleMatches(rule, movie);
});
it('should match array', async () => {
const rule = testRule(new KeywordPredicate({ keyword: ['space travel'] }));
it('should match', async () => {
const rule = testRule(new KeywordPredicate({ terms: ['space travel'] }));
assertRuleMatches(rule, movie);
});
it('should match despite different case', async () => {
const rule = testRule(new KeywordPredicate({ keyword: ['SpAcE TraVel'] }));
const rule = testRule(new KeywordPredicate({ terms: ['SpAcE TraVel'] }));
assertRuleMatches(rule, movie);
});
@@ -261,30 +250,22 @@ describe('keywordPredicate', () => {
it('should match if at least one cast match', async () => {
const rule = testRule(
new KeywordPredicate({
keyword: ['space travel', 'unknown keyword'],
terms: ['space travel', 'unknown keyword'],
})
);
assertRuleMatches(rule, movie);
});
it('should not match', async () => {
const rule = testRule(new KeywordPredicate({ keyword: ['unknown keyword'] }));
const rule = testRule(new KeywordPredicate({ terms: ['unknown keyword'] }));
assertRuleDoesntMatch(rule, movie);
});
});
describe('adult predicate', () => {
it('should match true/false', async () => {
const noRule = testRule(new AdultPredicate({ adult: 'false' }));
const yesRule = testRule(new AdultPredicate({ adult: 'true' }));
assertRuleMatches(noRule, movie);
assertRuleDoesntMatch(yesRule, movie);
});
it('should match yes/no', async () => {
const noRule = testRule(new AdultPredicate({ adult: 'no' }));
const yesRule = testRule(new AdultPredicate({ adult: 'yes' }));
const noRule = testRule(new AdultPredicate({ value: false }));
const yesRule = testRule(new AdultPredicate({ value: true }));
assertRuleMatches(noRule, movie);
assertRuleDoesntMatch(yesRule, movie);
@@ -293,39 +274,36 @@ describe('adult predicate', () => {
describe('runtime predicate', () => {
it('should match', async () => {
assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'less than 3 hours' })), movie);
assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'less than 2.5 hours' })), movie);
assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'more than 2 hour' })), movie);
assertRuleMatches(testRule(new RuntimePredicate({ runtime: 'more than 2.25 hours' })), movie);
assertRuleMatches(testRule(new RuntimePredicate({ operator: 'lt', threshold: 3 * 60 * 60 })), movie);
assertRuleMatches(testRule(new RuntimePredicate({ operator: 'gt', threshold: 2 * 60 * 60 })), movie);
});
it('should not match', async () => {
assertRuleDoesntMatch(testRule(new RuntimePredicate({ runtime: 'more than 3 hours' })), movie);
assertRuleDoesntMatch(testRule(new RuntimePredicate({ runtime: 'more than 2.5 weeks' })), movie);
assertRuleDoesntMatch(testRule(new RuntimePredicate({ runtime: 'less than 2 minutes' })), movie);
assertRuleDoesntMatch(testRule(new RuntimePredicate({ operator: 'gt', threshold: 3 * 60 * 60 })), movie);
assertRuleDoesntMatch(testRule(new RuntimePredicate({ operator: 'lt', threshold: 2 * 60 * 60 })), movie);
});
});
describe('originalLanguage predicate', () => {
it('should match', async () => {
assertRuleMatches(testRule(new OriginalLanguagePredicate({ originalLanguage: 'en' })), movie);
assertRuleMatches(testRule(new OriginalLanguagePredicate({ originalLanguage: ['en', 'fr'] })), movie);
assertRuleMatches(testRule(new OriginalLanguagePredicate({ terms: ['en'] })), movie);
assertRuleMatches(testRule(new OriginalLanguagePredicate({ terms: ['en', 'fr'] })), movie);
});
it('should not match', async () => {
assertRuleDoesntMatch(testRule(new OriginalLanguagePredicate({ originalLanguage: 'fr' })), movie);
assertRuleDoesntMatch(testRule(new OriginalLanguagePredicate({ originalLanguage: ['fr', 'de'] })), movie);
assertRuleDoesntMatch(testRule(new OriginalLanguagePredicate({ terms: ['fr'] })), movie);
assertRuleDoesntMatch(testRule(new OriginalLanguagePredicate({ terms: ['fr', 'de'] })), movie);
});
});
describe('status predicate', () => {
it('should match', async () => {
assertRuleMatches(testRule(new StatusPredicate({ status: 'released' })), movie);
assertRuleMatches(testRule(new StatusPredicate({ status: ['released', 'post production'] })), movie);
assertRuleMatches(testRule(new StatusPredicate({ terms: ['released'] })), movie);
assertRuleMatches(testRule(new StatusPredicate({ terms: ['released', 'post production'] })), movie);
});
it('should not match', async () => {
assertRuleDoesntMatch(testRule(new StatusPredicate({ status: 'canceled' })), movie);
assertRuleDoesntMatch(testRule(new StatusPredicate({ status: ['canceled', 'post production'] })), movie);
assertRuleDoesntMatch(testRule(new StatusPredicate({ terms: ['canceled'] })), movie);
assertRuleDoesntMatch(testRule(new StatusPredicate({ terms: ['canceled', 'post production'] })), movie);
});
});

View File

@@ -44,7 +44,7 @@ export interface NotFilterOptions {
}
export type AdultOptions = {
adult: string;
adult: string | boolean;
};
export type AgeOptions = {
@@ -81,7 +81,7 @@ export type ProductionCompanyOptions = {
};
export type ReleasedOptions = {
released: string;
released: string | boolean;
};
export type ScoreOptions = {

View File

@@ -1,13 +1,13 @@
import { PredicateBuilder } from '@core/lib/rules';
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { BooleanPredicate } from '@core/lib/rules/predicate/boolean';
import { BooleanPredicate, BooleanPredicateParameters, fromHumanReadableBoolean } from '@core/lib/rules/predicate/boolean';
import { AdultOptions } from '@core/lib/rules/interfaces';
export type AdultPredicateParameters = BooleanPredicateParameters;
export class AdultPredicate extends BooleanPredicate {
constructor(options: AdultOptions) {
super({
value: options.adult,
});
constructor(options: AdultPredicateParameters) {
super(options);
}
matches(movie: MovieDetails): boolean {
@@ -17,5 +17,10 @@ export class AdultPredicate extends BooleanPredicate {
export const AdultPredicateBuilder: PredicateBuilder = {
key: 'adult',
build: (data: AdultOptions) => new AdultPredicate(data),
build: (data: AdultOptions) => {
const parameters = {
value: typeof data.adult === 'boolean' ? data.adult : fromHumanReadableBoolean(data.adult),
};
return new AdultPredicate(parameters);
},
};

View File

@@ -1,22 +1,24 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import { fromHumanReadableDuration, TimePredicate } from '@core/lib/rules/predicate/time';
import { fromHumanReadableDuration, TimePredicate, TimePredicateParameters } from '@core/lib/rules/predicate/time';
import { AgeOptions } from '@core/lib/rules/interfaces';
export type AgePredicateParameters = TimePredicateParameters;
export class AgePredicate extends TimePredicate {
constructor(options: AgeOptions) {
super(fromHumanReadableDuration(options.age), ageMetric);
constructor(options: AgePredicateParameters) {
super(options);
}
protected getMetrics(movie: MovieDetails): number {
if (!movie.releaseDate) {
return 0;
}
return (Date.now() - Date.parse(movie.releaseDate)) / 1000;
}
}
const ageMetric = (movie: MovieDetails): number => {
if (!movie.releaseDate) {
return 0;
}
return (Date.now() - Date.parse(movie.releaseDate)) / 1000;
};
export const AgePredicateBuilder: PredicateBuilder = {
key: 'age',
build: (data: AgeOptions) => new AgePredicate(data),
build: (data: AgeOptions) => new AgePredicate(fromHumanReadableDuration(data.age)),
};

View File

@@ -1,19 +1,19 @@
import { Predicate } from '@core/lib/rules';
export interface BooleanPredicateOptions {
value: string;
export interface BooleanPredicateParameters {
value: boolean;
}
export abstract class BooleanPredicate extends Predicate {
protected targetValue: boolean;
protected constructor(options: BooleanPredicateOptions) {
protected constructor(options: BooleanPredicateParameters) {
super();
this.targetValue = fromHumanReadableBoolean(options.value);
this.targetValue = options.value;
}
}
const fromHumanReadableBoolean = (str: string): boolean => {
const acceptableTrueBoolean = ['yes', 'true', '1'];
export const fromHumanReadableBoolean = (str: string): boolean => {
const acceptableTrueBoolean = ['yes', 'true'];
return acceptableTrueBoolean.includes(str);
};

View File

@@ -1,16 +1,18 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { PredicateBuilder } from '@core/lib/rules';
import { CastOptions } from '@core/lib/rules/interfaces';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
export type CastPredicateParameters = TagsPredicateParameters & {
excludeVoice: boolean;
};
export class CastPredicate extends TagsPredicate {
private excludeVoice: boolean = false;
constructor(options: CastOptions) {
super({
terms: Array.isArray(options.cast) ? options.cast : options.cast.names,
});
this.excludeVoice = Array.isArray(options.cast) ? false : options.cast.voice?.toLowerCase() === 'exclude';
constructor(options: CastPredicateParameters) {
super(options);
this.excludeVoice = options.excludeVoice;
}
getTags(movie: MovieDetails): string[] {
@@ -29,5 +31,11 @@ export class CastPredicate extends TagsPredicate {
export const CastPredicateBuilder: PredicateBuilder = {
key: 'cast',
build: (data: CastOptions) => new CastPredicate(data),
build: (data: CastOptions) => {
const parameters: CastPredicateParameters = {
terms: Array.isArray(data.cast) ? data.cast : data.cast.names,
excludeVoice: Array.isArray(data.cast) ? false : data.cast.voice?.toLowerCase() === 'exclude',
};
return new CastPredicate(parameters);
},
};

View File

@@ -1,16 +1,18 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
import { CrewOptions } from '@core/lib/rules/interfaces';
export type CrewPredicateParameters = TagsPredicateParameters & {
job?: string;
};
export class CrewPredicate extends TagsPredicate {
private job?: string;
constructor(options: CrewOptions) {
super({
terms: Array.isArray(options.crew) ? options.crew : options.crew.names,
});
this.job = Array.isArray(options.crew) ? undefined : options.crew.job;
constructor(parameters: CrewPredicateParameters) {
super(parameters);
this.job = parameters.job;
}
getTags(movie: MovieDetails): string[] {
@@ -29,5 +31,11 @@ export class CrewPredicate extends TagsPredicate {
export const CrewPredicateBuilder: PredicateBuilder = {
key: 'crew',
build: (data: CrewOptions) => new CrewPredicate(data),
build: (data: CrewOptions) => {
const parameters = {
terms: Array.isArray(data.crew) ? data.crew : data.crew.names,
job: Array.isArray(data.crew) ? undefined : data.crew.job,
};
return new CrewPredicate(parameters);
},
};

View File

@@ -1,13 +1,13 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
import { GenreOptions } from '@core/lib/rules/interfaces';
export type GenrePredicateParameters = TagsPredicateParameters;
export class GenrePredicate extends TagsPredicate {
constructor(options: GenreOptions) {
super({
terms: Array.isArray(options.genre) ? options.genre : [options.genre],
});
constructor(options: GenrePredicateParameters) {
super(options);
}
getTags(movie: MovieDetails): string[] {
@@ -21,5 +21,10 @@ export class GenrePredicate extends TagsPredicate {
export const GenrePredicateBuilder: PredicateBuilder = {
key: 'genre',
build: (data: GenreOptions) => new GenrePredicate(data),
build: (data: GenreOptions) => {
const parameters = {
terms: Array.isArray(data.genre) ? data.genre : [data.genre],
};
return new GenrePredicate(parameters);
},
};

View File

@@ -1,13 +1,13 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
import { KeywordOptions } from '@core/lib/rules/interfaces';
export type KeywordPredicateParameters = TagsPredicateParameters;
export class KeywordPredicate extends TagsPredicate {
constructor(options: KeywordOptions) {
super({
terms: Array.isArray(options.keyword) ? options.keyword : [options.keyword],
});
constructor(options: KeywordPredicateParameters) {
super(options);
}
getTags(movie: MovieDetails): string[] {
@@ -24,5 +24,10 @@ export class KeywordPredicate extends TagsPredicate {
export const KeywordPredicateBuilder: PredicateBuilder = {
key: 'keyword',
build: (data: KeywordOptions) => new KeywordPredicate(data),
build: (data: KeywordOptions) => {
const parameters = {
terms: Array.isArray(data.keyword) ? data.keyword : [data.keyword],
};
return new KeywordPredicate(parameters);
},
};

View File

@@ -1,37 +1,29 @@
import { Predicate } from '@core/lib/rules';
import { MovieDetails } from '@core/api/overseerr/interfaces';
export type NumberPredicateOptions = {
export type NumberPredicateParameters = {
threshold: number;
operator: 'lt' | 'gt';
};
export interface MetricsExtractor {
(movie: MovieDetails): number;
}
export class NumberPredicate extends Predicate {
export abstract class NumberPredicate extends Predicate {
private threshold: number;
private operator: 'lt' | 'gt';
private metrics: MetricsExtractor;
constructor(options: NumberPredicateOptions, metrics: MetricsExtractor) {
constructor(options: NumberPredicateParameters) {
super();
this.threshold = options.threshold;
this.operator = options.operator;
this.metrics = metrics;
}
private getMetrics(movie: MovieDetails): number {
return this.metrics(movie);
}
protected abstract getMetrics(movie: MovieDetails): number;
matches(movie: MovieDetails): boolean {
const measuredSeconds = this.getMetrics(movie);
const metric = this.getMetrics(movie);
if (this.operator === 'lt') {
return measuredSeconds < this.threshold;
return metric < this.threshold;
}
return measuredSeconds > this.threshold;
return metric > this.threshold;
}
}

View File

@@ -1,13 +1,13 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
import { OriginalLanguageOptions } from '@core/lib/rules/interfaces';
export type OriginalLanguagePredicateParameters = TagsPredicateParameters;
export class OriginalLanguagePredicate extends TagsPredicate {
constructor(options: OriginalLanguageOptions) {
super({
terms: Array.isArray(options.originalLanguage) ? options.originalLanguage : [options.originalLanguage],
});
constructor(options: OriginalLanguagePredicateParameters) {
super(options);
}
getTags(movie: MovieDetails): string[] {
@@ -21,5 +21,10 @@ export class OriginalLanguagePredicate extends TagsPredicate {
export const OriginalLanguagePredicateBuilder: PredicateBuilder = {
key: 'originalLanguage',
build: (data: OriginalLanguageOptions) => new OriginalLanguagePredicate(data),
build: (data: OriginalLanguageOptions) => {
const parameters = {
terms: Array.isArray(data.originalLanguage) ? data.originalLanguage : [data.originalLanguage],
};
return new OriginalLanguagePredicate(parameters);
},
};

View File

@@ -1,13 +1,13 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { ProductionCompanyOptions } from '@core/lib/rules/interfaces';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
export type ProductionCompanyPredicateParameters = TagsPredicateParameters;
export class ProductionCompanyPredicate extends TagsPredicate {
constructor(options: ProductionCompanyOptions) {
super({
terms: Array.isArray(options.productionCompany) ? options.productionCompany : [options.productionCompany],
});
constructor(options: ProductionCompanyPredicateParameters) {
super(options);
}
getTags(movie: MovieDetails): string[] {
@@ -24,5 +24,10 @@ export class ProductionCompanyPredicate extends TagsPredicate {
export const ProductionCompanyPredicateBuilder: PredicateBuilder = {
key: 'productionCompany',
build: (data: ProductionCompanyOptions) => new ProductionCompanyPredicate(data),
build: (data: ProductionCompanyOptions) => {
const parameters = {
terms: Array.isArray(data.productionCompany) ? data.productionCompany : [data.productionCompany],
};
return new ProductionCompanyPredicate(parameters);
},
};

View File

@@ -1,13 +1,13 @@
import { PredicateBuilder } from '@core/lib/rules';
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { BooleanPredicate } from '@core/lib/rules/predicate/boolean';
import { BooleanPredicate, BooleanPredicateParameters, fromHumanReadableBoolean } from '@core/lib/rules/predicate/boolean';
import { ReleasedOptions } from '@core/lib/rules/interfaces';
export type ReleasedPredicateParameters = BooleanPredicateParameters;
export class ReleasedPredicate extends BooleanPredicate {
constructor(options: ReleasedOptions) {
super({
value: options.released,
});
constructor(options: ReleasedPredicateParameters) {
super(options);
}
matches(movie: MovieDetails): boolean {
@@ -25,5 +25,10 @@ export class ReleasedPredicate extends BooleanPredicate {
export const ReleasedPredicateBuilder: PredicateBuilder = {
key: 'released',
build: (data: ReleasedOptions) => new ReleasedPredicate(data),
build: (data: ReleasedOptions) => {
const parameters = {
value: typeof data.released === 'boolean' ? data.released : fromHumanReadableBoolean(data.released),
};
return new ReleasedPredicate(parameters);
},
};

View File

@@ -1,22 +1,24 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import { fromHumanReadableDuration, TimePredicate } from '@core/lib/rules/predicate/time';
import { fromHumanReadableDuration, TimePredicate, TimePredicateParameters } from '@core/lib/rules/predicate/time';
import { RuntimeOptions } from '@core/lib/rules/interfaces';
export type RuntimePredicateParameters = TimePredicateParameters;
export class RuntimePredicate extends TimePredicate {
constructor(options: RuntimeOptions) {
super(fromHumanReadableDuration(options.runtime), runtimeMetric);
constructor(options: RuntimePredicateParameters) {
super(options);
}
protected getMetrics(movie: MovieDetails): number {
if (!movie.runtime) {
return 0;
}
return Number(movie.runtime) * 60;
}
}
const runtimeMetric = (movie: MovieDetails): number => {
if (!movie.runtime) {
return 0;
}
return Number(movie.runtime) * 60;
};
export const RuntimePredicateBuilder: PredicateBuilder = {
key: 'runtime',
build: (data: RuntimeOptions) => new RuntimePredicate(data),
build: (data: RuntimeOptions) => new RuntimePredicate(fromHumanReadableDuration(data.runtime)),
};

View File

@@ -1,32 +1,43 @@
import { NumberPredicate } from '@core/lib/rules/predicate/number';
import { NumberPredicate, NumberPredicateParameters } from '@core/lib/rules/predicate/number';
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import logger from '@core/log';
import { ScoreOptions } from '@core/lib/rules/interfaces';
export type ScorePredicateParameters = NumberPredicateParameters;
export class ScorePredicate extends NumberPredicate {
constructor(options: ScoreOptions) {
super(fromHumanReadableScore(options.score), (movie: MovieDetails) => movie.voteAverage);
constructor(options: ScorePredicateParameters) {
super(options);
}
protected getMetrics(movie: MovieDetails): number {
return movie.voteAverage;
}
}
export const fromHumanReadableScore = (str: string): { threshold: number; operator: 'lt' | 'gt' } => {
const regex = /(below|under|less than|above|greater than) ((?:\d+(\.\d+)|\d+\/\d+)?)/gm;
const ratioRegex = /\d+\/\d+/gm;
const regex = /(below|under|less than|above|greater than) ((?:\d+(\.\d+)?|\d+\/\d+)?)$/gm;
const ratioRegex = /(\d+)\/(\d+)/gm;
const groups = [...str.matchAll(regex)][0];
if (!groups) {
throw new Error(`Unparsable score string '${str}' !`);
}
const operator = groups[1];
const thresholdString = groups[2];
let threshold = 0;
let threshold = Number(thresholdString);
const ratioGroups = [...thresholdString.matchAll(ratioRegex)][0];
if (ratioGroups) {
const numerator = Number(ratioGroups[1]);
const denominator = Number(ratioGroups[2]);
if (numerator > denominator) {
logger.warn(
`Invalid expression: ${str}. In score expression, denominator should be greater than numerator. Using numerator value ${numerator} as threshold`
);
}
if (denominator === 0) {
logger.warning(`In score expression, denominator should not be 0. Using numerator value ${numerator} as threshold`);
logger.warn(`In score expression, denominator should not be 0. Using numerator value ${numerator} as threshold`);
threshold = numerator;
} else {
// Bring the value back in the range of 0..10 with 2 decimals
@@ -41,5 +52,5 @@ export const fromHumanReadableScore = (str: string): { threshold: number; operat
export const ScorePredicateBuilder: PredicateBuilder = {
key: 'score',
build: (data: ScoreOptions) => new ScorePredicate(data),
build: (data: ScoreOptions) => new ScorePredicate(fromHumanReadableScore(data.score)),
};

View File

@@ -1,13 +1,13 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
import { StatusOptions } from '@core/lib/rules/interfaces';
export type StatusPredicateParameters = TagsPredicateParameters;
export class StatusPredicate extends TagsPredicate {
constructor(options: StatusOptions) {
super({
terms: Array.isArray(options.status) ? options.status : [options.status],
});
constructor(options: StatusPredicateParameters) {
super(options);
}
getTags(movie: MovieDetails): string[] {
@@ -21,5 +21,10 @@ export class StatusPredicate extends TagsPredicate {
export const StatusPredicateBuilder: PredicateBuilder = {
key: 'status',
build: (data: StatusOptions) => new StatusPredicate(data),
build: (data: StatusOptions) => {
const parameters = {
terms: Array.isArray(data.status) ? data.status : [data.status],
};
return new StatusPredicate(parameters);
},
};

View File

@@ -1,14 +1,14 @@
import { Predicate } from '@core/lib/rules';
import { MovieDetails } from '@core/api/overseerr/interfaces';
export type TagsPredicateOptions = {
export type TagsPredicateParameters = {
terms: string[];
};
export abstract class TagsPredicate extends Predicate {
private terms: string[];
protected constructor(options: TagsPredicateOptions) {
protected constructor(options: TagsPredicateParameters) {
super();
this.terms = options.terms;
}
@@ -20,5 +20,3 @@ export abstract class TagsPredicate extends Predicate {
return this.terms.some((t) => tags.includes(t.toLowerCase()));
}
}
export default TagsPredicate;

View File

@@ -1,19 +1,14 @@
import { NumberPredicate, NumberPredicateOptions } from '@core/lib/rules/predicate/number';
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { NumberPredicate, NumberPredicateParameters } from '@core/lib/rules/predicate/number';
export type TimePredicateOptions = NumberPredicateOptions;
export type TimePredicateParameters = NumberPredicateParameters;
export interface TimeMetricsExtractor {
(movie: MovieDetails): number;
}
export class TimePredicate extends NumberPredicate {
protected constructor(options: TimePredicateOptions, metrics: TimeMetricsExtractor) {
super(options, metrics);
export abstract class TimePredicate extends NumberPredicate {
protected constructor(options: TimePredicateParameters) {
super(options);
}
}
export const fromHumanReadableDuration = (str: string): NumberPredicateOptions => {
export const fromHumanReadableDuration = (str: string): NumberPredicateParameters => {
const regex = /(less than|more than) (\d+(\.\d+)?) (year|month|week|day|hour|minute)[s]?/gm;
const groups = [...str.matchAll(regex)][0];
const operator = groups[1];

View File

@@ -1,15 +1,21 @@
import { fromHumanReadableNumber, NumberPredicate } from '@core/lib/rules/predicate/number';
import { fromHumanReadableNumber, NumberPredicate, NumberPredicateParameters } from '@core/lib/rules/predicate/number';
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import { VoteCountOptions } from '@core/lib/rules/interfaces';
export type VoteCountPredicateParameters = NumberPredicateParameters;
export class VoteCountPredicate extends NumberPredicate {
constructor(options: VoteCountOptions) {
super(fromHumanReadableNumber(options.voteCount), (movie: MovieDetails) => movie.voteCount);
constructor(options: VoteCountPredicateParameters) {
super(options);
}
protected getMetrics(movie: MovieDetails): number {
return movie.voteCount;
}
}
export const VoteCountPredicateBuilder: PredicateBuilder = {
key: 'voteCount',
build: (data: VoteCountOptions) => new VoteCountPredicate(data),
build: (data: VoteCountOptions) => new VoteCountPredicate(fromHumanReadableNumber(data.voteCount)),
};

View File

@@ -1,14 +1,18 @@
import { MovieDetails } from '@core/api/overseerr/interfaces';
import { PredicateBuilder } from '@core/lib/rules';
import TagsPredicate from '@core/lib/rules/predicate/tag';
import { WatchProvidersOptions } from '@core/lib/rules/interfaces';
import { TagsPredicate, TagsPredicateParameters } from '@core/lib/rules/predicate/tag';
export type WatchProvidersPredicateParameters = TagsPredicateParameters & {
region: string;
};
export class WatchprovidersPredicate extends TagsPredicate {
private region: string;
constructor(options: WatchProvidersOptions) {
super({ terms: options.watchProviders.names });
this.region = options.watchProviders.region.toLowerCase();
constructor(options: WatchProvidersPredicateParameters) {
super(options);
this.region = options.region;
}
getTags(movie: MovieDetails): string[] {
@@ -29,5 +33,11 @@ export class WatchprovidersPredicate extends TagsPredicate {
export const WatchProvidersPredicateBuilder: PredicateBuilder = {
key: 'watchProviders',
build: (data: WatchProvidersOptions) => new WatchprovidersPredicate(data),
build: (data: WatchProvidersOptions) => {
const parameters = {
terms: data.watchProviders.names,
region: data.watchProviders.region ?? '',
};
return new WatchprovidersPredicate(parameters);
},
};

View File

@@ -69,6 +69,7 @@ let rulesets: Map<string, Ruleset>;
export const loadRulesets = (settings: Settings): void => {
try {
logger.info('Loading rulesets...');
rulesets = new Map<string, Ruleset>();
const configuration = settings.rulesets;
for (const options of configuration) {

View File

@@ -3,6 +3,7 @@ import yaml from 'js-yaml';
import logger from '@core/log';
import { RulesetOptions } from '@core/lib/rules/interfaces';
import Ajv from 'ajv';
import betterAjvErrors from 'better-ajv-errors';
import envVar from '@core/env';
const SCHEMA_PATH = './schema/schema.json';
@@ -82,11 +83,15 @@ class Settings {
try {
const yamlSettings = yaml.load(fs.readFileSync(this.path, 'utf8')) as YamlSettings;
const schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
const validator = new Ajv();
const isValid = validator.validate(schema, yamlSettings);
const validate = new Ajv().compile(schema);
const isValid = validate(yamlSettings);
if (!isValid) {
throw new Error(`Invalid configuration file. ${JSON.stringify(validator.errors)}`);
const output = betterAjvErrors(schema, yamlSettings, validate.errors!, {
indent: 2,
});
logger.error(output);
throw new Error(`Invalid configuration file.`);
}
this._data = yamlSettings.config as CrawlrrSettings;

View File

@@ -1,4 +1,5 @@
import { createLogger, format, transports } from 'winston';
import envVar from '@core/env';
const logFormat = format.printf(({ timestamp, level, message, stack }) => {
const log = `[ ${timestamp.replace(/[TZ]/gm, ' ')}] ${level}: ${message}`;
@@ -7,7 +8,7 @@ const logFormat = format.printf(({ timestamp, level, message, stack }) => {
});
const logger = createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
level: process.env.NODE_ENV === 'production' ? (envVar('LOG_LEVEL', 'info') as string) : 'debug',
format: format.combine(format.errors({ stack: true }), format.timestamp(), format.colorize(), format.prettyPrint(), format.simple(), logFormat),
transports: [new transports.Console()],
exceptionHandlers: [new transports.Console()],