Compare commits

..

1 Commits

Author SHA1 Message Date
0xsysr3ll
d57af7334c fix(requests): mark requests as failed when Radarr/Sonarr unreachable
This PR fixes requests silently failing when Radarr/Sonarr are unreachable. Requests are now marked as `FAILED` and notifications are sent to alert both users and admins.

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
2025-11-23 17:42:54 +01:00
25 changed files with 1979 additions and 2759 deletions

View File

@@ -1,33 +1,14 @@
<!--
Please read contributing guide before submitting
your pull request. Please fill in each section below to help us better prioritize your pull request. Thanks!
-->
#### Description
## Description
#### Screenshot (if UI-related)
<!--- 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. -->
#### To-Dos
- 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

@@ -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="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/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: '3.0.0'
appVersion: '2.7.3'
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: 3.0.0](https://img.shields.io/badge/AppVersion-3.0.0-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: 2.7.3](https://img.shields.io/badge/AppVersion-2.7.3-informational?style=flat-square)
Seerr helm chart for Kubernetes
@@ -22,7 +22,7 @@ Kubernetes: `>=1.23.0-0`
## Installation
Refer to [Seerr kubernetes documentation](https://docs.seerr.dev/getting-started/kubernetes)
Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation)
## Update Notes

View File

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

View File

@@ -26,7 +26,8 @@ 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 .
git clone https://github.com/seerr-team/seerr.git
cd seerr
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
docker run --rm -v /path/to/appdata/config:/data alpine chown -R 1000:1000 /data
sudo chown -R 1000:1000 /path/to/appdata/config
```
This ensures the `node` user (UID 1000) owns the config directory and can read and write to it.

View File

@@ -174,36 +174,4 @@ 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

@@ -22,17 +22,6 @@ This is typically not needed. Please refer to your webhook provider's documentat
This value will be sent as an `Authorization` HTTP header.
### Custom Headers (optional)
You can add additional custom HTTP headers to be sent with each webhook request. This is useful for API keys, custom authentication schemes, or any other headers your webhook endpoint requires.
- Click "Add Header" to add a new header
- Enter the header name and value
:::warning
You cannot configure both the **Authorization Header** field and a custom `Authorization` header in Custom Headers at the same time. You must choose one method.
:::
### JSON Payload
Customize the JSON payload to suit your needs. Seerr provides several [template variables](#template-variables) for use in the payload, which will be replaced with the relevant data when the notifications are triggered.

View File

@@ -2,7 +2,7 @@
"name": "seerr",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@10.24.0",
"packageManager": "pnpm@10.17.1",
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "node postinstall-win.js",
@@ -33,38 +33,38 @@
},
"license": "MIT",
"dependencies": {
"@dr.pogodin/csurf": "^1.16.6",
"@formatjs/intl-displaynames": "6.8.13",
"@dr.pogodin/csurf": "^1.14.1",
"@formatjs/intl-displaynames": "6.2.6",
"@formatjs/intl-locale": "3.1.1",
"@formatjs/intl-pluralrules": "5.4.6",
"@formatjs/intl-pluralrules": "5.1.10",
"@formatjs/intl-utils": "3.8.4",
"@formatjs/swc-plugin-experimental": "^0.4.0",
"@headlessui/react": "1.7.12",
"@heroicons/react": "2.2.0",
"@heroicons/react": "2.0.16",
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.56",
"@tanem/react-nprogress": "5.0.30",
"@types/ua-parser-js": "^0.7.36",
"@types/wink-jaro-distance": "^2.0.2",
"ace-builds": "1.43.4",
"axios": "1.13.2",
"axios-rate-limit": "1.4.0",
"ace-builds": "1.15.2",
"axios": "1.10.0",
"axios-rate-limit": "1.3.0",
"bcrypt": "5.1.0",
"bowser": "2.13.1",
"bowser": "2.11.0",
"connect-typeorm": "1.1.4",
"cookie-parser": "1.4.7",
"copy-to-clipboard": "3.3.3",
"country-flag-icons": "1.6.4",
"country-flag-icons": "1.5.5",
"cronstrue": "2.23.0",
"date-fns": "2.29.3",
"dayjs": "1.11.19",
"dayjs": "1.11.7",
"dns-caching": "^0.2.7",
"email-templates": "12.0.3",
"email-templates": "12.0.1",
"express": "4.21.2",
"express-openapi-validator": "4.13.8",
"express-rate-limit": "6.7.0",
"express-session": "1.18.2",
"formik": "^2.4.9",
"express-session": "1.17.3",
"formik": "^2.4.6",
"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.16.3",
"pg": "8.11.0",
"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.44.0",
"react-aria": "3.23.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.10.2",
"react-select": "5.7.0",
"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.3",
"semver": "7.7.1",
"sharp": "^0.33.4",
"sqlite3": "5.1.7",
"swagger-ui-express": "4.6.2",
"swr": "2.3.7",
"swr": "2.2.5",
"tailwind-merge": "^2.6.0",
"typeorm": "0.3.12",
"ua-parser-js": "^1.0.35",
"undici": "^7.16.0",
"validator": "^13.15.23",
"web-push": "3.6.7",
"undici": "^7.3.0",
"validator": "^13.15.15",
"web-push": "3.5.0",
"wink-jaro-distance": "^2.0.0",
"winston": "3.18.3",
"winston": "3.8.2",
"winston-daily-rotate-file": "4.7.1",
"xml2js": "0.4.23",
"yamljs": "0.3.0",
@@ -123,33 +123,32 @@
"@tailwindcss/forms": "0.5.10",
"@tailwindcss/typography": "0.5.16",
"@types/bcrypt": "5.0.0",
"@types/cookie-parser": "1.4.10",
"@types/country-flag-icons": "1.2.2",
"@types/csurf": "1.11.5",
"@types/cookie-parser": "1.4.3",
"@types/country-flag-icons": "1.2.0",
"@types/csurf": "1.11.2",
"@types/email-templates": "8.0.4",
"@types/express": "4.17.17",
"@types/express-session": "1.18.2",
"@types/lodash": "4.17.21",
"@types/express-session": "1.17.6",
"@types/lodash": "4.14.191",
"@types/mime": "3",
"@types/node": "22.10.5",
"@types/node-schedule": "2.1.8",
"@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "4.4.12",
"@types/react-transition-group": "4.4.5",
"@types/secure-random-password": "0.2.1",
"@types/semver": "7.7.1",
"@types/swagger-ui-express": "4.1.8",
"@types/validator": "^13.15.10",
"@types/web-push": "3.6.4",
"@types/semver": "7.3.13",
"@types/swagger-ui-express": "4.1.3",
"@types/validator": "^13.15.3",
"@types/web-push": "3.3.2",
"@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.22",
"baseline-browser-mapping": "^2.8.32",
"commitizen": "4.3.1",
"autoprefixer": "10.4.13",
"commitizen": "4.3.0",
"copyfiles": "2.4.1",
"cy-mobile-commands": "0.3.0",
"cypress": "14.1.0",
@@ -158,22 +157,22 @@
"eslint-config-next": "^14.2.4",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-formatjs": "4.9.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-no-relative-import-paths": "1.5.2",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.3",
"lint-staged": "13.1.2",
"nodemon": "3.1.11",
"postcss": "8.5.6",
"nodemon": "3.1.9",
"postcss": "8.4.31",
"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.2",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"ts-node": "10.9.1",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"typescript": "4.9.5"
},
"engines": {
@@ -182,7 +181,7 @@
},
"overrides": {
"sqlite3/node-gyp": "8.4.1",
"@types/express-session": "1.18.2"
"@types/express-session": "1.17.6"
},
"config": {
"commitizen": {
@@ -205,11 +204,8 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"bcrypt",
"cypress",
"sharp",
"sqlite3"
"sqlite3",
"bcrypt"
]
}
}

4118
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -196,33 +196,16 @@ class WebhookAgent
}
try {
const headers: Record<string, string> = {};
if (settings.options.authHeader) {
headers.Authorization = settings.options.authHeader;
}
if (
settings.options.customHeaders &&
settings.options.customHeaders.length > 0
) {
settings.options.customHeaders.forEach((header) => {
if (header.key && header.value) {
// Don't override Authorization header if it's already set via authHeader
if (
header.key.toLowerCase() !== 'authorization' ||
!settings.options.authHeader
) {
headers[header.key] = header.value;
}
}
});
}
await axios.post(
webhookUrl,
this.buildPayload(type, payload),
Object.keys(headers).length > 0 ? { headers } : undefined
settings.options.authHeader
? {
headers: {
Authorization: settings.options.authHeader,
},
}
: undefined
);
return true;

View File

@@ -374,10 +374,9 @@ class JellyfinScanner {
) ?? []
).length;
const jellyfinSeasons = await this.jfClient.getSeasons(Id);
for (const season of seasons) {
const matchedJellyfinSeason = jellyfinSeasons.find((md) => {
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
const matchedJellyfinSeason = JellyfinSeasons.find((md) => {
if (tvdbSeasonFromAnidb) {
// In AniDB we don't have the concept of seasons,
// we have multiple shows with only Season 1 (and sometimes a season with index 0 for specials).
@@ -398,52 +397,38 @@ class JellyfinScanner {
// Check if we found the matching season and it has all the available episodes
if (matchedJellyfinSeason) {
// If we have a matched Jellyfin season, get its children metadata so we can check details
const episodes = await this.jfClient.getEpisodes(
Id,
matchedJellyfinSeason.Id
);
//Get count of episodes that are HD and 4K
let totalStandard = 0;
let total4k = 0;
if (!this.enable4kShow) {
const episodes = await this.jfClient.getEpisodes(
Id,
matchedJellyfinSeason.Id
);
//use for loop to make sure this loop _completes_ in full
//before the next section
for (const episode of episodes) {
let episodeCount = 1;
for (const episode of episodes) {
let episodeCount = 1;
// count number of combined episodes
if (
episode.IndexNumber !== undefined &&
episode.IndexNumberEnd !== undefined
) {
episodeCount =
episode.IndexNumberEnd - episode.IndexNumber + 1;
}
totalStandard += episodeCount;
// count number of combined episodes
if (
episode.IndexNumber !== undefined &&
episode.IndexNumberEnd !== undefined
) {
episodeCount =
episode.IndexNumberEnd - episode.IndexNumber + 1;
}
} else {
// 4K detection enabled - request media info to check resolution
const episodes = await this.jfClient.getEpisodes(
Id,
matchedJellyfinSeason.Id,
{ includeMediaInfo: true }
);
for (const episode of episodes) {
let episodeCount = 1;
if (!this.enable4kShow) {
totalStandard += episodeCount;
} else {
const ExtendedEpisodeData = await this.jfClient.getItemData(
episode.Id
);
// count number of combined episodes
if (
episode.IndexNumber !== undefined &&
episode.IndexNumberEnd !== undefined
) {
episodeCount =
episode.IndexNumberEnd - episode.IndexNumber + 1;
}
// MediaSources field is included in response when includeMediaInfo is true
// We iterate all MediaSources to detect if episode has both standard AND 4K versions
episode.MediaSources?.some((MediaSource) => {
ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
return MediaSource.MediaStreams.some((MediaStream) => {
if (MediaStream.Type === 'Video') {
if ((MediaStream.Width ?? 0) >= 2000) {

View File

@@ -275,7 +275,6 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
webhookUrl: string;
jsonPayload: string;
authHeader?: string;
customHeaders?: { key: string; value: string }[];
supportVariables?: boolean;
};
}

View File

@@ -626,6 +626,76 @@ 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;
@@ -705,7 +775,7 @@ authRoutes.post('/logout', async (req, res, next) => {
});
return next({ status: 500, message: 'Failed to destroy session.' });
}
logger.debug('Successfully logged out user', {
logger.info('Successfully logged out user', {
label: 'Auth',
userId,
});

View File

@@ -279,7 +279,6 @@ notificationRoutes.get('/webhook', (_req, res) => {
'utf8'
)
),
customHeaders: webhookSettings.options.customHeaders ?? [],
supportVariables: webhookSettings.options.supportVariables ?? false,
},
};
@@ -302,7 +301,6 @@ notificationRoutes.post('/webhook', async (req, res, next) => {
),
webhookUrl: req.body.options.webhookUrl,
authHeader: req.body.options.authHeader,
customHeaders: req.body.options.customHeaders ?? [],
supportVariables: req.body.options.supportVariables ?? false,
},
};
@@ -335,7 +333,6 @@ notificationRoutes.post('/webhook/test', async (req, res, next) => {
),
webhookUrl: req.body.options.webhookUrl,
authHeader: req.body.options.authHeader,
customHeaders: req.body.options.customHeaders ?? [],
supportVariables: req.body.options.supportVariables ?? false,
},
};

View File

@@ -420,13 +420,32 @@ export class MediaRequestSubscriber
mediaId: entity.media.id,
});
} catch (e) {
logger.error('Something went wrong sending request to Radarr', {
label: 'Media Request',
errorMessage: e.message,
requestId: entity.id,
mediaId: entity.media.id,
const requestRepository = getRepository(MediaRequest);
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
});
throw new Error(e.message);
if (media) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
logger.warn(
'Failed to send movie request to Radarr due to connection or configuration error, marking status as FAILED',
{
label: 'Media Request',
requestId: entity.id,
mediaId: entity.media.id,
errorMessage: e.message,
}
);
MediaRequest.sendNotification(
entity,
media,
Notification.MEDIA_FAILED
);
}
}
}
}
@@ -727,13 +746,32 @@ export class MediaRequestSubscriber
mediaId: entity.media.id,
});
} catch (e) {
logger.error('Something went wrong sending request to Sonarr', {
label: 'Media Request',
errorMessage: e.message,
requestId: entity.id,
mediaId: entity.media.id,
const requestRepository = getRepository(MediaRequest);
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
});
throw new Error(e.message);
if (media) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
logger.warn(
'Failed to send series request to Sonarr due to connection or configuration error, marking status as FAILED',
{
label: 'Media Request',
requestId: entity.id,
mediaId: entity.media.id,
errorMessage: e.message,
}
);
MediaRequest.sendNotification(
entity,
media,
Notification.MEDIA_FAILED
);
}
}
}
}

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" aria-label={label}>
<label htmlFor="localLogin" className="block">
<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" aria-label={option.name}>
<label htmlFor={option.id} className="block">
<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" aria-label={option.name}>
<label htmlFor={option.id} className="block">
<div className="flex flex-col">
<span className="font-medium text-white">{option.name}</span>
<span className="font-normal text-gray-400">

View File

@@ -5,12 +5,7 @@ import SettingsBadge from '@app/components/Settings/SettingsBadge';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { isValidURL } from '@app/utils/urlValidationHelper';
import {
ArrowDownOnSquareIcon,
BeakerIcon,
PlusIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import {
ArrowPathIcon,
QuestionMarkCircleIcon,
@@ -85,16 +80,6 @@ const messages = defineMessages(
supportVariablesTip:
'Available variables are documented in the webhook template variables section',
authheader: 'Authorization Header',
customHeaders: 'Custom Headers',
customHeadersTip:
'Add custom HTTP headers to include with webhook requests',
customHeadersAdd: 'Add Header',
customHeadersRemove: 'Remove',
customHeadersKey: 'Header Name',
customHeadersValue: 'Header Value',
customHeadersIncomplete: 'All headers must have both name and value',
customHeadersAuthConflict:
'Cannot use both Authorization Header and custom Authorization header. Please remove one.',
validationJsonPayloadRequired: 'You must provide a valid JSON payload',
webhooksettingssaved: 'Webhook notification settings saved successfully!',
webhooksettingsfailed: 'Webhook notification settings failed to save.',
@@ -140,43 +125,6 @@ const NotificationsWebhook = () => {
supportVariables: Yup.boolean(),
customHeaders: Yup.array()
.of(
Yup.object().shape({
key: Yup.string(),
value: Yup.string(),
})
)
.test(
'complete-headers',
intl.formatMessage(messages.customHeadersIncomplete),
function (headers) {
if (!headers || headers.length === 0) return true;
return headers.every(
(header) =>
(!header.key || !header.key.trim()) ===
(!header.value || !header.value.trim())
);
}
)
.test(
'auth-conflict',
intl.formatMessage(messages.customHeadersAuthConflict),
function (headers) {
const { authHeader } = this.parent;
if (!authHeader || !headers || headers.length === 0) return true;
const hasCustomAuthHeader = headers.some(
(header) =>
header.key &&
header.value &&
header.key.toLowerCase() === 'authorization'
);
return !hasCustomAuthHeader;
}
),
jsonPayload: Yup.string()
.when('enabled', {
is: true,
@@ -211,7 +159,6 @@ const NotificationsWebhook = () => {
webhookUrl: data.options.webhookUrl,
jsonPayload: data.options.jsonPayload,
authHeader: data.options.authHeader,
customHeaders: data.options.customHeaders ?? [],
supportVariables: data.options.supportVariables ?? false,
}}
validationSchema={NotificationsWebhookSchema}
@@ -224,9 +171,6 @@ const NotificationsWebhook = () => {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
customHeaders: values.customHeaders.filter(
(h: { key: string; value: string }) => h.key && h.value
),
supportVariables: values.supportVariables,
},
});
@@ -285,9 +229,6 @@ const NotificationsWebhook = () => {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
customHeaders: values.customHeaders.filter(
(h: { key: string; value: string }) => h.key && h.value
),
supportVariables: values.supportVariables ?? false,
},
});
@@ -403,86 +344,6 @@ const NotificationsWebhook = () => {
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="customHeaders" className="text-label">
{intl.formatMessage(messages.customHeaders)}
<span className="label-tip">
{intl.formatMessage(messages.customHeadersTip)}
</span>
</label>
<div className="form-input-area">
<div className="space-y-2">
{values.customHeaders.map(
(header: { key: string; value: string }, index: number) => (
<div key={index} className="flex gap-2">
<div className="flex-1">
<div className="form-input-field">
<Field
name={`customHeaders.${index}.key`}
type="text"
placeholder={intl.formatMessage(
messages.customHeadersKey
)}
/>
</div>
</div>
<div className="flex-1">
<div className="form-input-field">
<Field
name={`customHeaders.${index}.value`}
type="text"
placeholder={intl.formatMessage(
messages.customHeadersValue
)}
/>
</div>
</div>
<div className="flex items-center">
<Button
buttonType="danger"
buttonSize="sm"
onClick={(e) => {
e.preventDefault();
const newHeaders = values.customHeaders.filter(
(
_: { key: string; value: string },
i: number
) => i !== index
);
setFieldValue('customHeaders', newHeaders);
}}
title={intl.formatMessage(
messages.customHeadersRemove
)}
>
<TrashIcon />
</Button>
</div>
</div>
)
)}
<Button
buttonType="default"
buttonSize="sm"
onClick={(e) => {
e.preventDefault();
setFieldValue('customHeaders', [
...values.customHeaders,
{ key: '', value: '' },
]);
}}
>
<PlusIcon />
<span>{intl.formatMessage(messages.customHeadersAdd)}</span>
</Button>
</div>
{errors.customHeaders &&
touched.customHeaders &&
typeof errors.customHeaders === 'string' && (
<div className="error">{errors.customHeaders}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="webhook-json-payload" className="text-label">
{intl.formatMessage(messages.customJson)}

View File

@@ -2,7 +2,6 @@ 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';
@@ -57,21 +56,13 @@ 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: !isAuthPage ? 30000 : 0,
revalidateOnFocus: !isAuthPage,
revalidateOnMount: !isAuthPage,
revalidateOnReconnect: !isAuthPage,
refreshInterval: 30000,
errorRetryInterval: 30000,
shouldRetryOnError: false,
});

View File

@@ -681,14 +681,6 @@
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsWebhook.authheader": "Authorization Header",
"components.Settings.Notifications.NotificationsWebhook.customHeaders": "Custom Headers",
"components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "Add Header",
"components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "Cannot use both Authorization Header and custom Authorization header. Please remove one.",
"components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "All headers must have both name and value",
"components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Header Name",
"components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "Remove",
"components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "Add custom HTTP headers to include with webhook requests",
"components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Header Value",
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default",
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!",

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() || 'Unknown',
'X-Plex-Platform-Version': browser.getBrowserVersion(),
'X-Plex-Device': browser.getOSName(),
'X-Plex-Device-Name': `${browser.getBrowserName()} (Seerr)`,
'X-Plex-Device-Screen-Resolution':