Compare commits

..

34 Commits

Author SHA1 Message Date
gauthier-th
a45a465bea docs: add docs for dns caching 2025-08-19 13:57:00 +02:00
gauthier-th
aeb8023027 fix: add force min/max TTL in network settings 2025-08-19 13:56:59 +02:00
gauthier-th
f958b11d51 fix: update dns-caching package 2025-08-19 13:56:58 +02:00
gauthier-th
e31281cac0 fix: add env variable for min/max ttl & update dns-caching 2025-08-19 13:56:57 +02:00
gauthier-th
4ab79ffb76 fix: update dns-caching to v0.2.0 2025-08-19 13:56:57 +02:00
gauthier-th
c17384a339 fix: remove useless lru-cache dependency 2025-08-19 13:56:56 +02:00
Gauthier
eae78ec728 fix: correct dns-caching module configuration 2025-08-19 13:56:56 +02:00
Gauthier
ec4ed21a49 fix: correct dns-caching module configuration 2025-08-19 13:56:55 +02:00
Gauthier
1eec1823d5 refactor: use our own dns-caching package instead 2025-08-19 13:56:55 +02:00
Gauthier
e45a6c530b fix: remove old ipv4first setting 2025-08-19 13:56:54 +02:00
Gauthier
03d905ae98 fix: remove FetchAPI-related code 2025-08-19 13:56:53 +02:00
fallenbagel
7b2b45b066 refactor: removed useless condition when its always truthy 2025-08-19 13:56:53 +02:00
fallenbagel
5c6d7a6f19 refactor: remove console logs 2025-08-19 13:56:52 +02:00
fallenbagel
ab2fd0a324 refactor: remove cypress testing options in dnsCacheManager 2025-08-19 13:56:52 +02:00
fallenbagel
ede79a8ad4 refactor: use date-fns for formatting age and remove useless code 2025-08-19 13:56:51 +02:00
fallenbagel
54c4e9a6bd chore(i18n): extract translation keys 2025-08-19 13:56:51 +02:00
fallenbagel
2266fd43d8 feat(dnscache): global stats 2025-08-19 13:56:50 +02:00
fallenbagel
753ea43922 fix(dnscache): fix miss counter 2025-08-19 13:56:50 +02:00
fallenbagel
7fc8d3d4e4 refactor: clean up console logs 2025-08-19 13:56:44 +02:00
fallenbagel
00728dafdf fix(dnscache): use entry specific hits and misses not global 2025-08-19 13:56:43 +02:00
fallenbagel
7ddca119a8 chore: ignore cypress/config/settings.json 2025-08-19 13:56:42 +02:00
fallenbagel
ef30ea523f chore(cypresssettings): git ignore cypress json settings 2025-08-19 13:56:42 +02:00
fallenbagel
bb60926bf7 style(cypress): run prettier 2025-08-19 13:56:41 +02:00
fallenbagel
bb47dc6c02 feat(dnscache): dns cache entries are now flushable 2025-08-19 13:56:40 +02:00
fallenbagel
18e935d0bb test(cypress): fix cypress testing 2025-08-19 13:56:39 +02:00
fallenbagel
145dfe0e14 chore(i18n): extract translation keys 2025-08-19 13:56:39 +02:00
fallenbagel
81f4c24b7b feat: make dnsCache optional and enable-able through network settings 2025-08-19 13:56:38 +02:00
fallenbagel
73fd763890 feat(networksettings): cache dns off by default 2025-08-19 13:56:37 +02:00
fallenbagel
2a12cb84c6 feat: dns cache stats in jobs & cache page (and cleanup) 2025-08-19 13:56:37 +02:00
fallenbagel
73feb07007 fix: typos 2025-08-19 13:56:36 +02:00
fallenbagel
c856a9be0e feat(dns): improve DNS cache with multi-strategy fallback system
- multiple DNS resolution strategie
- graceful fallbacks between IPv6 and IPv4 addresses
- network error reporting in fetch fix
- compatibility with cypress testing (I HOPE)
2025-08-19 13:56:36 +02:00
fallenbagel
6828924493 feat: dynamic ttl which is revalidated while using stale dns cache
This is done as tmdb ttl is very less like 40 seconds so to make sure
any issues wont be caused due to cached dns (previously we were caching
for 5 minutes no matter what ttl)
2025-08-19 13:56:35 +02:00
fallenbagel
2f80a536c3 feat: simple implementation of dnscaching 2025-08-19 13:56:35 +02:00
fallenbagel
965df89614 feat(dns): implement dns caching 2025-08-19 13:56:34 +02:00
22 changed files with 66 additions and 408 deletions

View File

@@ -1,21 +0,0 @@
---
title: Gotify
description: Configure Gotify notifications.
sidebar_position: 5
---
# Gotify
## Configuration
### Server URL
Set this to the URL of your Gotify server.
### Application Token
Add an application to your Gotify server, and set this field to the generated application token.
:::info
Please refer to the [Gotify API documentation](https://gotify.net/docs) for more details on configuring these notifications.
:::

View File

@@ -1,29 +0,0 @@
---
title: ntfy.sh
description: Configure ntfy.sh notifications.
sidebar_position: 6
---
# ntfy.sh
## Configuration
### Server Root URL
Set this to the URL of your ntfy.sh server.
### Topic
Set this to the topic you want to send notifications to.
### Username + Password authentication (optional)
Set this to the username and password for your ntfy.sh server.
### Token authentication (optional)
Set this to the token for your ntfy.sh server.
:::info
Please refer to the [ntfy.sh API documentation](https://docs.ntfy.sh/) for more details on configuring these notifications.
:::

View File

@@ -1,23 +0,0 @@
---
title: Pushbullet
description: Configure Pushbullet notifications.
sidebar_position: 7
---
# Pushbullet
:::info
Users can optionally configure personal notifications in their user settings.
User notifications are separate from system notifications, and the available notification types are dependent on user permissions.
:::
## Configuration
### Access Token
[Create an access token](https://www.pushbullet.com/#settings) and set it here to grant Jellyseerr access to the Pushbullet API.
### Channel Tag (optional)
Optionally, [create a channel](https://www.pushbullet.com/my-channel) to allow other users to follow the notification feed using the specified channel tag.

View File

@@ -1,27 +0,0 @@
---
title: Pushover
description: Configure Pushover notifications.
sidebar_position: 8
---
# Pushover
:::info
Users can optionally configure personal notifications in their user settings.
User notifications are separate from system notifications, and the available notification types are dependent on user permissions.
:::
## Configuration
### Application/API Token
[Register an application](https://pushover.net/apps/build) and enter the API token in this field. (You can use one of the [official icons in our GitHub repository](https://github.com/fallenbagel/jellyseerr/tree/develop/public) when configuring the application.)
For more details on registering applications or the API token, please see the [Pushover API documentation](https://pushover.net/api#registration).
### User Key
Set this to the user key for your Pushover account. Alternatively, you can set this to a group key to deliver notifications to multiple users.
For more details, please see the [Pushover API documentation](https://pushover.net/api#identifiers).

View File

@@ -1,17 +0,0 @@
---
title: Slack
description: Configure Slack notifications.
sidebar_position: 9
---
# Slack
## Configuration
### Webhook URL
Simply [create a webhook](https://my.slack.com/services/new/incoming-webhook/) and enter the URL in this field.
:::info
Please refer to the [Slack API documentation](https://api.slack.com/messaging/webhooks) for more details on configuring these notifications.
:::

View File

@@ -1,39 +0,0 @@
---
title: Telegram
description: Configure Telegram notifications.
sidebar_position: 10
---
# Telegram
:::info
Users can optionally configure personal notifications in their user settings.
User notifications are separate from system notifications, and the available notification types are dependent on user permissions.
:::
## Configuration
:::info
In order to configure Telegram notifications, you first need to [create a bot](https://telegram.me/BotFather).
Bots **cannot** initiate conversations with users, so users must have your bot added to a conversation in order to receive notifications.
:::
### Bot Username (optional)
If this value is configured, users will be able to click a link to start a chat with your bot and configure their own personal notifications.
The bot username should end with `_bot`, and the `@` prefix should be omitted.
### Bot Authentication Token
At the end of the bot creation process, [@BotFather](https://telegram.me/botfather) will provide an authentication token.
### Chat ID
To obtain your chat ID, simply create a new group chat, add [@get_id_bot](https://telegram.me/get_id_bot), and issue the `/my_id` command.
### Send Silently (optional)
Optionally, notifications can be sent silently. Silent notifications send messages without notification sounds.

View File

@@ -1,138 +0,0 @@
---
title: Webhook
description: Configure webhook notifications.
sidebar_position: 4
---
# Webhook
The webhook notification agent enables you to send a custom JSON payload to any endpoint for specific notification events.
## Configuration
### Webhook URL
The URL you would like to post notifications to. Your JSON will be sent as the body of the request.
### Authorization Header (optional)
:::info
This is typically not needed. Please refer to your webhook provider's documentation for details.
:::
This value will be sent as an `Authorization` HTTP header.
### JSON Payload
Customize the JSON payload to suit your needs. Jellyseerr provides several [template variables](#template-variables) for use in the payload, which will be replaced with the relevant data when the notifications are triggered.
## Template Variables
### General
| Variable | Value |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `{{notification_type}}` | The type of notification (e.g. `MEDIA_PENDING` or `ISSUE_COMMENT`) |
| `{{event}}` | A friendly description of the notification event |
| `{{subject}}` | The notification subject (typically the media title) |
| `{{message}}` | The notification message body (the media overview/synopsis for request notifications; the issue description for issue notificatons) |
| `{{image}}` | The notification image (typically the media poster) |
### Notify User
These variables are for the target recipient of the notification.
| Variable | Value |
| ---------------------------------------- | ------------------------------------------------------------- |
| `{{notifyuser_username}}` | The target notification recipient's username |
| `{{notifyuser_email}}` | The target notification recipient's email address |
| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL |
| `{{notifyuser_settings_discordId}}` | The target notification recipient's Discord ID (if set) |
| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) |
:::info
The `notifyuser` variables are not defined for the following request notification types, as they are intended for application administrators rather than end users:
- Request Pending Approval
- Request Automatically Approved
- Request Processing Failed
On the other hand, the `notifyuser` variables _will_ be replaced with the requesting user's information for the below notification types:
- Request Approved
- Request Declined
- Request Available
If you would like to use the requesting user's information in your webhook, please instead include the relevant variables from the [Request](#request) section below.
:::
### Special
The following variables must be used as a key in the JSON payload (e.g., `"{{extra}}": []`).
| Variable | Value |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `{{media}}` | The relevant media object |
| `{{request}}` | The relevant request object |
| `{{issue}}` | The relevant issue object |
| `{{comment}}` | The relevant issue comment object |
| `{{extra}}` | The "extra" array of additional data for certain notifications (e.g., season/episode numbers for series-related notifications) |
#### Media
The `{{media}}` will be `null` if there is no relevant media object for the notification.
These following special variables are only included in media-related notifications, such as requests.
| Variable | Value |
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
| `{{media_type}}` | The media type (`movie` or `tv`) |
| `{{media_tmdbid}}` | The media's TMDB ID |
| `{{media_tvdbid}}` | The media's TheTVDB ID |
| `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
| `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
#### Request
The `{{request}}` will be `null` if there is no relevant media object for the notification.
The following special variables are only included in request-related notifications.
| Variable | Value |
| ----------------------------------------- | ----------------------------------------------- |
| `{{request_id}}` | The request ID |
| `{{requestedBy_username}}` | The requesting user's username |
| `{{requestedBy_email}}` | The requesting user's email address |
| `{{requestedBy_avatar}}` | The requesting user's avatar URL |
| `{{requestedBy_settings_discordId}}` | The requesting user's Discord ID (if set) |
| `{{requestedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) |
#### Issue
The `{{issue}}` will be `null` if there is no relevant media object for the notification.
The following special variables are only included in issue-related notifications.
| Variable | Value |
| ---------------------------------------- | ----------------------------------------------- |
| `{{issue_id}}` | The issue ID |
| `{{reportedBy_username}}` | The requesting user's username |
| `{{reportedBy_email}}` | The requesting user's email address |
| `{{reportedBy_avatar}}` | The requesting user's avatar URL |
| `{{reportedBy_settings_discordId}}` | The requesting user's Discord ID (if set) |
| `{{reportedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) |
#### Comment
The `{{comment}}` will be `null` if there is no relevant media object for the notification.
The following special variables are only included in issue comment-related notifications.
| Variable | Value |
| ----------------------------------------- | ----------------------------------------------- |
| `{{comment_message}}` | The comment message |
| `{{commentedBy_username}}` | The commenting user's username |
| `{{commentedBy_email}}` | The commenting user's email address |
| `{{commentedBy_avatar}}` | The commenting user's avatar URL |
| `{{commentedBy_settings_discordId}}` | The commenting user's Discord ID (if set) |
| `{{commentedBy_settings_telegramChatId}}` | The commenting user's Telegram Chat ID (if set) |

View File

@@ -57,7 +57,7 @@
"cronstrue": "2.23.0",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"dns-caching": "^0.2.5",
"dns-caching": "^0.2.4",
"email-templates": "12.0.1",
"email-validator": "2.0.4",
"express": "4.21.2",

14
pnpm-lock.yaml generated
View File

@@ -84,8 +84,8 @@ importers:
specifier: 1.11.7
version: 1.11.7
dns-caching:
specifier: ^0.2.5
version: 0.2.5
specifier: ^0.2.4
version: 0.2.4
email-templates:
specifier: 12.0.1
version: 12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7)
@@ -4864,8 +4864,8 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dns-caching@0.2.5:
resolution: {integrity: sha512-1cnB6i/OG4zfYbRnDjWZDT+FGLvOBuJlFIxVZvHBiaa62SCfaGoP4Si50O14HyoHmx1gadeGWigYXdX5LQAFvg==}
dns-caching@0.2.4:
resolution: {integrity: sha512-J48CLnMScOAtWIdExkz+522A0nPUwG5o+w7vVsXBJDipVLugCnps5AVJMn9bOkqQm4GarHtutMHYJEryCTeMjA==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
@@ -15700,7 +15700,7 @@ snapshots:
dlv@1.1.3: {}
dns-caching@0.2.5:
dns-caching@0.2.4:
dependencies:
lru-cache: 11.1.0
@@ -16093,7 +16093,7 @@ snapshots:
debug: 4.4.0(supports-color@5.5.0)
enhanced-resolve: 5.17.0
eslint: 8.35.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@@ -16115,7 +16115,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:

View File

@@ -145,7 +145,6 @@ export interface IMDBRating {
title: string;
url: string;
criticsScore: number;
criticsScoreCount: number;
}
/**
@@ -188,7 +187,6 @@ class IMDBRadarrProxy extends ExternalAPI {
title: data[0].Title,
url: `https://www.imdb.com/title/${data[0].ImdbId}`,
criticsScore: data[0].MovieRatings.Imdb.Value,
criticsScoreCount: data[0].MovieRatings.Imdb.Count,
};
} catch (e) {
throw new Error(

View File

@@ -1,4 +1,3 @@
import type { DnsEntries, DnsStats } from 'dns-caching';
import type { PaginatedResponse } from './common';
export type LogMessage = {
@@ -62,12 +61,38 @@ export interface CacheItem {
};
}
export interface DNSAddresses {
ipv4: number;
ipv6: number;
}
export interface DNSRecord {
addresses: DNSAddresses;
activeAddress: string;
family: number;
age: number;
ttl: number;
networkErrors: number;
hits: number;
misses: number;
}
export interface DNSStats {
size: number;
maxSize: number;
hits: number;
misses: number;
failures: number;
ipv4Fallbacks: number;
hitRate: number;
}
export interface CacheResponse {
apiCaches: CacheItem[];
imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>;
dnsCache: {
stats: DnsStats | undefined;
entries: DnsEntries | undefined;
entries: Record<string, DNSRecord>;
stats: DNSStats;
};
}

View File

@@ -54,8 +54,6 @@ issueRoutes.get<Record<string, string>, IssueResultsResponse>(
.leftJoinAndSelect('issue.createdBy', 'createdBy')
.leftJoinAndSelect('issue.media', 'media')
.leftJoinAndSelect('issue.modifiedBy', 'modifiedBy')
.leftJoinAndSelect('issue.comments', 'comments')
.leftJoinAndSelect('comments.user', 'user')
.where('issue.status IN (:...issueStatus)', {
issueStatus: statusFilter,
});

View File

@@ -28,9 +28,8 @@ import discoverSettingRoutes from '@server/routes/settings/discover';
import { ApiError } from '@server/types/error';
import { appDataPath } from '@server/utils/appDataVolume';
import { getAppVersion } from '@server/utils/appVersion';
import { dnsCache } from '@server/utils/dnsCache';
import dnsCache from '@server/utils/dnsCache';
import { getHostname } from '@server/utils/getHostname';
import type { DnsEntries, DnsStats } from 'dns-caching';
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import fs from 'fs';
@@ -757,8 +756,8 @@ settingsRoutes.get('/cache', async (_req, res) => {
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
const avatarImageCache = await ImageProxy.getImageStats('avatar');
const stats: DnsStats | undefined = dnsCache?.getStats();
const entries: DnsEntries | undefined = dnsCache?.getCacheEntries();
const stats = dnsCache?.getStats();
const entries = dnsCache?.getCacheEntries();
return res.status(200).json({
apiCaches,

View File

@@ -1,7 +1,7 @@
import logger from '@server/logger';
import { DnsCacheManager } from 'dns-caching';
export let dnsCache: DnsCacheManager | undefined;
let dnsCache: DnsCacheManager | undefined;
export function initializeDnsCache({
forceMinTtl,
@@ -24,3 +24,5 @@ export function initializeDnsCache({
});
dnsCache.initialize();
}
export default dnsCache;

View File

@@ -84,7 +84,6 @@ const SettingsTabs = ({
Select a Tab
</label>
<select
id="tabs"
onChange={(e) => {
router.push(e.target.value);
}}

View File

@@ -1,7 +1,6 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip';
import { issueOptions } from '@app/components/IssueModal/constants';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
@@ -27,7 +26,6 @@ const messages = defineMessages('components.IssueList.IssueItem', {
opened: 'Opened',
viewissue: 'View Issue',
unknownissuetype: 'Unknown',
descriptionpreview: 'Issue Description',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -109,15 +107,8 @@ const IssueItem = ({ issue }: IssueItemProps) => {
}
}
const description = issue.comments?.[0]?.message || '';
const maxDescriptionLength = 120;
const shouldTruncate = description.length > maxDescriptionLength;
const truncatedDescription = shouldTruncate
? description.substring(0, maxDescriptionLength) + '...'
: description;
return (
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:flex-row">
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
@@ -177,38 +168,8 @@ const IssueItem = ({ issue }: IssueItemProps) => {
>
{isMovie(title) ? title.title : title.name}
</Link>
{description && (
<div className="mt-1 max-w-full">
<div className="overflow-hidden text-sm text-gray-300">
{shouldTruncate ? (
<Tooltip
content={
<div className="max-w-sm p-3">
<div className="mb-1 text-sm font-medium text-gray-200">
Issue Description
</div>
<div className="whitespace-pre-wrap text-sm leading-relaxed text-gray-300">
{description}
</div>
</div>
}
tooltipConfig={{
placement: 'top',
offset: [0, 8],
}}
>
<span className="block cursor-help truncate transition-colors hover:text-gray-200">
{truncatedDescription}
</span>
</Tooltip>
) : (
<span className="block break-words">{description}</span>
)}
</div>
</div>
)}
{problemSeasonEpisodeLine.length > 0 && (
<div className="card-field mt-1">
<div className="card-field">
{problemSeasonEpisodeLine.map((t, k) => (
<span key={k}>{t}</span>
))}

View File

@@ -99,7 +99,7 @@ const messages = defineMessages('components.MovieDetails', {
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
rtaudiencescore: 'Rotten Tomatoes Audience Score',
tmdbuserscore: 'TMDB User Score',
imdbuserscore: 'IMDB User Score votes: {formattedCount}',
imdbuserscore: 'IMDB User Score',
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
watchlistDeleted:
'<strong>{title}</strong> Removed from watchlist successfully!',
@@ -812,18 +812,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
</Tooltip>
)}
{ratingData?.imdb?.criticsScore && (
<Tooltip
content={intl.formatMessage(messages.imdbuserscore, {
formattedCount: intl.formatNumber(
ratingData.imdb.criticsScoreCount,
{
notation: 'compact',
compactDisplay: 'short',
maximumFractionDigits: 1,
}
),
})}
>
<Tooltip content={intl.formatMessage(messages.imdbuserscore)}>
<a
href={ratingData.imdb.url}
className="media-rating"

View File

@@ -152,6 +152,7 @@ const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => {
href="/apple-splash-1136-640.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"

View File

@@ -1,6 +1,5 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal';
import useRequestOverride from '@app/hooks/useRequestOverride';
@@ -96,58 +95,36 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
<div className="flex items-center justify-between">
<div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
<div className="white mb-1 flex flex-nowrap">
<span className="flex w-40 items-center truncate md:w-auto">
<Tooltip content={intl.formatMessage(messages.requestedby)}>
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
</Tooltip>
<Tooltip content={intl.formatMessage(messages.requestedby)}>
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
</Tooltip>
<span className="w-40 truncate md:w-auto">
<Link
href={
request.requestedBy.id === user?.id
? '/profile'
: `/users/${request.requestedBy.id}`
}
className="flex items-center font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
>
<span className="avatar-sm">
<CachedImage
type="avatar"
src={request.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
height={20}
/>
</span>
{request.requestedBy.displayName}
</Link>
</span>
</div>
{request.modifiedBy && (
<div className="flex flex-nowrap">
<span className="flex w-40 items-center truncate md:w-auto">
<Tooltip
content={intl.formatMessage(messages.lastmodifiedby)}
>
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<Tooltip content={intl.formatMessage(messages.lastmodifiedby)}>
<EyeIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<span className="w-40 truncate md:w-auto">
<Link
href={
request.modifiedBy.id === user?.id
? '/profile'
: `/users/${request.modifiedBy.id}`
}
className="flex items-center font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
>
<span className="avatar-sm">
<CachedImage
type="avatar"
src={request.modifiedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
height={20}
/>
</span>
{request.modifiedBy.displayName}
</Link>
</span>

View File

@@ -65,6 +65,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
dnscachehits: 'Hits',
dnscachemisses: 'Misses',
dnscacheage: 'Age',
dnscachenetworkerrors: 'Network Errors',
flushdnscache: 'Flush DNS Cache',
dnsCacheGlobalStats: 'Global DNS Cache Stats',
dnsCacheGlobalStatsDescription:
@@ -628,6 +629,9 @@ const SettingsJobs = () => {
<Table.TH>{intl.formatMessage(messages.dnscachehits)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachemisses)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscacheage)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscachenetworkerrors)}
</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
@@ -640,6 +644,7 @@ const SettingsJobs = () => {
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD>{intl.formatNumber(data.networkErrors)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"

View File

@@ -378,7 +378,7 @@ const UserProfile = () => {
{user.userType === UserType.PLEX &&
(user.id === currentUser?.id ||
currentHasPermission(Permission.ADMIN)) &&
(!watchData || !!watchData.recentlyWatched?.length) &&
(!watchData || !!watchData.recentlyWatched.length) &&
!watchDataError && (
<>
<div className="slider-header">
@@ -389,7 +389,7 @@ const UserProfile = () => {
<Slider
sliderKey="media"
isLoading={!watchData}
items={watchData?.recentlyWatched?.map((item) => (
items={watchData?.recentlyWatched.map((item) => (
<TmdbTitleCard
key={`media-slider-item-${item.id}`}
id={item.id}

View File

@@ -180,7 +180,6 @@
"components.IssueDetails.toaststatusupdated": "Issue status updated successfully!",
"components.IssueDetails.toaststatusupdatefailed": "Something went wrong while updating the issue status.",
"components.IssueDetails.unknownissuetype": "Unknown",
"components.IssueList.IssueItem.descriptionpreview": "Issue Description",
"components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {Episode} other {Episodes}}",
"components.IssueList.IssueItem.issuestatus": "Status",
"components.IssueList.IssueItem.issuetype": "Type",
@@ -315,7 +314,7 @@
"components.MovieDetails.cast": "Cast",
"components.MovieDetails.digitalrelease": "Digital Release",
"components.MovieDetails.downloadstatus": "Download Status",
"components.MovieDetails.imdbuserscore": "IMDB User Score votes: {formattedCount}",
"components.MovieDetails.imdbuserscore": "IMDB User Score",
"components.MovieDetails.managemovie": "Manage Movie",
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
"components.MovieDetails.markavailable": "Mark as Available",
@@ -884,6 +883,7 @@
"components.Settings.SettingsJobsCache.dnscachehits": "Hits",
"components.Settings.SettingsJobsCache.dnscachemisses": "Misses",
"components.Settings.SettingsJobsCache.dnscachename": "Hostname",
"components.Settings.SettingsJobsCache.dnscachenetworkerrors": "Network Errors",
"components.Settings.SettingsJobsCache.download-sync": "Download Sync",
"components.Settings.SettingsJobsCache.download-sync-reset": "Download Sync Reset",
"components.Settings.SettingsJobsCache.editJobSchedule": "Modify Job",
@@ -988,8 +988,6 @@
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.SettingsNetwork.dnsCache": "DNS Cache",
"components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "DNS Cache Maximum TTL",
"components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "DNS Cache Minimum TTL",
"components.Settings.SettingsNetwork.dnsCacheHoverTip": "Do NOT enable this if you are experiencing issues with DNS lookups",
"components.Settings.SettingsNetwork.dnsCacheTip": "Enable caching of DNS lookups to optimize performance and avoid making unnecessary API calls",
"components.Settings.SettingsNetwork.docs": "documentation",