Compare commits

...

21 Commits

Author SHA1 Message Date
fallenbagel
8fa68da481 refactor(serviceavailabilitychecker): add missing closing parenthesis in debug log 2025-12-30 05:01:03 +08:00
fallenbagel
cf5a85ba0b refactor(serviceavailabilitychecker): correct set initialization for season numbers 2025-12-30 04:59:14 +08:00
fallenbagel
9cbd5f4260 refactor(jellyfin-scanner): correct variable name for 4k availability checks 2025-12-30 04:57:08 +08:00
fallenbagel
09233a32b3 fix(jellyfin-scanner): use service instance for 4k availability detection
when 4k services are enabled, jellyfin scanner will now check which arr instance has the file
todetermine availability tier instead of relying solely on resolution detection. This should fix
theincorrecta availability status when a 4k request results in a lower resolution file. Fallbacks
tooriginal resolution based detection when media not found.

fix #1744
2025-12-29 23:29:27 +08:00
fallenbagel
57d583e1bd refactor(jellyfin-scanner): extend BaseScanner for jellyfin scanner (#2226)
* refactor(jellyfin-scanner): extend BaseScanner for jellyfin scanner

Refactors JellyfinScanner to extend BaseScanner class to align the jellyfin scanner architecture
with the plex scanner and reduce code duplication.

* fix(jellyfin-scanner): add imdbId handling back to fix a regression from original behaviour

* fix: add imdbId assignment for existing media entries

* fix: include imdbId in processed 4k media items and improve 4k detection

* fix(jellyfin-scanner): filter seasons based on settings for special episodes (regression)
2025-12-29 20:05:47 +08:00
samohtxotom
8bbe7864af chore(metadata-settings): add autoDismiss to toast notifications (#2254) 2025-12-27 06:27:12 +08:00
Gauthier
66b4e2c871 chore(issuetemplate): add a checkbox to search for existing issues (#2255) 2025-12-27 06:26:16 +08:00
fallenbagel
3ee69663dc fix(local-login): remove automatic plex linking and reduce logout log verbosity (#2225)
Removed redundant Plex user discovery logic that applies to all media servers currently. This is now
handled explicitly via linked accounts settings page. Also changed the successful logout log level
from info to debug since its routine behaviour
2025-12-15 19:44:43 +08:00
Ludovic Ortega
539d49879d chore: fix translate badge svg url (#2228)
* chore: fix translate badge svg url

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* fix: use https instead of http

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

---------

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2025-12-14 05:37:36 +08:00
RolliePollie18
15356dfe49 fix(jellyfin-scan): reduce jellyfin API calls during recently added scan (#2205)
* fix(jellyfin scanner): reduce jellyfin API calls during recently added scan

Significantly reduce number of API calls, addressing CPU spikes on Jellyfin 10.10+ servers.- Move
getSeasons() call outside the seasons loop (N calls to 1)- Request MediaSources via getEpisodes()
field parameter instead of  individual getItemData() calls per episode (N calls to 1 per season)
Performance improvements (tested on library with 12 TV shows):- Scan duration: 43.7s to 9.1s - Peak
CPU: 277% to 115% - CPU spike duration: 36s to 2s Functionality is unchanged, all availability
statuses identicalbefore and after.

* fix: add getEpisodes overloads to remove unsafe type assertion

* refactor(jellyfin): use generics instead of overloads

---------

Co-authored-by: patrick-acland <patrick.acland@kraken.tech>
2025-12-09 22:20:47 +08:00
fallenbagel
1f04eeb040 fix: disable automatic auth revalidation on auth pages (#2213)
* fix: disable automatic auth revalidation on auth pages

Prevents unnecessary `/api/v1/auth/me` requests on login, setup, and password reset pages.

fix #738

* fix: update regex to include resetpassword guid & add missing condition in refreshInterval
2025-12-09 13:17:17 +01:00
Thibaut Noah
e3028c21f2 docs: add webpush related troubleshooting steps (#2170)
* Update troubleshooting.mdx

Add potential fixes for users who fail to enable their web push notifications

* Update docs/troubleshooting.mdx

Modify appName syntax for better coding norm

Co-authored-by: Gauthier <mail@gauthierth.fr>

* refactor: apply suggestions from review comments

Co-authored-by: Gauthier <mail@gauthierth.fr>

* docs(troubleshooting): fix typos in troubleshooting doc page

---------

Co-authored-by: Gauthier <mail@gauthierth.fr>
Co-authored-by: fallenbagel <98979876+fallenbagel@users.noreply.github.com>
2025-12-09 08:49:42 +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
28 changed files with 3112 additions and 3670 deletions

View File

@@ -91,6 +91,14 @@ body:
attributes:
label: Additional Context
description: Please provide any additional information that may be relevant or helpful.
- type: checkboxes
id: search-existing
attributes:
label: Search Existing Issues
description: Have you searched existing issues to see if this bug has already been reported?
options:
- label: Yes, I have searched existing issues.
required: true
- type: checkboxes
id: terms
attributes:

View File

@@ -27,6 +27,14 @@ body:
attributes:
label: Additional Context
description: Provide any additional information or screenshots that may be relevant or helpful.
- type: checkboxes
id: search-existing
attributes:
label: Search Existing Issues
description: Have you searched existing issues to see if this feature has already been requested?
options:
- label: Yes, I have searched existing issues.
required: true
- type: checkboxes
id: terms
attributes:

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/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

@@ -174,4 +174,36 @@ This can happen if you have a new installation of Jellyfin/Emby or if you have c
This process should restore your admin privileges while preserving your settings.
## Failed to enable web push notifications
### Option 1: You are using Pi-hole
When using Pi-hole, you need to whitelist the proper domains in order for the queries to not be intercepted and blocked by Pi-hole.
If you are using a chromium based browser (eg: Chrome, Brave, Edge...), the domain you need to whitelist is `fcm.googleapis.com`
If you are using Firefox, the domain you need to whitelist is `push.services.mozilla.com`
1. Log into your Pi-hole through the admin interface, then click on Domains situated under GROUP MANAGEMENT.
2. Add the domain corresponding to your browser in the `Domain to be added` field and then click on Add to allowed domains.
3. Now in order for those changes to be used you need to flush your current dns cache.
4. You can do so by using this command line in your Pi-hole terminal:
```bash
pihole restartdns
```
If this command fails (which is unlikely), use this equivalent:
```bash
pihole -f && pihole restartdns
```
5. Then restart your Seerr instance and try to enable the web push notifications again.
### Option 2: You are using Brave browser
Brave is a "De-Googled" browser. So by default or if you refused a prompt in the past, it cuts the access to the FCM (Firebase Cloud Messaging) service, which is mandatory for the web push notifications on Chromium based browsers.
1. Open Brave and paste this address in the url bar: `brave://settings/privacy`
2. Look for the option: "Use Google services for push messaging"
3. Activate this option
4. Relaunch Brave completely
5. You should now see the notifications prompt appearing instead of an error message.
If you still encounter issues, please reach out on our support channels.

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",
"@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"
]
}
}

4120
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -112,6 +112,10 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
DateCreated?: string;
}
type EpisodeReturn<T> = T extends { includeMediaInfo: true }
? JellyfinLibraryItemExtended[]
: JellyfinLibraryItem[];
export interface JellyfinItemsReponse {
Items: JellyfinLibraryItemExtended[];
TotalRecordCount: number;
@@ -145,7 +149,7 @@ class JellyfinAPI extends ExternalAPI {
{},
{
headers: {
'X-Emby-Authorization': authHeaderVal,
Authorization: authHeaderVal,
'Content-Type': 'application/json',
Accept: 'application/json',
},
@@ -415,13 +419,22 @@ class JellyfinAPI extends ExternalAPI {
}
}
public async getEpisodes(
public async getEpisodes<
T extends { includeMediaInfo?: boolean } | undefined = undefined
>(
seriesID: string,
seasonID: string
): Promise<JellyfinLibraryItem[]> {
seasonID: string,
options?: T
): Promise<EpisodeReturn<T>> {
try {
const episodeResponse = await this.get<any>(
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
`/Shows/${seriesID}/Episodes`,
{
params: {
seasonId: seasonID,
...(options?.includeMediaInfo && { fields: 'MediaSources' }),
},
}
);
return episodeResponse.Items.filter(

View File

@@ -34,6 +34,8 @@ interface ProcessOptions {
is4k?: boolean;
mediaAddedAt?: Date;
ratingKey?: string;
jellyfinMediaId?: string;
imdbId?: string;
serviceId?: number;
externalServiceId?: number;
externalServiceSlug?: string;
@@ -95,6 +97,8 @@ class BaseScanner<T> {
is4k = false,
mediaAddedAt,
ratingKey,
jellyfinMediaId,
imdbId,
serviceId,
externalServiceId,
externalServiceSlug,
@@ -133,6 +137,21 @@ class BaseScanner<T> {
changedExisting = true;
}
if (
jellyfinMediaId &&
existing[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] !==
jellyfinMediaId
) {
existing[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
jellyfinMediaId;
changedExisting = true;
}
if (imdbId && !existing.imdbId) {
existing.imdbId = imdbId;
changedExisting = true;
}
if (
serviceId !== undefined &&
existing[is4k ? 'serviceId4k' : 'serviceId'] !== serviceId
@@ -173,6 +192,7 @@ class BaseScanner<T> {
} else {
const newMedia = new Media();
newMedia.tmdbId = tmdbId;
newMedia.imdbId = imdbId;
newMedia.status =
!is4k && !processing
@@ -203,6 +223,13 @@ class BaseScanner<T> {
newMedia.ratingKey4k =
is4k && this.enable4kMovie ? ratingKey : undefined;
}
if (jellyfinMediaId) {
newMedia.jellyfinMediaId = !is4k ? jellyfinMediaId : undefined;
newMedia.jellyfinMediaId4k =
is4k && this.enable4kMovie ? jellyfinMediaId : undefined;
}
await mediaRepository.save(newMedia);
this.log(`Saved new media: ${title}`);
}
@@ -221,11 +248,12 @@ class BaseScanner<T> {
*/
protected async processShow(
tmdbId: number,
tvdbId: number,
tvdbId: number | undefined,
seasons: ProcessableSeason[],
{
mediaAddedAt,
ratingKey,
jellyfinMediaId,
serviceId,
externalServiceId,
externalServiceSlug,
@@ -257,7 +285,7 @@ class BaseScanner<T> {
(es) => es.seasonNumber === season.seasonNumber
);
// We update the rating keys in the seasons loop because we need episode counts
// We update the rating keys and jellyfinMediaId in the seasons loop because we need episode counts
if (media && season.episodes > 0 && media.ratingKey !== ratingKey) {
media.ratingKey = ratingKey;
}
@@ -271,6 +299,23 @@ class BaseScanner<T> {
media.ratingKey4k = ratingKey;
}
if (
media &&
season.episodes > 0 &&
media.jellyfinMediaId !== jellyfinMediaId
) {
media.jellyfinMediaId = jellyfinMediaId;
}
if (
media &&
season.episodes4k > 0 &&
this.enable4kShow &&
media.jellyfinMediaId4k !== jellyfinMediaId
) {
media.jellyfinMediaId4k = jellyfinMediaId;
}
if (existingSeason) {
// Here we update seasons if they already exist.
// If the season is already marked as available, we
@@ -491,6 +536,22 @@ class BaseScanner<T> {
)
? ratingKey
: undefined,
jellyfinMediaId: newSeasons.some(
(sn) =>
sn.status === MediaStatus.PARTIALLY_AVAILABLE ||
sn.status === MediaStatus.AVAILABLE
)
? jellyfinMediaId
: undefined,
jellyfinMediaId4k:
this.enable4kShow &&
newSeasons.some(
(sn) =>
sn.status4k === MediaStatus.PARTIALLY_AVAILABLE ||
sn.status4k === MediaStatus.AVAILABLE
)
? jellyfinMediaId
: undefined,
status: isAllStandardSeasons
? MediaStatus.AVAILABLE
: newSeasons.some(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,193 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
interface InstanceAvailability {
hasStandard: boolean;
has4k: boolean;
serviceStandardId?: number;
service4kId?: number;
externalStandardId?: number;
external4kId?: number;
}
interface SeasonInstanceAvailability {
seasonNumber: number;
episodesStandard: number;
episodes4k: number;
}
interface ShowInstanceAvailability extends InstanceAvailability {
seasons: SeasonInstanceAvailability[];
}
class ServiceAvailabilityChecker {
private movieCache: Map<number, InstanceAvailability>;
private showCache: Map<number, ShowInstanceAvailability>;
constructor() {
this.movieCache = new Map();
this.showCache = new Map();
}
public clearCache(): void {
this.movieCache.clear();
this.showCache.clear();
}
public async checkMovieAvailability(
tmdbid: number
): Promise<InstanceAvailability> {
const cached = this.movieCache.get(tmdbid);
if (cached) {
return cached;
}
const settings = getSettings();
const result: InstanceAvailability = {
hasStandard: false,
has4k: false,
};
if (!settings.radarr || settings.radarr.length === 0) {
return result;
}
for (const radarrSettings of settings.radarr) {
try {
const radarr = this.createRadarrClient(radarrSettings);
const movie = await radarr.getMovieByTmdbId(tmdbid);
if (movie?.hasFile) {
if (radarrSettings.is4k) {
result.has4k = true;
result.service4kId = radarrSettings.id;
result.external4kId = movie.id;
} else {
result.hasStandard = true;
result.serviceStandardId = radarrSettings.id;
result.externalStandardId = movie.id;
}
}
logger.debug(
`Found movie (TMDB: ${tmdbid}) in ${
radarrSettings.is4k ? '4K' : 'Standard'
} Radarr instance (name: ${radarrSettings.name})`,
{
label: 'Service Availability',
radarrId: radarrSettings.id,
movieId: movie?.id,
}
);
} catch {
// movie not found in this instance, continue
}
}
this.movieCache.set(tmdbid, result);
return result;
}
public async checkShowAvailability(
tvdbid: number
): Promise<ShowInstanceAvailability> {
const cached = this.showCache.get(tvdbid);
if (cached) {
return cached;
}
const settings = getSettings();
const result: ShowInstanceAvailability = {
hasStandard: false,
has4k: false,
seasons: [],
};
if (!settings.sonarr || settings.sonarr.length === 0) {
return result;
}
const standardSeasons = new Map<number, number>();
const seasons4k = new Map<number, number>();
for (const sonarrSettings of settings.sonarr) {
try {
const sonarr = this.createSonarrClient(sonarrSettings);
const series = await sonarr.getSeriesByTvdbId(tvdbid);
if (series?.id && series.statistics?.episodeFileCount > 0) {
if (sonarrSettings.is4k) {
result.has4k = true;
result.service4kId = sonarrSettings.id;
result.external4kId = series.id;
} else {
result.hasStandard = true;
result.serviceStandardId = sonarrSettings.id;
result.externalStandardId = series.id;
}
for (const season of series.seasons) {
const episodeCount = season.statistics?.episodeFileCount ?? 0;
if (episodeCount > 0) {
const targetMap = sonarrSettings.is4k
? seasons4k
: standardSeasons;
const current = targetMap.get(season.seasonNumber) ?? 0;
targetMap.set(
season.seasonNumber,
Math.max(current, episodeCount)
);
}
}
logger.debug(
`Found series (TVDB: ${tvdbid}) in ${
sonarrSettings.is4k ? '4K' : 'Standard'
} Sonarr instance (name: ${sonarrSettings.name})`,
{
label: 'Service Availability',
sonarrId: sonarrSettings.id,
seriesId: series.id,
}
);
}
} catch {
// series not found in this instance, continue
}
}
const allSeasonNumbers = new Set([
...standardSeasons.keys(),
...seasons4k.keys(),
]);
result.seasons = Array.from(allSeasonNumbers).map((seasonNumber) => ({
seasonNumber,
episodesStandard: standardSeasons.get(seasonNumber) ?? 0,
episodes4k: seasons4k.get(seasonNumber) ?? 0,
}));
this.showCache.set(tvdbid, result);
return result;
}
private createRadarrClient(settings: RadarrSettings): RadarrAPI {
return new RadarrAPI({
url: RadarrAPI.buildUrl(settings, '/api/v3'),
apiKey: settings.apiKey,
});
}
private createSonarrClient(settings: SonarrSettings): SonarrAPI {
return new SonarrAPI({
url: SonarrAPI.buildUrl(settings, '/api/v3'),
apiKey: settings.apiKey,
});
}
}
const serviceAvailabilityChecker = new ServiceAvailabilityChecker();
export default serviceAvailabilityChecker;

View File

@@ -626,76 +626,6 @@ authRoutes.post('/local', async (req, res, next) => {
});
}
const mainUser = await userRepository.findOneOrFail({
select: { id: true, plexToken: true, plexId: true },
where: { id: 1 },
});
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
if (!user.plexId) {
try {
const plexUsersResponse = await mainPlexTv.getUsers();
const account = plexUsersResponse.MediaContainer.User.find(
(account) =>
account.$.email &&
account.$.email.toLowerCase() === user.email.toLowerCase()
)?.$;
if (
account &&
(await mainPlexTv.checkUserAccess(parseInt(account.id)))
) {
logger.info(
'Found matching Plex user; updating user with Plex data',
{
label: 'API',
ip: req.ip,
email: body.email,
userId: user.id,
plexId: account.id,
plexUsername: account.username,
}
);
user.plexId = parseInt(account.id);
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
user.userType = UserType.PLEX;
await userRepository.save(user);
}
} catch (e) {
logger.error('Something went wrong fetching Plex users', {
label: 'API',
errorMessage: e.message,
});
}
}
if (
user.plexId &&
user.plexId !== mainUser.plexId &&
!(await mainPlexTv.checkUserAccess(user.plexId))
) {
logger.warn(
'Failed sign-in attempt from Plex user without access to the media server',
{
label: 'API',
account: {
ip: req.ip,
email: body.email,
userId: user.id,
plexId: user.plexId,
},
}
);
return next({
status: 403,
message: 'Access denied.',
});
}
// Set logged in session
if (user && req.session) {
req.session.userId = user.id;
@@ -775,7 +705,7 @@ authRoutes.post('/logout', async (req, res, next) => {
});
return next({ status: 500, message: 'Failed to destroy session.' });
}
logger.info('Successfully logged out user', {
logger.debug('Successfully logged out user', {
label: 'Auth',
userId,
});

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

@@ -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

@@ -320,12 +320,14 @@ const SettingsMetadata = () => {
addToast(intl.formatMessage(messages.metadataSettingsSaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(
intl.formatMessage(messages.failedToSaveMetadataSettings),
{
appearance: 'error',
autoDismiss: true,
}
);
}
@@ -422,6 +424,7 @@ const SettingsMetadata = () => {
),
{
appearance: 'success',
autoDismiss: true,
}
);
}

View File

@@ -2,6 +2,7 @@ import { UserType } from '@server/constants/user';
import type { PermissionCheckOptions } from '@server/lib/permissions';
import { hasPermission, Permission } from '@server/lib/permissions';
import type { NotificationAgentKey } from '@server/lib/settings';
import { useRouter } from 'next/router';
import type { MutatorCallback } from 'swr';
import useSWR from 'swr';
@@ -56,13 +57,21 @@ export const useUser = ({
id,
initialData,
}: { id?: number; initialData?: User } = {}): UserHookResponse => {
const router = useRouter();
const isAuthPage = /^\/(login|setup|resetpassword(?:\/|$))/.test(
router.pathname
);
const {
data,
error,
mutate: revalidate,
} = useSWR<User>(id ? `/api/v1/user/${id}` : `/api/v1/auth/me`, {
fallbackData: initialData,
refreshInterval: 30000,
refreshInterval: !isAuthPage ? 30000 : 0,
revalidateOnFocus: !isAuthPage,
revalidateOnMount: !isAuthPage,
revalidateOnReconnect: !isAuthPage,
errorRetryInterval: 30000,
shouldRetryOnError: false,
});

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':