Merge branch 'develop' of github.com:fallenbagel/jellyseerr into feature-emby-connect-login

This commit is contained in:
Hermanus Engelbrecht
2025-03-01 11:25:41 +01:00
61 changed files with 754 additions and 335 deletions

View File

@@ -94,7 +94,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/345752?v=4",
"profile": "https://github.com/jab416171",
"contributions": [
"doc"
"doc",
"code"
]
},
{
@@ -338,7 +339,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
"profile": "https://gauthierth.fr/",
"contributions": [
"code"
"code",
"maintenance"
]
},
{
@@ -619,6 +621,87 @@
"contributions": [
"code"
]
},
{
"login": "j0srisk",
"name": "Joseph Risk",
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
"profile": "http://josephrisk.com",
"contributions": [
"code"
]
},
{
"login": "Loetwiek",
"name": "Loetwiek",
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
"profile": "https://github.com/Loetwiek",
"contributions": [
"code"
]
},
{
"login": "Fuochi",
"name": "Fuochi",
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
"profile": "https://github.com/Fuochi",
"contributions": [
"doc"
]
},
{
"login": "demrich",
"name": "David Emrich",
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
"profile": "https://github.com/demrich",
"contributions": [
"code"
]
},
{
"login": "maxnatamo",
"name": "Max T. Kristiansen",
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
"profile": "https://maxtrier.dk",
"contributions": [
"code"
]
},
{
"login": "DamsDev1",
"name": "Damien Fajole",
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
"profile": "https://damsdev.me",
"contributions": [
"code"
]
},
{
"login": "AhmedNSidd",
"name": "Ahmed Siddiqui",
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
"profile": "https://github.com/AhmedNSidd",
"contributions": [
"code"
]
},
{
"login": "JackW6809",
"name": "JackOXI",
"avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
"profile": "https://github.com/JackW6809",
"contributions": [
"code"
]
},
{
"login": "StancuFlorin",
"name": "Stancu Florin",
"avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
"profile": "http://indicus.ro",
"contributions": [
"code"
]
}
]
}

View File

@@ -97,7 +97,7 @@ 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 Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Jellyseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/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>

View File

@@ -131,7 +131,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a> <a href="#maintenance-gauthier-th" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
@@ -288,7 +288,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<td align="center" valign="top" width="14.28%"><a href="https://github.com/byakurau"><img src="https://avatars.githubusercontent.com/u/1811683?v=4?s=100" width="100px;" alt="byakurau"/><br /><sub><b>byakurau</b></sub></a><br /><a href="#translation-byakurau" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miknii"><img src="https://avatars.githubusercontent.com/u/109232569?v=4?s=100" width="100px;" alt="miknii"/><br /><sub><b>miknii</b></sub></a><br /><a href="#translation-miknii" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eclipseop"><img src="https://avatars.githubusercontent.com/u/5846213?v=4?s=100" width="100px;" alt="Mackenzie"/><br /><sub><b>Mackenzie</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Eclipseop" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s0up4200"><img src="https://avatars.githubusercontent.com/u/18177310?v=4?s=100" width="100px;" alt="soup"/><br /><sub><b>soup</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s0up4200"><img src="https://avatars.githubusercontent.com/u/18177310?v=4?s=100" width="100px;" alt="soup"/><br /><sub><b>soup</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceptonit"><img src="https://avatars.githubusercontent.com/u/12678743?v=4?s=100" width="100px;" alt="ceptonit"/><br /><sub><b>ceptonit</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ceptonit" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aedelbro"><img src="https://avatars.githubusercontent.com/u/36162221?v=4?s=100" width="100px;" alt="aedelbro"/><br /><sub><b>aedelbro</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=aedelbro" title="Code">💻</a></td>
</tr>
@@ -321,6 +321,8 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JackW6809" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -4,7 +4,7 @@
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
"main": {
"apiKey": "testkey",
"applicationTitle": "Overseerr",
"applicationTitle": "Jellyseerr",
"applicationUrl": "",
"csrfProtection": false,
"cacheImages": false,
@@ -71,7 +71,7 @@
"ignoreTls": false,
"requireTls": false,
"allowSelfSigned": false,
"senderName": "Overseerr"
"senderName": "Jellyseerr"
}
},
"discord": {

View File

@@ -1,19 +1,19 @@
openapi: '3.0.2'
info:
title: 'Overseerr API'
title: 'Jellyseerr API'
version: '1.0.0'
description: |
This is the documentation for the Overseerr API backend.
This is the documentation for the Jellyseerr API backend.
Two primary authentication methods are supported:
- **Cookie Authentication**: A valid sign-in to the `/auth/plex` or `/auth/local` will generate a valid authentication cookie.
- **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Overseerr.
- **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Jellyseerr.
tags:
- name: public
description: Public API endpoints requiring no authentication.
- name: settings
description: Endpoints related to Overseerr's settings and configuration.
description: Endpoints related to Jellyseerr's settings and configuration.
- name: auth
description: Endpoints related to logging in or out, and the currently authenticated user.
- name: users
@@ -160,7 +160,7 @@ components:
example: en
applicationTitle:
type: string
example: Overseerr
example: Jellyseerr
applicationUrl:
type: string
example: https://os.example.com
@@ -1438,7 +1438,7 @@ components:
example: no-reply@example.com
senderName:
type: string
example: Overseerr
example: Jellyseerr
smtpHost:
type: string
example: 127.0.0.1
@@ -1969,8 +1969,8 @@ components:
paths:
/status:
get:
summary: Get Overseerr status
description: Returns the current Overseerr status in a JSON object.
summary: Get Jellyseerr status
description: Returns the current Jellyseerr status in a JSON object.
security: []
tags:
- public

View File

@@ -5,7 +5,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "node postinstall-win.js",
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
"dev": "nodemon -e ts --watch server --watch jellyseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json",
"build:next": "next build",
"build": "pnpm build:next && pnpm build:server",

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -3,7 +3,7 @@
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const OFFLINE_VERSION = 3;
const OFFLINE_VERSION = 4;
const CACHE_NAME = 'offline';
// Customize this with a different URL if needed.
const OFFLINE_URL = '/offline.html';
@@ -107,6 +107,25 @@ self.addEventListener('push', (event) => {
);
}
// Set the badge with the amount of pending requests
// Only update the badge if the payload confirms they are the admin
if (
(payload.notificationType === 'MEDIA_APPROVED' ||
payload.notificationType === 'MEDIA_DECLINED') &&
payload.isAdmin
) {
if ('setAppBadge' in navigator) {
navigator.setAppBadge(payload.pendingRequestsCount);
}
return;
}
if (payload.notificationType === 'MEDIA_PENDING') {
if ('setAppBadge' in navigator) {
navigator.setAppBadge(payload.pendingRequestsCount);
}
}
event.waitUntil(self.registration.showNotification(payload.subject, options));
});

View File

@@ -72,7 +72,7 @@ class GithubAPI extends ExternalAPI {
);
}
public async getOverseerrReleases({
public async getJellyseerrReleases({
take = 20,
}: {
take?: number;
@@ -88,14 +88,14 @@ class GithubAPI extends ExternalAPI {
return data;
} catch (e) {
logger.warn(
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
{ label: 'GitHub API', errorMessage: e.message }
);
return [];
}
}
public async getOverseerrCommits({
public async getJellyseerrCommits({
take = 20,
branch = 'develop',
}: {
@@ -114,7 +114,7 @@ class GithubAPI extends ExternalAPI {
return data;
} catch (e) {
logger.warn(
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
{ label: 'GitHub API', errorMessage: e.message }
);
return [];

View File

@@ -124,9 +124,9 @@ class PlexAPI {
// },
options: {
identifier: settings.clientId,
product: 'Overseerr',
deviceName: 'Overseerr',
platform: 'Overseerr',
product: 'Jellyseerr',
deviceName: 'Jellyseerr',
platform: 'Jellyseerr',
},
});
}

View File

@@ -256,6 +256,7 @@ class TheMovieDb extends ExternalAPI {
language,
append_to_response:
'credits,external_ids,videos,keywords,release_dates,watch/providers',
include_video_language: language + ', en',
},
43200
);
@@ -280,6 +281,7 @@ class TheMovieDb extends ExternalAPI {
language,
append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
include_video_language: language + ', en',
},
43200
);

View File

@@ -41,9 +41,9 @@ import path from 'path';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
const API_SPEC_PATH = path.join(__dirname, '../jellyseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`);
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

View File

@@ -19,6 +19,8 @@ export interface NotificationPayload {
request?: MediaRequest;
issue?: Issue;
comment?: IssueComment;
pendingRequestsCount?: number;
isAdmin?: boolean;
}
export abstract class BaseAgent<T extends NotificationAgentConfig> {

View File

@@ -188,7 +188,7 @@ class SlackAgent
type: 'actions',
elements: [
{
action_id: 'open-in-overseerr',
action_id: 'open-in-jellyseerr',
type: 'button',
url,
text: {

View File

@@ -1,6 +1,7 @@
import { IssueType, IssueTypeName } from '@server/constants/issue';
import { MediaType } from '@server/constants/media';
import { MediaRequestStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import MediaRequest from '@server/entity/MediaRequest';
import { User } from '@server/entity/User';
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
import type { NotificationAgentConfig } from '@server/lib/settings';
@@ -19,6 +20,8 @@ interface PushNotificationPayload {
actionUrl?: string;
actionUrlTitle?: string;
requestId?: number;
pendingRequestsCount?: number;
isAdmin?: boolean;
}
class WebPushAgent
@@ -129,6 +132,8 @@ class WebPushAgent
requestId: payload.request?.id,
actionUrl,
actionUrlTitle,
pendingRequestsCount: payload.pendingRequestsCount,
isAdmin: payload.isAdmin,
};
}
@@ -152,6 +157,51 @@ class WebPushAgent
const mainUser = await userRepository.findOne({ where: { id: 1 } });
const requestRepository = getRepository(MediaRequest);
const pendingRequests = await requestRepository.find({
where: { status: MediaRequestStatus.PENDING },
});
const webPushNotification = async (
pushSub: UserPushSubscription,
notificationPayload: Buffer
) => {
logger.debug('Sending web push notification', {
label: 'Notifications',
recipient: pushSub.user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await webpush.sendNotification(
{
endpoint: pushSub.endpoint,
keys: {
auth: pushSub.auth,
p256dh: pushSub.p256dh,
},
},
notificationPayload
);
} catch (e) {
logger.error(
'Error sending web push notification; removing subscription',
{
label: 'Notifications',
recipient: pushSub.user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
}
);
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(pushSub);
}
};
if (
payload.notifyUser &&
// Check if user has webpush notifications enabled and fallback to true if undefined
@@ -169,7 +219,11 @@ class WebPushAgent
pushSubs.push(...notifySubs);
}
if (payload.notifyAdmin) {
if (
payload.notifyAdmin ||
type === Notification.MEDIA_APPROVED ||
type === Notification.MEDIA_DECLINED
) {
const users = await userRepository.find();
const manageUsers = users.filter(
@@ -192,7 +246,42 @@ class WebPushAgent
})
.getMany();
pushSubs.push(...allSubs);
// We only want to send the custom notification when type is approved or declined
// Otherwise, default to the normal notification
if (
type === Notification.MEDIA_APPROVED ||
type === Notification.MEDIA_DECLINED
) {
if (mainUser && allSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
// Custom payload only for updating the app badge
const notificationBadgePayload = Buffer.from(
JSON.stringify(
this.getNotificationPayload(type, {
subject: payload.subject,
notifySystem: false,
notifyAdmin: true,
isAdmin: true,
pendingRequestsCount: pendingRequests.length,
})
),
'utf-8'
);
await Promise.all(
allSubs.map(async (sub) => {
webPushNotification(sub, notificationBadgePayload);
})
);
}
} else {
pushSubs.push(...allSubs);
}
}
if (mainUser && pushSubs.length > 0) {
@@ -202,6 +291,10 @@ class WebPushAgent
settings.vapidPrivate
);
if (type === Notification.MEDIA_PENDING) {
payload = { ...payload, pendingRequestsCount: pendingRequests.length };
}
const notificationPayload = Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
@@ -209,39 +302,7 @@ class WebPushAgent
await Promise.all(
pushSubs.map(async (sub) => {
logger.debug('Sending web push notification', {
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
notificationPayload
);
} catch (e) {
logger.error(
'Error sending web push notification; removing subscription',
{
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
}
);
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
webPushNotification(sub, notificationPayload);
})
);
}

View File

@@ -130,7 +130,7 @@ class WatchlistSync {
switch (e.constructor) {
// During watchlist sync, these errors aren't necessarily
// a problem with Overseerr. Since we are auto syncing these constantly, it's
// a problem with Jellyseerr. Since we are auto syncing these constantly, it's
// possible they are unexpectedly at their quota limit, for example. So we'll
// instead log these as debug messages.
case RequestPermissionError:

View File

@@ -43,14 +43,14 @@ const logger = winston.createLogger({
}),
new winston.transports.DailyRotateFile({
filename: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
? `${process.env.CONFIG_DIRECTORY}/logs/jellyseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/jellyseerr-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '7d',
createSymlink: true,
symlinkName: 'overseerr.log',
symlinkName: 'jellyseerr.log',
}),
new winston.transports.DailyRotateFile({
filename: process.env.CONFIG_DIRECTORY

View File

@@ -157,7 +157,7 @@ authRoutes.post('/plex', async (req, res, next) => {
});
} else {
logger.info(
'Sign-in attempt from Plex user with access to the media server; creating new Overseerr user',
'Sign-in attempt from Plex user with access to the media server; creating new Jellyseerr user',
{
label: 'API',
ip: req.ip,
@@ -611,7 +611,7 @@ authRoutes.post('/local', async (req, res, next) => {
.getOne();
if (!user || !(await user.passwordMatch(body.password))) {
logger.warn('Failed sign-in attempt using invalid Overseerr password', {
logger.warn('Failed sign-in attempt using invalid Jellyseerr password', {
label: 'API',
ip: req.ip,
email: body.email,
@@ -701,7 +701,7 @@ authRoutes.post('/local', async (req, res, next) => {
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
logger.error(
'Something went wrong authenticating with Overseerr password',
'Something went wrong authenticating with Jellyseerr password',
{
label: 'API',
errorMessage: e.message,

View File

@@ -837,7 +837,8 @@ discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
select: ['id', 'plexToken'],
});
if (activeUser) {
if (activeUser && !activeUser?.plexToken) {
// Non-Plex users can only see their own watchlist
const [result, total] = await getRepository(Watchlist).findAndCount({
where: { requestedBy: { id: activeUser?.id } },
relations: {
@@ -866,6 +867,7 @@ discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
});
}
// List watchlist from Plex
const plexTV = new PlexTvAPI(activeUser.plexToken);
const watchlist = await plexTV.getWatchlist({ offset });

View File

@@ -55,7 +55,7 @@ router.get<unknown, StatusResponse>('/status', async (req, res) => {
let commitsBehind = 0;
if (currentVersion.startsWith('develop-') && commitTag !== 'local') {
const commits = await githubApi.getOverseerrCommits();
const commits = await githubApi.getJellyseerrCommits();
if (commits.length) {
const filteredCommits = commits.filter(
@@ -74,7 +74,7 @@ router.get<unknown, StatusResponse>('/status', async (req, res) => {
}
}
} else if (commitTag !== 'local') {
const releases = await githubApi.getOverseerrReleases();
const releases = await githubApi.getJellyseerrReleases();
if (releases.length) {
const latestVersion = releases[0];
@@ -403,7 +403,7 @@ router.get('/watchproviders/tv', async (req, res, next) => {
router.get('/', (_req, res) => {
return res.status(200).json({
api: 'Overseerr API',
api: 'Jellyseerr API',
version: '1.0',
});
});

View File

@@ -119,28 +119,10 @@ userSettingsRoutes.post<
}
const oldEmail = user.email;
const oldUsername = user.username;
user.username = req.body.username;
if (user.jellyfinUsername) {
if (user.userType !== UserType.PLEX) {
user.email = req.body.email || user.jellyfinUsername || user.email;
}
// Edge case for local users, because they have no Jellyfin username to fall back on
// if the email is not provided
if (user.userType === UserType.LOCAL) {
if (req.body.email) {
user.email = req.body.email;
if (
!user.username &&
user.email !== oldEmail &&
!oldEmail.includes('@')
) {
user.username = oldEmail;
}
} else if (req.body.username) {
user.email = oldUsername || user.email;
user.username = req.body.username;
}
}
const existingUser = await userRepository.findOne({
where: { email: user.email },
@@ -437,7 +419,7 @@ userSettingsRoutes.post<{ username: string; password: string }>(
const hostname = getHostname();
const deviceId = Buffer.from(
`BOT_overseerr_${req.user.username ?? ''}`
`BOT_jellyseerr_${req.user.username ?? ''}`
).toString('base64');
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);

View File

@@ -1,85 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 361 157">
<defs>
<style>
.cls-1 {
fill: #fff;
}
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="plex-logo"
x="0px"
y="0px"
viewBox="0 0 1000 460.89727"
xml:space="preserve"
sodipodi:docname="plex-logo.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata25"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs23">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview21"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="0.27956081"
inkscape:cx="783.06912"
inkscape:cy="-132.85701"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="plex-logo" />
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
.st1{fill:#EBAF00;}
</style>
<path
class="st0"
d="m 164.18919,82.43243 c -39.86487,0 -65.540543,11.48648 -87.162163,38.51351 V 91.21621 H 0 v 366.21621 c 0,0 1.3513514,0.67567 5.4054053,1.35135 5.4054057,1.35135 33.7837827,7.43243 54.7297287,-10.13514 18.243244,-15.54054 22.297295,-33.78378 22.297295,-54.05405 v -52.7027 c 22.297301,23.64864 47.297301,33.78378 82.432431,33.78378 75.67567,0 133.78378,-61.48648 133.78378,-143.24323 0,-88.51352 -56.08108,-150 -134.45945,-150 z m -14.86487,223.64864 c -42.56756,0 -76.351351,-35.13513 -76.351351,-77.7027 0,-41.89189 39.864871,-75.67567 76.351351,-75.67567 43.24324,0 76.35135,33.1081 76.35135,76.35135 0,43.24324 -33.78378,77.02702 -76.35135,77.02702 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:6.75675678" /><path
class="st0"
d="m 408.1081,223.64864 c 0,31.75676 3.37838,70.27027 34.45946,112.16216 0.67567,0.67567 2.02702,2.7027 2.02702,2.7027 -12.83783,21.62162 -28.37837,36.48648 -49.32432,36.48648 -16.21622,0 -32.43243,-8.78378 -45.94595,-23.64864 -14.18918,-16.21622 -20.94594,-37.16216 -20.94594,-59.45946 V 0 h 79.05405 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:6.75675678" /><polygon
class="st1"
points="117.9,33.9 104.1,13.5 118.3,13.5 132,33.9 118.3,54.2 104.1,54.2 "
id="polygon8"
style="fill:#ebaf00"
transform="scale(6.7567568)" /><polygon
class="st0"
points="135.7,31.6 148,13.5 133.8,13.5 128.7,21 "
id="polygon10"
style="fill:#ffffff"
transform="scale(6.7567568)" /><path
class="st0"
d="m 869.59458,316.2162 c 0,0 16.2162,22.2973 16.2162,22.2973 15.54058,24.32432 35.8108,36.48648 59.45949,36.48648 25,-0.67567 42.56752,-22.29729 49.3243,-30.4054 0,0 -12.16218,-10.81081 -27.7027,-29.05405 -20.94598,-24.32432 -48.64868,-68.91892 -49.3243,-70.94594 z"
id="path12"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:6.75675678" /><path
style="fill:#ffffff;stroke-width:6.75675678"
inkscape:connector-curvature="0"
id="path16"
d="m 632.43242,287.16215 c -16.21622,14.86486 -27.02703,22.97297 -49.32432,22.97297 -39.86487,0 -62.83784,-28.37837 -66.21622,-59.45945 h 211.4865 c 1.35131,-4.05406 2.027,-9.45946 2.027,-18.24324 0,-85.81082 -62.83783,-150 -145.27026,-150 -78.37837,0 -142.56756,65.54054 -142.56756,147.29729 0,81.08108 64.18919,145.27026 144.59459,145.27026 56.08108,0 104.72973,-31.75675 131.08105,-87.83783 z M 585.8108,147.29729 c 35.13513,0 61.48648,22.97297 67.56756,53.37838 H 519.59458 c 6.75676,-31.75676 31.75676,-53.37838 66.21622,-53.37838 z"
class="st0" />
.cls-2 {
fill: #eaaf20;
}
</style>
</defs>
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
<g>
<g id="Layer_1">
<path id="path4" class="cls-1"
d="M60.6,28.8c-14.3,0-23.5,3.9-31.3,13v-10H1.6v123.7s.5.2,1.9.5c1.9.5,12.1,2.5,19.6-3.4,6.5-5.3,8-11.4,8-18.3v-17.8c8,8,17,11.4,29.6,11.4,27.2,0,48-20.8,48-48.4s-20.1-50.7-48.3-50.7h0ZM55.2,104.3c-15.3,0-27.4-11.9-27.4-26.3s14.3-25.6,27.4-25.6,27.4,11.2,27.4,25.8-12.1,26-27.4,26Z" />
<path id="path6" class="cls-1"
d="M148.1,76.5c0,10.7,1.2,23.7,12.4,37.9.2.2.7.9.7.9-4.6,7.3-10.2,12.3-17.7,12.3s-11.6-3-16.5-8c-5.1-5.5-7.5-12.6-7.5-20.1V.9h28.4l.2,75.6Z" />
<polygon id="polygon8" class="cls-2"
points="287.6 78.3 254.1 31.7 288.6 31.7 321.8 78.3 288.6 124.6 254.1 124.6 287.6 78.3" />
<polygon id="polygon10" class="cls-1"
points="330.8 73 360.6 31.7 326.2 31.7 313.8 48.9 330.8 73" />
<path id="path12" class="cls-1"
d="M313.8,107.7l5.8,7.5c5.6,8.2,12.9,12.3,21.3,12.3,9-.2,15.3-7.5,17.7-10.3,0,0-4.4-3.7-9.9-9.8-7.5-8.2-17.5-23.3-17.7-24l-17.2,24.2Z" />
<path id="path16" class="cls-1"
d="M228.7,97.9c-5.8,5-9.7,7.8-17.7,7.8-14.3,0-22.6-9.6-23.8-20.1h75.9c.5-1.4.7-3.2.7-6.2,0-29-22.6-50.7-52.2-50.7s-51.2,22.1-51.2,49.8,23,49.1,51.9,49.1,37.6-10.7,47.1-29.7h-30.8ZM211.9,50.7c12.6,0,22.1,7.8,24.3,18h-48c2.4-10.7,11.4-18,23.8-18h0Z" />
<path id="path4-2" data-name="path4" class="cls-1"
d="M59.3,28.2c-14.3,0-23.5,3.9-31.3,13v-10H.4v123.7s.5.2,1.9.5c1.9.5,12.1,2.5,19.6-3.4,6.5-5.3,8-11.4,8-18.3v-17.8c8,8,17,11.4,29.6,11.4,27.2,0,48-20.8,48-48.4s-20.1-50.7-48.3-50.7h0ZM54,103.8c-15.3,0-27.4-11.9-27.4-26.3s14.3-25.6,27.4-25.6,27.4,11.2,27.4,25.8-12.1,26-27.4,26Z" />
<path id="path6-2" data-name="path6" class="cls-1"
d="M146.9,75.9c0,10.7,1.2,23.7,12.4,37.9.2.2.7.9.7.9-4.6,7.3-10.2,12.3-17.7,12.3s-11.6-3-16.5-8c-5.1-5.5-7.5-12.6-7.5-20.1V.4h28.4l.2,75.6Z" />
<polygon id="polygon8-2" data-name="polygon8" class="cls-2"
points="286.4 77.8 252.9 31.2 287.3 31.2 320.6 77.8 287.3 124.1 252.9 124.1 286.4 77.8" />
<polygon id="polygon10-2" data-name="polygon10" class="cls-1"
points="329.5 72.5 359.4 31.2 324.9 31.2 312.6 48.3 329.5 72.5" />
<path id="path12-2" data-name="path12" class="cls-1"
d="M312.6,107.2l5.8,7.5c5.6,8.2,12.9,12.3,21.3,12.3,9-.2,15.3-7.5,17.7-10.3,0,0-4.4-3.7-9.9-9.8-7.5-8.2-17.5-23.3-17.7-24l-17.2,24.2Z" />
<path id="path16-2" data-name="path16" class="cls-1"
d="M227.4,97.4c-5.8,5-9.7,7.8-17.7,7.8-14.3,0-22.6-9.6-23.8-20.1h75.9c.5-1.4.7-3.2.7-6.2,0-29-22.6-50.7-52.2-50.7s-51.2,22.1-51.2,49.8,23,49.1,51.9,49.1,37.6-10.7,47.1-29.7h-30.8ZM210.7,50.1c12.6,0,22.1,7.8,24.3,18h-48c2.4-10.7,11.4-18,23.8-18h0Z" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -14,6 +14,7 @@ type AirDateBadgeProps = {
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
const WEEK = 1000 * 60 * 60 * 24 * 8;
const intl = useIntl();
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const dAirDate = new Date(airDate);
const nowDate = new Date();
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
@@ -38,7 +39,7 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
timeZone,
})}
</Badge>
{showRelative && (

View File

@@ -298,7 +298,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
src={
title?.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -233,7 +233,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -25,6 +25,10 @@ const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => {
return (
<>
<Component
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
{...componentProps}
className={`rounded-l-only ${componentProps.className ?? ''}`}
type={

View File

@@ -33,7 +33,7 @@ import { useRouter } from 'next/router';
import { useState } from 'react';
import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import useSWR, { mutate } from 'swr';
import * as Yup from 'yup';
const messages = defineMessages('components.IssueDetails', {
@@ -155,6 +155,7 @@ const IssueDetails = () => {
autoDismiss: true,
});
revalidateIssue();
mutate('/api/v1/issue/count');
} catch (e) {
addToast(intl.formatMessage(messages.toaststatusupdatefailed), {
appearance: 'error',
@@ -169,6 +170,7 @@ const IssueDetails = () => {
method: 'DELETE',
});
if (!res.ok) throw new Error();
mutate('/api/v1/issue/count');
addToast(intl.formatMessage(messages.toastissuedeleted), {
appearance: 'success',
@@ -240,7 +242,7 @@ const IssueDetails = () => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -142,7 +142,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -15,7 +15,7 @@ import { Field, Formik } from 'formik';
import Link from 'next/link';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import useSWR, { mutate } from 'swr';
import * as Yup from 'yup';
const messages = defineMessages('components.IssueModal.CreateIssueModal', {
@@ -138,6 +138,8 @@ const CreateIssueModal = ({
autoDismiss: true,
}
);
mutate('/api/v1/issue/count');
}
if (onCancel) {

View File

@@ -1,3 +1,4 @@
import Badge from '@app/components/Common/Badge';
import { menuMessages } from '@app/components/Layout/Sidebar';
import useClickOutside from '@app/hooks/useClickOutside';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -26,9 +27,16 @@ import {
} from '@heroicons/react/24/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { cloneElement, useRef, useState } from 'react';
import { cloneElement, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
interface MobileMenuProps {
pendingRequestsCount: number;
openIssuesCount: number;
revalidateIssueCount: () => void;
revalidateRequestsCount: () => void;
}
interface MenuLink {
href: string;
svgIcon: JSX.Element;
@@ -41,7 +49,12 @@ interface MenuLink {
dataTestId?: string;
}
const MobileMenu = () => {
const MobileMenu = ({
pendingRequestsCount,
openIssuesCount,
revalidateIssueCount,
revalidateRequestsCount,
}: MobileMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const intl = useIntl();
const [isOpen, setIsOpen] = useState(false);
@@ -139,6 +152,21 @@ const MobileMenu = () => {
})
);
useEffect(() => {
if (openIssuesCount) {
revalidateIssueCount();
}
if (pendingRequestsCount) {
revalidateRequestsCount();
}
}, [
revalidateIssueCount,
revalidateRequestsCount,
pendingRequestsCount,
openIssuesCount,
]);
return (
<div className="fixed bottom-0 left-0 right-0 z-50">
<Transition
@@ -159,7 +187,7 @@ const MobileMenu = () => {
<Link
key={`mobile-menu-link-${link.href}`}
href={link.href}
className={`flex items-center space-x-2 ${
className={`flex items-center ${
isActive ? 'text-indigo-500' : ''
}`}
onKeyDown={(e) => {
@@ -174,7 +202,25 @@ const MobileMenu = () => {
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
className: 'h-5 w-5',
})}
<span>{link.content}</span>
<span className="ml-2">{link.content}</span>
{link.href === '/requests' &&
pendingRequestsCount > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="ml-auto flex">
<Badge className="rounded-md border-indigo-500 bg-gradient-to-br from-indigo-600 to-purple-600">
{pendingRequestsCount}
</Badge>
</div>
)}
{link.href === '/issues' &&
openIssuesCount > 0 &&
hasPermission(Permission.MANAGE_ISSUES) && (
<div className="ml-auto flex">
<Badge className="rounded-md border-indigo-500 bg-gradient-to-br from-indigo-600 to-purple-600">
{openIssuesCount}
</Badge>
</div>
)}
</Link>
);
})}
@@ -190,7 +236,7 @@ const MobileMenu = () => {
<Link
key={`mobile-menu-link-${link.href}`}
href={link.href}
className={`flex flex-col items-center space-y-1 ${
className={`relative flex flex-col items-center space-y-1 ${
isActive ? 'text-indigo-500' : ''
}`}
>
@@ -200,6 +246,23 @@ const MobileMenu = () => {
className: 'h-6 w-6',
}
)}
{link.href === '/requests' &&
pendingRequestsCount > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="absolute left-3 bottom-3">
<Badge
className={`bg-gradient-to-br ${
router.pathname.match(link.activeRegExp)
? 'border-indigo-600 from-indigo-700 to-purple-700'
: 'border-indigo-500 from-indigo-600 to-purple-600'
} flex h-4 w-4 items-center justify-center !px-[9px] !py-[9px] text-[9px]`}
>
{pendingRequestsCount > 99
? '99+'
: pendingRequestsCount}
</Badge>
</div>
)}
</Link>
);
})}

View File

@@ -1,3 +1,4 @@
import Badge from '@app/components/Common/Badge';
import UserWarnings from '@app/components/Layout/UserWarnings';
import VersionStatus from '@app/components/Layout/VersionStatus';
import useClickOutside from '@app/hooks/useClickOutside';
@@ -18,7 +19,7 @@ import {
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useRef } from 'react';
import { Fragment, useEffect, useRef } from 'react';
import { useIntl } from 'react-intl';
export const menuMessages = defineMessages('components.Layout.Sidebar', {
@@ -35,6 +36,10 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', {
interface SidebarProps {
open?: boolean;
setClosed: () => void;
pendingRequestsCount: number;
openIssuesCount: number;
revalidateIssueCount: () => void;
revalidateRequestsCount: () => void;
}
interface SidebarLinkProps {
@@ -114,13 +119,35 @@ const SidebarLinks: SidebarLinkProps[] = [
},
];
const Sidebar = ({ open, setClosed }: SidebarProps) => {
const Sidebar = ({
open,
setClosed,
pendingRequestsCount,
openIssuesCount,
revalidateIssueCount,
revalidateRequestsCount,
}: SidebarProps) => {
const navRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const intl = useIntl();
const { hasPermission } = useUser();
useClickOutside(navRef, () => setClosed());
useEffect(() => {
if (openIssuesCount) {
revalidateIssueCount();
}
if (pendingRequestsCount) {
revalidateRequestsCount();
}
}, [
revalidateIssueCount,
revalidateRequestsCount,
pendingRequestsCount,
openIssuesCount,
]);
return (
<>
<div className="lg:hidden">
@@ -253,18 +280,48 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
href={sidebarLink.href}
as={sidebarLink.as}
className={`group flex items-center rounded-md px-2 py-2 text-lg font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
${
router.pathname.match(sidebarLink.activeRegExp)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
${
router.pathname.match(sidebarLink.activeRegExp)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
data-testid={sidebarLink.dataTestId}
>
{sidebarLink.svgIcon}
{intl.formatMessage(
menuMessages[sidebarLink.messagesKey]
)}
{sidebarLink.messagesKey === 'requests' &&
pendingRequestsCount > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="ml-auto flex">
<Badge
className={`rounded-md bg-gradient-to-br ${
router.pathname.match(sidebarLink.activeRegExp)
? 'border-indigo-600 from-indigo-700 to-purple-700'
: 'border-indigo-500 from-indigo-600 to-purple-600'
}`}
>
{pendingRequestsCount}
</Badge>
</div>
)}
{sidebarLink.messagesKey === 'issues' &&
openIssuesCount > 0 &&
hasPermission(Permission.MANAGE_ISSUES) && (
<div className="ml-auto flex">
<Badge
className={`rounded-md bg-gradient-to-br ${
router.pathname.match(sidebarLink.activeRegExp)
? 'border-indigo-600 from-indigo-700 to-purple-700'
: 'border-indigo-500 from-indigo-600 to-purple-600'
}`}
>
{openIssuesCount}
</Badge>
</div>
)}
</Link>
);
})}

View File

@@ -10,6 +10,7 @@ import { useUser } from '@app/hooks/useUser';
import { ArrowLeftIcon, Bars3BottomLeftIcon } from '@heroicons/react/24/solid';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
type LayoutProps = {
children: React.ReactNode;
@@ -22,6 +23,18 @@ const Layout = ({ children }: LayoutProps) => {
const router = useRouter();
const { currentSettings } = useSettings();
const { setLocale } = useLocale();
const { data: requestResponse, mutate: revalidateRequestsCount } = useSWR(
'/api/v1/request/count',
{
revalidateOnMount: true,
}
);
const { data: issueResponse, mutate: revalidateIssueCount } = useSWR(
'/api/v1/issue/count',
{
revalidateOnMount: true,
}
);
useEffect(() => {
if (setLocale && user) {
@@ -55,10 +68,21 @@ const Layout = ({ children }: LayoutProps) => {
<div className="absolute top-0 h-64 w-full bg-gradient-to-bl from-gray-800 to-gray-900">
<div className="relative inset-0 h-full w-full bg-gradient-to-t from-gray-900 to-transparent" />
</div>
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
<Sidebar
open={isSidebarOpen}
setClosed={() => setSidebarOpen(false)}
pendingRequestsCount={requestResponse?.pending ?? 0}
openIssuesCount={issueResponse?.open ?? 0}
revalidateIssueCount={() => revalidateIssueCount()}
revalidateRequestsCount={() => revalidateRequestsCount()}
/>
<div className="sm:hidden">
<MobileMenu />
<MobileMenu
pendingRequestsCount={requestResponse?.pending ?? 0}
openIssuesCount={issueResponse?.open ?? 0}
revalidateIssueCount={() => revalidateIssueCount()}
revalidateRequestsCount={() => revalidateRequestsCount()}
/>
</div>
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">

View File

@@ -114,6 +114,9 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
autoComplete="current-password"
data-testid="password"
className="!bg-gray-700/80 placeholder:text-gray-400"
data-1pignore="false"
data-lpignore="false"
data-bwignore="false"
/>
</div>
<div className="flex">

View File

@@ -25,7 +25,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers';
@@ -505,7 +505,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"
@@ -594,42 +594,45 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
</Button>
</Tooltip>
)}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<>
{toggleWatchlist ? (
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED &&
user?.userType !== UserType.PLEX && (
<>
{toggleWatchlist ? (
<Tooltip
content={intl.formatMessage(messages.addtowatchlist)}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<PlayButton links={mediaLinks} />
<RequestButton
mediaType="movie"

View File

@@ -2,7 +2,7 @@ interface PWAHeaderProps {
applicationTitle?: string;
}
const PWAHeader = ({ applicationTitle = 'Overseerr' }: PWAHeaderProps) => {
const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => {
return (
<>
<link

View File

@@ -20,6 +20,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import Link from 'next/link';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { mutate } from 'swr';
const messages = defineMessages('components.RequestBlock', {
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
@@ -59,6 +60,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
if (onUpdate) {
onUpdate();
mutate('/api/v1/request/count');
}
setIsUpdating(false);
};
@@ -72,6 +74,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
if (onUpdate) {
onUpdate();
mutate('/api/v1/request/count');
}
setIsUpdating(false);

View File

@@ -15,6 +15,7 @@ import type Media from '@server/entity/Media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import { useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import { mutate } from 'swr';
const messages = defineMessages('components.RequestButton', {
viewrequest: 'View Request',
@@ -101,6 +102,7 @@ const RequestButton = ({
if (data) {
onUpdate();
mutate('/api/v1/request/count');
}
};
@@ -123,6 +125,7 @@ const RequestButton = ({
);
onUpdate();
mutate('/api/v1/request/count');
};
const buttons: ButtonOption[] = [];

View File

@@ -80,6 +80,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
if (!res.ok) throw new Error();
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
mutate('/api/v1/request/count');
};
return (
@@ -271,6 +272,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
if (data) {
revalidate();
mutate('/api/v1/request/count');
}
};
@@ -280,6 +282,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
});
if (!res.ok) throw new Error();
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
mutate('/api/v1/request/count');
};
const retryRequest = async () => {
@@ -618,7 +621,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -27,7 +27,7 @@ import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import useSWR, { mutate } from 'swr';
const messages = defineMessages('components.RequestList.RequestItem', {
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
@@ -69,6 +69,7 @@ const RequestItemError = ({
});
if (!res.ok) throw new Error();
revalidateList();
mutate('/api/v1/request/count');
};
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
@@ -334,6 +335,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
if (data) {
revalidate();
mutate('/api/v1/request/count');
}
};
@@ -344,6 +346,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
if (!res.ok) throw new Error();
revalidateList();
mutate('/api/v1/request/count');
};
const deleteMediaFile = async () => {
@@ -453,7 +456,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -16,7 +16,7 @@ import type { Collection } from '@server/models/Collection';
import { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import useSWR, { mutate } from 'swr';
const messages = defineMessages('components.RequestModal', {
requestadmin: 'This request will be approved automatically.',
@@ -220,6 +220,7 @@ const CollectionRequestModal = ({
? MediaStatus.UNKNOWN
: MediaStatus.PARTIALLY_AVAILABLE
);
mutate('/api/v1/request/count');
}
addToast(
@@ -239,7 +240,16 @@ const CollectionRequestModal = ({
} finally {
setIsUpdating(false);
}
}, [requestOverrides, data, onComplete, addToast, intl, selectedParts, is4k]);
}, [
requestOverrides,
data?.parts,
data?.name,
onComplete,
addToast,
intl,
selectedParts,
is4k,
]);
const hasAutoApprove = hasPermission(
[
@@ -441,7 +451,7 @@ const CollectionRequestModal = ({
src={
part.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -104,6 +104,7 @@ const MovieRequestModal = ({
if (!res.ok) throw new Error();
const mediaRequest: MediaRequest = await res.json();
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
mutate('/api/v1/request/count');
if (mediaRequest) {
if (onComplete) {
@@ -138,7 +139,16 @@ const MovieRequestModal = ({
} finally {
setIsUpdating(false);
}
}, [data, onComplete, addToast, requestOverrides, hasPermission, intl, is4k]);
}, [
requestOverrides,
data?.id,
data?.title,
is4k,
onComplete,
addToast,
intl,
hasPermission,
]);
const cancelRequest = async () => {
setIsUpdating(true);
@@ -150,6 +160,7 @@ const MovieRequestModal = ({
if (!res.ok) throw new Error();
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
mutate('/api/v1/request/count');
if (res.status === 204) {
if (onComplete) {
@@ -197,6 +208,7 @@ const MovieRequestModal = ({
if (!res.ok) throw new Error();
}
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
mutate('/api/v1/request/count');
addToast(
<span>

View File

@@ -92,7 +92,7 @@ const SearchByNameModal = ({
<Image
src={
item.remotePoster ??
'/images/overseerr_poster_not_found.png'
'/images/jellyseerr_poster_not_found.png'
}
alt={item.title}
className="w-100 h-auto rounded-md"

View File

@@ -106,6 +106,7 @@ const TvRequestModal = ({
if (onUpdating) {
onUpdating(true);
mutate('/api/v1/request/count');
}
try {
@@ -141,6 +142,7 @@ const TvRequestModal = ({
if (!res.ok) throw new Error();
}
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
mutate('/api/v1/request/count');
addToast(
<span>
@@ -189,6 +191,7 @@ const TvRequestModal = ({
if (onUpdating) {
onUpdating(true);
mutate('/api/v1/request/count');
}
try {

View File

@@ -238,6 +238,10 @@ const NotificationsDiscord = () => {
name="botUsername"
type="text"
placeholder={settings.currentSettings.applicationTitle}
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.botUsername &&

View File

@@ -104,7 +104,7 @@ const NotificationsEmail = () => {
otherwise: Yup.string().nullable(),
})
.matches(
/-----BEGIN PGP PRIVATE KEY BLOCK-----.+-----END PGP PRIVATE KEY BLOCK-----/s,
/-----BEGIN PGP PRIVATE KEY BLOCK-----.+-----END PGP PRIVATE KEY BLOCK-----/,
intl.formatMessage(messages.validationPgpPrivateKey)
),
pgpPassword: Yup.string().when('pgpPrivateKey', {
@@ -295,6 +295,10 @@ const NotificationsEmail = () => {
name="emailFrom"
type="text"
inputMode="email"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.emailFrom &&
@@ -316,6 +320,10 @@ const NotificationsEmail = () => {
name="smtpHost"
type="text"
inputMode="url"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.smtpHost &&
@@ -337,6 +345,10 @@ const NotificationsEmail = () => {
type="text"
inputMode="numeric"
className="short"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
{errors.smtpPort &&
touched.smtpPort &&
@@ -390,7 +402,15 @@ const NotificationsEmail = () => {
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field id="authUser" name="authUser" type="text" />
<Field
id="authUser"
name="authUser"
type="text"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
</div>
</div>
@@ -400,12 +420,7 @@ const NotificationsEmail = () => {
</label>
<div className="form-input-area">
<div className="form-input-field">
<SensitiveInput
as="field"
id="authPass"
name="authPass"
autoComplete="one-time-code"
/>
<SensitiveInput as="field" id="authPass" name="authPass" />
</div>
</div>
</div>
@@ -430,6 +445,10 @@ const NotificationsEmail = () => {
type="textarea"
rows="10"
className="font-mono text-xs"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.pgpPrivateKey &&
@@ -457,7 +476,10 @@ const NotificationsEmail = () => {
as="field"
id="pgpPassword"
name="pgpPassword"
autoComplete="one-time-code"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.pgpPassword &&

View File

@@ -245,7 +245,7 @@ const NotificationsTelegram = () => {
as="field"
id="botAPI"
name="botAPI"
autoComplete="one-time-code"
type="text"
/>
</div>
{errors.botAPI &&
@@ -264,7 +264,15 @@ const NotificationsTelegram = () => {
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field id="botUsername" name="botUsername" type="text" />
<Field
id="botUsername"
name="botUsername"
type="text"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.botUsername &&
touched.botUsername &&
@@ -294,7 +302,15 @@ const NotificationsTelegram = () => {
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field id="chatId" name="chatId" type="text" />
<Field
id="chatId"
name="chatId"
type="text"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.chatId &&
touched.chatId &&

View File

@@ -382,6 +382,10 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
id="name"
name="name"
type="text"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('name', e.target.value);
@@ -475,7 +479,6 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
as="field"
id="apiKey"
name="apiKey"
autoComplete="one-time-code"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('apiKey', e.target.value);

View File

@@ -35,7 +35,7 @@ import useSWR from 'swr';
const messages = defineMessages('components.Settings.SettingsLogs', {
logs: 'Logs',
logsDescription:
'You can also view these logs directly via <code>stdout</code>, or in <code>{appDataPath}/logs/overseerr.log</code>.',
'You can also view these logs directly via <code>stdout</code>, or in <code>{appDataPath}/logs/jellyseerr.log</code>.',
time: 'Timestamp',
level: 'Severity',
label: 'Label',

View File

@@ -872,6 +872,10 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
id="tautulliPort"
name="tautulliPort"
className="short"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
{errors.tautulliPort &&
touched.tautulliPort &&
@@ -909,6 +913,10 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
inputMode="url"
id="tautulliUrlBase"
name="tautulliUrlBase"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.tautulliUrlBase &&
@@ -929,7 +937,6 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
as="field"
id="tautulliApiKey"
name="tautulliApiKey"
autoComplete="one-time-code"
/>
</div>
{errors.tautulliApiKey &&
@@ -950,6 +957,10 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
inputMode="url"
id="tautulliExternalUrl"
name="tautulliExternalUrl"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.tautulliExternalUrl &&

View File

@@ -119,6 +119,8 @@ const ServerInstance = ({
<h3 className="truncate font-medium leading-5 text-white">
<a
href={serviceUrl}
target="_blank"
rel="noopener noreferrer"
className="transition duration-300 hover:text-white hover:underline"
>
{name}
@@ -147,6 +149,8 @@ const ServerInstance = ({
</span>
<a
href={internalUrl}
target="_blank"
rel="noopener noreferrer"
className="transition duration-300 hover:text-white hover:underline"
>
{internalUrl}
@@ -159,7 +163,12 @@ const ServerInstance = ({
{profileName}
</p>
</div>
<a href={serviceUrl} className="opacity-50 hover:opacity-100">
<a
href={serviceUrl}
target="_blank"
rel="noopener noreferrer"
className="opacity-50 hover:opacity-100"
>
{isSonarr ? (
<SonarrLogo className="h-10 w-10 flex-shrink-0" />
) : (

View File

@@ -415,6 +415,10 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
id="name"
name="name"
type="text"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('name', e.target.value);
@@ -508,7 +512,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
as="field"
id="apiKey"
name="apiKey"
autoComplete="one-time-code"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('apiKey', e.target.value);

View File

@@ -8,7 +8,7 @@ import RequestModal from '@app/components/RequestModal';
import ErrorCard from '@app/components/TitleCard/ErrorCard';
import Placeholder from '@app/components/TitleCard/Placeholder';
import { useIsTouch } from '@app/hooks/useIsTouch';
import { Permission, useUser } from '@app/hooks/useUser';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { withProperties } from '@app/utils/typeHelpers';
@@ -352,7 +352,7 @@ const TitleCard = ({
src={
image
? `https://image.tmdb.org/t/p/w300_and_h450_face${image}`
: `/images/overseerr_poster_not_found_logo_top.png`
: `/images/jellyseerr_poster_not_found_logo_top.png`
}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
fill
@@ -373,42 +373,44 @@ const TitleCard = ({
: intl.formatMessage(globalMessages.tvshow)}
</div>
</div>
{showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
<div className="flex flex-col gap-1">
{toggleWatchlist ? (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={onClickWatchlistBtn}
>
<StarIcon className={'h-3 text-amber-300'} />
</Button>
) : (
<Button
className="z-40"
buttonSize={'sm'}
onClick={onClickDeleteWatchlistBtn}
>
<MinusCircleIcon className={'h-3'} />
</Button>
)}
{showHideButton &&
currentStatus !== MediaStatus.PROCESSING &&
currentStatus !== MediaStatus.AVAILABLE &&
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
currentStatus !== MediaStatus.PENDING && (
{showDetail &&
currentStatus !== MediaStatus.BLACKLISTED &&
user?.userType !== UserType.PLEX && (
<div className="flex flex-col gap-1">
{toggleWatchlist ? (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => setShowBlacklistModal(true)}
onClick={onClickWatchlistBtn}
>
<EyeSlashIcon className={'h-3'} />
<StarIcon className={'h-3 text-amber-300'} />
</Button>
) : (
<Button
className="z-40"
buttonSize={'sm'}
onClick={onClickDeleteWatchlistBtn}
>
<MinusCircleIcon className={'h-3'} />
</Button>
)}
</div>
)}
{showHideButton &&
currentStatus !== MediaStatus.PROCESSING &&
currentStatus !== MediaStatus.AVAILABLE &&
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
currentStatus !== MediaStatus.PENDING && (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => setShowBlacklistModal(true)}
>
<EyeSlashIcon className={'h-3'} />
</Button>
)}
</div>
)}
{showDetail &&
showHideButton &&
currentStatus == MediaStatus.BLACKLISTED && (

View File

@@ -28,7 +28,7 @@ import Season from '@app/components/TvDetails/Season';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers';
@@ -547,7 +547,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/overseerr_poster_not_found.png'
: '/images/jellyseerr_poster_not_found.png'
}
alt=""
sizes="100vw"
@@ -636,42 +636,45 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</Button>
</Tooltip>
)}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<>
{toggleWatchlist ? (
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED &&
user?.userType !== UserType.PLEX && (
<>
{toggleWatchlist ? (
<Tooltip
content={intl.formatMessage(messages.addtowatchlist)}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<PlayButton links={mediaLinks} />
<RequestButton
mediaType="tv"

View File

@@ -210,7 +210,9 @@ const UserList = () => {
username: Yup.string().required(
intl.formatMessage(messages.validationUsername)
),
email: Yup.string().email(intl.formatMessage(messages.validationEmail)),
email: Yup.string()
.required()
.email(intl.formatMessage(messages.validationEmail)),
password: Yup.lazy((value) =>
!value
? Yup.string()
@@ -388,6 +390,7 @@ const UserList = () => {
<div className="form-row">
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
<span className="label-required">*</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
@@ -396,6 +399,10 @@ const UserList = () => {
name="email"
type="text"
inputMode="email"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.email &&

View File

@@ -100,7 +100,9 @@ const UserGeneralSettings = () => {
const UserGeneralSettingsSchema = Yup.object().shape({
email:
user?.id === 1
// email is required for everybody except non-admin jellyfin users
user?.id === 1 ||
(user?.userType !== UserType.JELLYFIN && user?.userType !== UserType.EMBY)
? Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired))

View File

@@ -10,7 +10,7 @@ export interface SettingsContextProps {
const defaultSettings = {
initialized: false,
applicationTitle: 'Overseerr',
applicationTitle: 'Jellyseerr',
applicationUrl: '',
hideAvailable: false,
localLogin: true,

View File

@@ -904,7 +904,7 @@
"components.Settings.SettingsLogs.level": "Severity",
"components.Settings.SettingsLogs.logDetails": "Log Details",
"components.Settings.SettingsLogs.logs": "Logs",
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{appDataPath}/logs/overseerr.log</code>.",
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{appDataPath}/logs/jellyseerr.log</code>.",
"components.Settings.SettingsLogs.message": "Message",
"components.Settings.SettingsLogs.pauseLogs": "Pause",
"components.Settings.SettingsLogs.resumeLogs": "Resume",

View File

@@ -11,6 +11,7 @@ import { LanguageContext } from '@app/context/LanguageContext';
import { SettingsProvider } from '@app/context/SettingsContext';
import { UserContext } from '@app/context/UserContext';
import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser';
import '@app/styles/globals.css';
import '@app/utils/fetchOverride';
import { polyfillIntl } from '@app/utils/polyfillIntl';
@@ -128,6 +129,35 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
loadLocaleData(currentLocale).then(setMessages);
}, [currentLocale]);
const { hasPermission } = useUser();
useEffect(() => {
const requestsCount = async () => {
const response = await fetch('/api/v1/request/count');
return await response.json();
};
// Cast navigator to a type that includes setAppBadge and clearAppBadge
// to avoid TypeScript errors while ensuring these methods exist before calling them.
const newNavigator = navigator as unknown as {
setAppBadge?: (count: number) => Promise<void>;
clearAppBadge?: () => Promise<void>;
};
if ('setAppBadge' in navigator) {
if (
!router.pathname.match(/(login|setup|resetpassword)/) &&
hasPermission(Permission.ADMIN)
) {
requestsCount().then((data) =>
newNavigator?.setAppBadge?.(data.pending)
);
} else {
newNavigator?.clearAppBadge?.();
}
}
}, [hasPermission, router.pathname]);
if (router.pathname.match(/(login|setup|resetpassword)/)) {
component = <Component {...pageProps} />;
} else {

View File

@@ -57,14 +57,14 @@ class PlexOAuth {
const browser = Bowser.getParser(window.navigator.userAgent);
this.plexHeaders = {
Accept: 'application/json',
'X-Plex-Product': 'Overseerr',
'X-Plex-Product': 'Jellyseerr',
'X-Plex-Version': 'Plex OAuth',
'X-Plex-Client-Identifier': clientId,
'X-Plex-Model': 'Plex OAuth',
'X-Plex-Platform': browser.getBrowserName(),
'X-Plex-Platform-Version': browser.getBrowserVersion(),
'X-Plex-Device': browser.getOSName(),
'X-Plex-Device-Name': `${browser.getBrowserName()} (Overseerr)`,
'X-Plex-Device-Name': `${browser.getBrowserName()} (Jellyseerr)`,
'X-Plex-Device-Screen-Resolution':
window.screen.width + 'x' + window.screen.height,
'X-Plex-Language': 'en',