Compare commits

..

10 Commits

Author SHA1 Message Date
renovate[bot]
5d4080bcc7 chore(deps): update dependency @headlessui/react to v2 2025-12-09 08:57:20 +00:00
Gauthier
9d8b343790 chore(deps): update all non-major dependencies (#2188)
Update all non-major dependencies. Modifications in `src` files are there to fix linting issues.
2025-12-09 09:40:35 +01:00
fallenbagel
f4fe16608a fix(jellyfin-api): use standard Authorization header (#2211)
Replace X-Emby-Authorization with Authorization header to fix authentication failures when users
have <EnableLegacyAuthorization>false</EnableLegacyAuthorization> in their Jellyfin system.xml.
2025-12-08 15:46:47 +01:00
Ludovic Ortega
d660a540da chore(helm): prepare for release (#2189)
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2025-12-07 17:22:28 +01:00
Ludovic Ortega
48ef2984e5 docs: fix chown command for windows users (#2192)
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2025-12-03 14:39:03 +01:00
Disparate2761
c5fc31c352 docs(buildfromsource): touch up path inconsistencies (#2184) 2025-12-01 14:57:01 +01:00
Ludovic Ortega
c3b9ea6ce4 chore: improve PR template (#2175) 2025-11-28 13:05:47 +01:00
Ludovic Ortega
b66b36186a docs: update weblate links (#2168)
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2025-11-22 23:29:35 +01:00
Ludovic Ortega
fb5196bdec chore: remove CHANGELOG.md (#2169) 2025-11-22 23:05:42 +01:00
0xsysr3ll
bde322de8e fix(override-rules): show correct genres for both *arr services (#2155) 2025-11-21 22:24:14 +01:00
26 changed files with 3046 additions and 5541 deletions

View File

@@ -1,14 +1,33 @@
#### Description
<!--
Please read contributing guide before submitting
your pull request. Please fill in each section below to help us better prioritize your pull request. Thanks!
-->
#### Screenshot (if UI-related)
## Description
#### To-Dos
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
- Fixes #XXXX
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
## Screenshots / Logs (if applicable)
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have read and followed the contribution [guidelines](https://github.com/seerr-team/seerr/blob/develop/CONTRIBUTING.md).
- [ ] Disclosed any use of AI (see our [policy](https://github.com/seerr-team/seerr/blob/develop/CONTRIBUTING.md#ai-assistance-notice))
- [ ] I have updated the documentation accordingly.
- [ ] All new and existing tests passed.
- [ ] Successful build `pnpm build`
- [ ] Translation keys `pnpm i18n:extract`
- [ ] Database migration (if required)
#### Issues Fixed or Closed
- Fixes #XXXX

View File

@@ -2,7 +2,6 @@
.next/
dist/
config/
CHANGELOG.md
pnpm-lock.yaml
cypress/config/settings.cypress.json

File diff suppressed because it is too large Load Diff

View File

@@ -151,9 +151,9 @@ When adding new UI text, please try to adhere to the following guidelines:
## Translation
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Seerr would be greatly appreciated! If your language is not listed below, please [open a feature request](/../../issues/new/choose).
We use [Weblate](https://translate.seerr.dev/projects/seerr/seerr-frontend/) for our translations, and your help with localizing Seerr would be greatly appreciated! If your language is not listed below, please [open a feature request](/../../issues/new/choose).
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/multi-auto.svg" alt="Translation status" /></a>
## Migrations

View File

@@ -8,7 +8,7 @@
<p align="center">
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
<a href="http://translate.seerr.dev/engage/seerr/"><img src="http://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.

View File

@@ -5,7 +5,7 @@ description: Seerr helm chart for Kubernetes
type: application
version: 3.0.0
# renovate: image=ghcr.io/seerr-team/seerr
appVersion: '2.7.3'
appVersion: '3.0.0'
maintainers:
- name: Seerr Team
url: https://github.com/orgs/seerr-team/people

View File

@@ -1,6 +1,6 @@
# seerr-chart
![Version: 3.0.0](https://img.shields.io/badge/Version-3.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.7.3](https://img.shields.io/badge/AppVersion-2.7.3-informational?style=flat-square)
![Version: 3.0.0](https://img.shields.io/badge/Version-3.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.0.0](https://img.shields.io/badge/AppVersion-3.0.0-informational?style=flat-square)
Seerr helm chart for Kubernetes
@@ -22,7 +22,7 @@ Kubernetes: `>=1.23.0-0`
## Installation
Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation)
Refer to [Seerr kubernetes documentation](https://docs.seerr.dev/getting-started/kubernetes)
## Update Notes

View File

@@ -16,7 +16,7 @@
## Installation
Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation)
Refer to [Seerr kubernetes documentation](https://docs.seerr.dev/getting-started/kubernetes)
## Update Notes

View File

@@ -26,8 +26,7 @@ sudo mkdir -p /opt/seerr && cd /opt/seerr
```
2. Clone the Seerr repository and checkout the main branch:
```bash
git clone https://github.com/seerr-team/seerr.git
cd seerr
git clone https://github.com/seerr-team/seerr.git .
git checkout main
```
3. Install the dependencies:

View File

@@ -28,7 +28,7 @@ Changes :
If you're migrating from a previous installation, you may need to update the ownership of your config folder:
```bash
sudo chown -R 1000:1000 /path/to/appdata/config
docker run --rm -v /path/to/appdata/config:/data alpine chown -R 1000:1000 /data
```
This ensures the `node` user (UID 1000) owns the config directory and can read and write to it.

View File

@@ -2,7 +2,7 @@
"name": "seerr",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@10.17.1",
"packageManager": "pnpm@10.24.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "node postinstall-win.js",
@@ -33,38 +33,38 @@
},
"license": "MIT",
"dependencies": {
"@dr.pogodin/csurf": "^1.14.1",
"@formatjs/intl-displaynames": "6.2.6",
"@dr.pogodin/csurf": "^1.16.6",
"@formatjs/intl-displaynames": "6.8.13",
"@formatjs/intl-locale": "3.1.1",
"@formatjs/intl-pluralrules": "5.1.10",
"@formatjs/intl-pluralrules": "5.4.6",
"@formatjs/intl-utils": "3.8.4",
"@formatjs/swc-plugin-experimental": "^0.4.0",
"@headlessui/react": "1.7.12",
"@heroicons/react": "2.0.16",
"@headlessui/react": "2.2.9",
"@heroicons/react": "2.2.0",
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30",
"@tanem/react-nprogress": "5.0.56",
"@types/ua-parser-js": "^0.7.36",
"@types/wink-jaro-distance": "^2.0.2",
"ace-builds": "1.15.2",
"axios": "1.10.0",
"axios-rate-limit": "1.3.0",
"ace-builds": "1.43.4",
"axios": "1.13.2",
"axios-rate-limit": "1.4.0",
"bcrypt": "5.1.0",
"bowser": "2.11.0",
"bowser": "2.13.1",
"connect-typeorm": "1.1.4",
"cookie-parser": "1.4.7",
"copy-to-clipboard": "3.3.3",
"country-flag-icons": "1.5.5",
"country-flag-icons": "1.6.4",
"cronstrue": "2.23.0",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"dayjs": "1.11.19",
"dns-caching": "^0.2.7",
"email-templates": "12.0.1",
"email-templates": "12.0.3",
"express": "4.21.2",
"express-openapi-validator": "4.13.8",
"express-rate-limit": "6.7.0",
"express-session": "1.17.3",
"formik": "^2.4.6",
"express-session": "1.18.2",
"formik": "^2.4.9",
"gravatar-url": "3.1.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
@@ -76,19 +76,19 @@
"node-schedule": "2.1.1",
"nodemailer": "6.10.0",
"openpgp": "5.11.2",
"pg": "8.11.0",
"pg": "8.16.3",
"plex-api": "5.3.2",
"pug": "3.0.3",
"react": "^18.3.1",
"react-ace": "10.1.0",
"react-animate-height": "2.1.2",
"react-aria": "3.23.0",
"react-aria": "3.44.0",
"react-dom": "^18.3.1",
"react-intersection-observer": "9.4.3",
"react-intl": "^6.6.8",
"react-markdown": "8.0.5",
"react-popper-tooltip": "4.4.2",
"react-select": "5.7.0",
"react-select": "5.10.2",
"react-spring": "9.7.1",
"react-tailwindcss-datepicker-sct": "1.3.4",
"react-toast-notifications": "2.5.1",
@@ -97,19 +97,19 @@
"react-use-clipboard": "1.0.9",
"reflect-metadata": "0.1.13",
"secure-random-password": "0.2.3",
"semver": "7.7.1",
"semver": "7.7.3",
"sharp": "^0.33.4",
"sqlite3": "5.1.7",
"swagger-ui-express": "4.6.2",
"swr": "2.2.5",
"swr": "2.3.7",
"tailwind-merge": "^2.6.0",
"typeorm": "0.3.12",
"ua-parser-js": "^1.0.35",
"undici": "^7.3.0",
"validator": "^13.15.15",
"web-push": "3.5.0",
"undici": "^7.16.0",
"validator": "^13.15.23",
"web-push": "3.6.7",
"wink-jaro-distance": "^2.0.0",
"winston": "3.8.2",
"winston": "3.18.3",
"winston-daily-rotate-file": "4.7.1",
"xml2js": "0.4.23",
"yamljs": "0.3.0",
@@ -123,32 +123,33 @@
"@tailwindcss/forms": "0.5.10",
"@tailwindcss/typography": "0.5.16",
"@types/bcrypt": "5.0.0",
"@types/cookie-parser": "1.4.3",
"@types/country-flag-icons": "1.2.0",
"@types/csurf": "1.11.2",
"@types/cookie-parser": "1.4.10",
"@types/country-flag-icons": "1.2.2",
"@types/csurf": "1.11.5",
"@types/email-templates": "8.0.4",
"@types/express": "4.17.17",
"@types/express-session": "1.17.6",
"@types/lodash": "4.14.191",
"@types/express-session": "1.18.2",
"@types/lodash": "4.17.21",
"@types/mime": "3",
"@types/node": "22.10.5",
"@types/node-schedule": "2.1.0",
"@types/node-schedule": "2.1.8",
"@types/nodemailer": "6.4.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "4.4.5",
"@types/react-transition-group": "4.4.12",
"@types/secure-random-password": "0.2.1",
"@types/semver": "7.3.13",
"@types/swagger-ui-express": "4.1.3",
"@types/validator": "^13.15.3",
"@types/web-push": "3.3.2",
"@types/semver": "7.7.1",
"@types/swagger-ui-express": "4.1.8",
"@types/validator": "^13.15.10",
"@types/web-push": "3.6.4",
"@types/xml2js": "0.4.11",
"@types/yamljs": "0.2.31",
"@types/yup": "0.29.14",
"@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.54.0",
"autoprefixer": "10.4.13",
"commitizen": "4.3.0",
"autoprefixer": "10.4.22",
"baseline-browser-mapping": "^2.8.32",
"commitizen": "4.3.1",
"copyfiles": "2.4.1",
"cy-mobile-commands": "0.3.0",
"cypress": "14.1.0",
@@ -157,22 +158,22 @@
"eslint-config-next": "^14.2.4",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-formatjs": "4.9.0",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-no-relative-import-paths": "1.5.2",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.3",
"lint-staged": "13.1.2",
"nodemon": "3.1.9",
"postcss": "8.4.31",
"nodemon": "3.1.11",
"postcss": "8.5.6",
"prettier": "2.8.4",
"prettier-plugin-organize-imports": "3.2.2",
"prettier-plugin-tailwindcss": "0.2.3",
"tailwindcss": "3.2.7",
"ts-node": "10.9.1",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"ts-node": "10.9.2",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "4.9.5"
},
"engines": {
@@ -181,7 +182,7 @@
},
"overrides": {
"sqlite3/node-gyp": "8.4.1",
"@types/express-session": "1.17.6"
"@types/express-session": "1.18.2"
},
"config": {
"commitizen": {
@@ -204,8 +205,11 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"sqlite3",
"bcrypt"
"@swc/core",
"bcrypt",
"cypress",
"sharp",
"sqlite3"
]
}
}

6806
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7755,32 +7755,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/OverrideRule'
/overrideRule/advancedRequest:
post:
summary: Advanced override rule request
description: Processes an advanced override rule request.
tags:
- overriderule
responses:
'200':
description: Advanced override rule request processed
content:
application/json:
schema:
type: object
properties:
rootFolder:
type: string
nullable: true
profileId:
type: number
nullable: true
tags:
type: array
items:
type: number
nullable: true
security:
- cookieAuth: []
- apiKey: []

View File

@@ -145,7 +145,7 @@ class JellyfinAPI extends ExternalAPI {
{},
{
headers: {
'X-Emby-Authorization': authHeaderVal,
Authorization: authHeaderVal,
'Content-Type': 'application/json',
Accept: 'application/json',
},

View File

@@ -1,13 +1,15 @@
import TheMovieDb from '@server/api/themoviedb';
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { getRepository } from '@server/datasource';
import OverrideRule from '@server/entity/OverrideRule';
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
import notificationManager, { Notification } from '@server/lib/notifications';
import overrideRules from '@server/lib/overrideRules';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
@@ -209,20 +211,121 @@ export class MediaRequest {
let tags = requestBody.tags;
if (useOverrides) {
const overrideRulesResult = await overrideRules({
mediaType: requestBody.mediaType,
is4k: requestBody.is4k || false,
tmdbMedia,
requestUser,
const defaultRadarrId = requestBody.is4k
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
const defaultSonarrId = requestBody.is4k
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
const overrideRuleRepository = getRepository(OverrideRule);
const overrideRules = await overrideRuleRepository.find({
where:
requestBody.mediaType === MediaType.MOVIE
? { radarrServiceId: defaultRadarrId }
: { sonarrServiceId: defaultSonarrId },
});
if (overrideRulesResult.rootFolder) {
rootFolder = overrideRulesResult.rootFolder;
}
if (overrideRulesResult.profileId) {
profileId = overrideRulesResult.profileId;
}
if (overrideRulesResult.tags) {
tags = overrideRulesResult.tags;
const appliedOverrideRules = overrideRules.filter((rule) => {
const hasAnimeKeyword =
'results' in tmdbMedia.keywords &&
tmdbMedia.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
);
// Skip override rules if the media is an anime TV show as anime TV
// is handled by default and override rules do not explicitly include
// the anime keyword
if (
requestBody.mediaType === MediaType.TV &&
hasAnimeKeyword &&
(!rule.keywords ||
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
) {
return false;
}
if (
rule.users &&
!rule.users
.split(',')
.some((userId) => Number(userId) === requestUser.id)
) {
return false;
}
if (
rule.genre &&
!rule.genre
.split(',')
.some((genreId) =>
tmdbMedia.genres
.map((genre) => genre.id)
.includes(Number(genreId))
)
) {
return false;
}
if (
rule.language &&
!rule.language
.split('|')
.some((languageId) => languageId === tmdbMedia.original_language)
) {
return false;
}
if (
rule.keywords &&
!rule.keywords.split(',').some((keywordId) => {
let keywordList: TmdbKeyword[] = [];
if ('keywords' in tmdbMedia.keywords) {
keywordList = tmdbMedia.keywords.keywords;
} else if ('results' in tmdbMedia.keywords) {
keywordList = tmdbMedia.keywords.results;
}
return keywordList
.map((keyword: TmdbKeyword) => keyword.id)
.includes(Number(keywordId));
})
) {
return false;
}
return true;
});
// hacky way to prioritize rules
// TODO: make this better
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
const aSpecificity = keys.filter((key) => a[key] !== null).length;
const bSpecificity = keys.filter((key) => b[key] !== null).length;
// Take the rule with the most specific condition first
return bSpecificity - aSpecificity;
})[0];
if (prioritizedRule) {
if (prioritizedRule.rootFolder) {
rootFolder = prioritizedRule.rootFolder;
}
if (prioritizedRule.profileId) {
profileId = prioritizedRule.profileId;
}
if (prioritizedRule.tags) {
tags = [
...new Set([
...(tags || []),
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
]),
];
}
logger.debug('Override rule applied.', {
label: 'Media Request',
overrides: prioritizedRule,
});
}
}

View File

@@ -1,152 +0,0 @@
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
import type {
TmdbKeyword,
TmdbMovieDetails,
TmdbTvDetails,
} from '@server/api/themoviedb/interfaces';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import OverrideRule from '@server/entity/OverrideRule';
import type { User } from '@server/entity/User';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
export type OverrideRulesResult = {
rootFolder: string | null;
profileId: number | null;
tags: number[] | null;
};
async function overrideRules({
mediaType,
is4k,
tmdbMedia,
requestUser,
}: {
mediaType: MediaType;
is4k: boolean;
tmdbMedia: TmdbMovieDetails | TmdbTvDetails;
requestUser: User;
}): Promise<OverrideRulesResult> {
const settings = getSettings();
let rootFolder: string | null = null;
let profileId: number | null = null;
let tags: number[] | null = null;
const defaultRadarrId = is4k
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
const defaultSonarrId = is4k
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
const overrideRuleRepository = getRepository(OverrideRule);
const overrideRules = await overrideRuleRepository.find({
where:
mediaType === MediaType.MOVIE
? { radarrServiceId: defaultRadarrId }
: { sonarrServiceId: defaultSonarrId },
});
const appliedOverrideRules = overrideRules.filter((rule) => {
const hasAnimeKeyword =
'results' in tmdbMedia.keywords &&
tmdbMedia.keywords.results.some(
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
);
// Skip override rules if the media is an anime TV show as anime TV
// is handled by default and override rules do not explicitly include
// the anime keyword
if (
mediaType === MediaType.TV &&
hasAnimeKeyword &&
(!rule.keywords ||
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
) {
return false;
}
if (
rule.users &&
!rule.users.split(',').some((userId) => Number(userId) === requestUser.id)
) {
return false;
}
if (
rule.genre &&
!rule.genre
.split(',')
.some((genreId) =>
tmdbMedia.genres.map((genre) => genre.id).includes(Number(genreId))
)
) {
return false;
}
if (
rule.language &&
!rule.language
.split('|')
.some((languageId) => languageId === tmdbMedia.original_language)
) {
return false;
}
if (
rule.keywords &&
!rule.keywords.split(',').some((keywordId) => {
let keywordList: TmdbKeyword[] = [];
if ('keywords' in tmdbMedia.keywords) {
keywordList = tmdbMedia.keywords.keywords;
} else if ('results' in tmdbMedia.keywords) {
keywordList = tmdbMedia.keywords.results;
}
return keywordList
.map((keyword: TmdbKeyword) => keyword.id)
.includes(Number(keywordId));
})
) {
return false;
}
return true;
});
// hacky way to prioritize rules
// TODO: make this better
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
const aSpecificity = keys.filter((key) => a[key] !== null).length;
const bSpecificity = keys.filter((key) => b[key] !== null).length;
// Take the rule with the most specific condition first
return bSpecificity - aSpecificity;
})[0];
if (prioritizedRule) {
if (prioritizedRule.rootFolder) {
rootFolder = prioritizedRule.rootFolder;
}
if (prioritizedRule.profileId) {
profileId = prioritizedRule.profileId;
}
if (prioritizedRule.tags) {
tags = [
...new Set([
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
]),
];
}
logger.debug('Override rule applied.', {
label: 'Media Request',
overrides: prioritizedRule,
});
}
return { rootFolder, profileId, tags };
}
export default overrideRules;

View File

@@ -1,12 +1,6 @@
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import OverrideRule from '@server/entity/OverrideRule';
import { User } from '@server/entity/User';
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
import overrideRules, {
type OverrideRulesResult,
} from '@server/lib/overrideRules';
import { Permission } from '@server/lib/permissions';
import { isAuthenticated } from '@server/middleware/auth';
import { Router } from 'express';
@@ -67,40 +61,6 @@ overrideRuleRoutes.post<
}
});
overrideRuleRoutes.post(
'/advancedRequest',
isAuthenticated(Permission.REQUEST_ADVANCED),
async (req, res, next) => {
try {
const tmdb = new TheMovieDb();
const tmdbMedia =
req.body.mediaType === MediaType.MOVIE
? await tmdb.getMovie({ movieId: req.body.tmdbId })
: await tmdb.getTvShow({ tvId: req.body.tmdbId });
const userRepository = getRepository(User);
const user = await userRepository.findOne({
where: { id: req.body.requestUser },
relations: { requests: true },
});
if (!user) {
return next({ status: 404, message: 'User not found.' });
}
const overrideRulesResult: OverrideRulesResult = await overrideRules({
mediaType: req.body.mediaType,
is4k: req.body.is4k,
tmdbMedia,
requestUser: user,
});
res.status(200).json(overrideRulesResult);
} catch {
next({ status: 404, message: 'Media not found' });
}
}
);
overrideRuleRoutes.put<
{ ruleId: string },
OverrideRule,

View File

@@ -25,7 +25,7 @@ const LabeledCheckbox: React.FC<LabeledCheckboxProps> = ({
<Field type="checkbox" id={id} name={id} onChange={onChange} />
</div>
<div className="ml-3 text-sm leading-6">
<label htmlFor="localLogin" className="block">
<label htmlFor="localLogin" className="block" aria-label={label}>
<div className="flex flex-col">
<span className="font-medium text-white">{label}</span>
<span className="font-normal text-gray-400">{description}</span>

View File

@@ -46,7 +46,7 @@ const NotificationType = ({
/>
</div>
<div className="ml-3 text-sm leading-6">
<label htmlFor={option.id} className="block">
<label htmlFor={option.id} className="block" aria-label={option.name}>
<div className="flex flex-col">
<span className="font-medium text-white">{option.name}</span>
<span className="font-normal text-gray-400">

View File

@@ -123,7 +123,7 @@ const PermissionOption = ({
/>
</div>
<div className="ml-3 text-sm leading-6">
<label htmlFor={option.id} className="block">
<label htmlFor={option.id} className="block" aria-label={option.name}>
<div className="flex flex-col">
<span className="font-medium text-white">{option.name}</span>
<span className="font-normal text-gray-400">

View File

@@ -13,9 +13,7 @@ import type {
ServiceCommonServerWithDetails,
} from '@server/interfaces/api/serviceInterfaces';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import type { OverrideRulesResult } from '@server/lib/overrideRules';
import { hasPermission } from '@server/lib/permissions';
import axios from 'axios';
import { isEqual } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -53,7 +51,6 @@ export type RequestOverrides = {
interface AdvancedRequesterProps {
type: 'movie' | 'tv';
tmdbId?: number;
is4k: boolean;
isAnime?: boolean;
defaultOverrides?: RequestOverrides;
@@ -63,7 +60,6 @@ interface AdvancedRequesterProps {
const AdvancedRequester = ({
type,
tmdbId,
is4k = false,
isAnime = false,
defaultOverrides,
@@ -288,35 +284,6 @@ const AdvancedRequester = ({
selectedTags,
]);
useEffect(() => {
(async () => {
if (tmdbId) {
try {
const { data: override } = await axios.post<OverrideRulesResult>(
'/api/v1/overrideRule/advancedRequest',
{
mediaType: type,
is4k,
requestUser: requestUser?.id ?? currentUser?.id,
tmdbId,
}
);
if (override.rootFolder) {
setSelectedFolder(override.rootFolder);
}
if (override.profileId) {
setSelectedProfile(override.profileId);
}
if (override.tags) {
setSelectedTags(override.tags);
}
} catch {
/* empty */
}
}
})();
}, [serverData, is4k, requestUser]);
if (!data && !error) {
return (
<div className="mb-2 w-full">

View File

@@ -288,7 +288,6 @@ const MovieRequestModal = ({
hasPermission(Permission.MANAGE_REQUESTS)) && (
<AdvancedRequester
type="movie"
tmdbId={tmdbId}
is4k={is4k}
requestUser={editRequest.requestedBy}
defaultOverrides={{
@@ -358,7 +357,6 @@ const MovieRequestModal = ({
{(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && (
<AdvancedRequester
tmdbId={tmdbId}
type="movie"
is4k={is4k}
onChange={(overrides) => {

View File

@@ -722,7 +722,6 @@ const TvRequestModal = ({
hasPermission(Permission.MANAGE_REQUESTS)) && (
<AdvancedRequester
type="tv"
tmdbId={tmdbId}
is4k={is4k}
isAnime={data?.keywords.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID

View File

@@ -12,7 +12,6 @@ import type {
TmdbGenre,
TmdbKeywordSearchResponse,
} from '@server/api/themoviedb/interfaces';
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import type {
Keyword,
@@ -185,9 +184,7 @@ export const GenreSelector = ({
}, [defaultValue, type]);
const loadGenreOptions = async (inputValue: string) => {
const results = await axios.get<GenreSliderItem[]>(
`/api/v1/discover/genreslider/${type}`
);
const results = await axios.get<TmdbGenre[]>(`/api/v1/genres/${type}`);
return results.data
.map((result) => ({
@@ -201,7 +198,7 @@ export const GenreSelector = ({
return (
<AsyncSelect
key={`genre-select-${defaultDataValue}`}
key={`genre-select-${type}-${defaultDataValue}`}
className="react-select-container"
classNamePrefix="react-select"
defaultValue={isMulti ? defaultDataValue : defaultDataValue?.[0]}

View File

@@ -337,7 +337,13 @@ const OverrideRuleModal = ({
<div className="form-input-area">
<div className="form-input-field">
<GenreSelector
type={values.radarrServiceId ? 'movie' : 'tv'}
type={
values.radarrServiceId != null
? 'movie'
: values.sonarrServiceId != null
? 'tv'
: 'tv'
}
defaultValue={values.genre}
isMulti
isDisabled={!isValidated || isTesting}

View File

@@ -63,7 +63,7 @@ class PlexOAuth {
'X-Plex-Client-Identifier': clientId,
'X-Plex-Model': 'Plex OAuth',
'X-Plex-Platform': browser.getBrowserName(),
'X-Plex-Platform-Version': browser.getBrowserVersion(),
'X-Plex-Platform-Version': browser.getBrowserVersion() || 'Unknown',
'X-Plex-Device': browser.getOSName(),
'X-Plex-Device-Name': `${browser.getBrowserName()} (Seerr)`,
'X-Plex-Device-Screen-Resolution':