diff --git a/.all-contributorsrc b/.all-contributorsrc index 5468b39c..8dbd97d6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -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" + ] } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab5f59da..1835e949 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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). Translation status diff --git a/README.md b/README.md index 6ed6bff7..8bb9c7c9 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon Joshua M. Boniface
Joshua M. Boniface

💻 - Gauthier
Gauthier

💻 + Gauthier
Gauthier

💻 🚧 Kara
Kara

🚇 Joaquin Olivero
Joaquin Olivero

💻 Julian Behr
Julian Behr

🌍 @@ -288,7 +288,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon byakurau
byakurau

🌍 miknii
miknii

🌍 Mackenzie
Mackenzie

💻 - soup
soup

📖 + soup
soup

📖 💻 ceptonit
ceptonit

📖 aedelbro
aedelbro

💻 @@ -321,6 +321,8 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon Ahmed Siddiqui
Ahmed Siddiqui

💻 + JackOXI
JackOXI

💻 + Stancu Florin
Stancu Florin

💻 diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index e49d8888..f376d880 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -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": { diff --git a/overseerr-api.yml b/jellyseerr-api.yml similarity index 99% rename from overseerr-api.yml rename to jellyseerr-api.yml index a713a5a1..ddd94202 100644 --- a/overseerr-api.yml +++ b/jellyseerr-api.yml @@ -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 diff --git a/package.json b/package.json index 74512020..ce508dd3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/images/overseerr_poster_not_found.png b/public/images/jellyseerr_poster_not_found.png similarity index 100% rename from public/images/overseerr_poster_not_found.png rename to public/images/jellyseerr_poster_not_found.png diff --git a/public/images/overseerr_poster_not_found_logo_center.png b/public/images/jellyseerr_poster_not_found_logo_center.png similarity index 100% rename from public/images/overseerr_poster_not_found_logo_center.png rename to public/images/jellyseerr_poster_not_found_logo_center.png diff --git a/public/images/overseerr_poster_not_found_logo_top.png b/public/images/jellyseerr_poster_not_found_logo_top.png similarity index 100% rename from public/images/overseerr_poster_not_found_logo_top.png rename to public/images/jellyseerr_poster_not_found_logo_top.png diff --git a/public/sw.js b/public/sw.js index 6a89315a..3aec6343 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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)); }); diff --git a/server/api/github.ts b/server/api/github.ts index c2c3fe6a..50027218 100644 --- a/server/api/github.ts +++ b/server/api/github.ts @@ -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 []; diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index 977d367b..5007fe05 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -124,9 +124,9 @@ class PlexAPI { // }, options: { identifier: settings.clientId, - product: 'Overseerr', - deviceName: 'Overseerr', - platform: 'Overseerr', + product: 'Jellyseerr', + deviceName: 'Jellyseerr', + platform: 'Jellyseerr', }, }); } diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 5cf449ea..cb4d4071 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -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 ); diff --git a/server/index.ts b/server/index.ts index e4e872ab..88baedb8 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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(); diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index d2b0b165..952e1acf 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -19,6 +19,8 @@ export interface NotificationPayload { request?: MediaRequest; issue?: Issue; comment?: IssueComment; + pendingRequestsCount?: number; + isAdmin?: boolean; } export abstract class BaseAgent { diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 7a3b9790..1d6485cc 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -188,7 +188,7 @@ class SlackAgent type: 'actions', elements: [ { - action_id: 'open-in-overseerr', + action_id: 'open-in-jellyseerr', type: 'button', url, text: { diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index 275a77e8..143961ec 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -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); }) ); } diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index 4919bf70..ed488767 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -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: diff --git a/server/logger.ts b/server/logger.ts index d5809a0e..4708dd20 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -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 diff --git a/server/routes/auth.ts b/server/routes/auth.ts index cec19e11..45eea256 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -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, diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 4bb12740..79ae7f28 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -837,7 +837,8 @@ discoverRoutes.get, 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, WatchlistResponse>( }); } + // List watchlist from Plex const plexTV = new PlexTvAPI(activeUser.plexToken); const watchlist = await plexTV.getWatchlist({ offset }); diff --git a/server/routes/index.ts b/server/routes/index.ts index f064e603..7d0ad5d8 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -55,7 +55,7 @@ router.get('/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('/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', }); }); diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 6ee0f893..ab6bd737 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -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); diff --git a/src/assets/services/plex.svg b/src/assets/services/plex.svg index 14c5abd9..53f28d1c 100644 --- a/src/assets/services/plex.svg +++ b/src/assets/services/plex.svg @@ -1,85 +1,43 @@ - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/AirDateBadge/index.tsx b/src/components/AirDateBadge/index.tsx index d4e438a6..a51f39fc 100644 --- a/src/components/AirDateBadge/index.tsx +++ b/src/components/AirDateBadge/index.tsx @@ -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, })} {showRelative && ( diff --git a/src/components/Blacklist/index.tsx b/src/components/Blacklist/index.tsx index a752e95f..a8b8dae7 100644 --- a/src/components/Blacklist/index.tsx +++ b/src/components/Blacklist/index.tsx @@ -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" diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index d9c9a813..c56781e8 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -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" diff --git a/src/components/Common/SensitiveInput/index.tsx b/src/components/Common/SensitiveInput/index.tsx index ae11b951..9a92d254 100644 --- a/src/components/Common/SensitiveInput/index.tsx +++ b/src/components/Common/SensitiveInput/index.tsx @@ -25,6 +25,10 @@ const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => { return ( <> { 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" diff --git a/src/components/IssueList/IssueItem/index.tsx b/src/components/IssueList/IssueItem/index.tsx index 7fd58b5e..53c618fe 100644 --- a/src/components/IssueList/IssueItem/index.tsx +++ b/src/components/IssueList/IssueItem/index.tsx @@ -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" diff --git a/src/components/IssueModal/CreateIssueModal/index.tsx b/src/components/IssueModal/CreateIssueModal/index.tsx index 8d880385..58836ef8 100644 --- a/src/components/IssueModal/CreateIssueModal/index.tsx +++ b/src/components/IssueModal/CreateIssueModal/index.tsx @@ -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) { diff --git a/src/components/Layout/MobileMenu/index.tsx b/src/components/Layout/MobileMenu/index.tsx index fe1e2e40..52e84d3d 100644 --- a/src/components/Layout/MobileMenu/index.tsx +++ b/src/components/Layout/MobileMenu/index.tsx @@ -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(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 (
{ { @@ -174,7 +202,25 @@ const MobileMenu = () => { {cloneElement(isActive ? link.svgIconSelected : link.svgIcon, { className: 'h-5 w-5', })} - {link.content} + {link.content} + {link.href === '/requests' && + pendingRequestsCount > 0 && + hasPermission(Permission.MANAGE_REQUESTS) && ( +
+ + {pendingRequestsCount} + +
+ )} + {link.href === '/issues' && + openIssuesCount > 0 && + hasPermission(Permission.MANAGE_ISSUES) && ( +
+ + {openIssuesCount} + +
+ )} ); })} @@ -190,7 +236,7 @@ const MobileMenu = () => { @@ -200,6 +246,23 @@ const MobileMenu = () => { className: 'h-6 w-6', } )} + {link.href === '/requests' && + pendingRequestsCount > 0 && + hasPermission(Permission.MANAGE_REQUESTS) && ( +
+ + {pendingRequestsCount > 99 + ? '99+' + : pendingRequestsCount} + +
+ )} ); })} diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index a947e262..d578bef8 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -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(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 ( <>
@@ -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) && ( +
+ + {pendingRequestsCount} + +
+ )} + {sidebarLink.messagesKey === 'issues' && + openIssuesCount > 0 && + hasPermission(Permission.MANAGE_ISSUES) && ( +
+ + {openIssuesCount} + +
+ )} ); })} diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index a1964b0b..50d463cf 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -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) => {
- - setSidebarOpen(false)} /> + setSidebarOpen(false)} + pendingRequestsCount={requestResponse?.pending ?? 0} + openIssuesCount={issueResponse?.open ?? 0} + revalidateIssueCount={() => revalidateIssueCount()} + revalidateRequestsCount={() => revalidateRequestsCount()} + />
- + revalidateIssueCount()} + revalidateRequestsCount={() => revalidateRequestsCount()} + />
diff --git a/src/components/Login/LocalLogin.tsx b/src/components/Login/LocalLogin.tsx index 2372bc7f..74bc1e39 100644 --- a/src/components/Login/LocalLogin.tsx +++ b/src/components/Login/LocalLogin.tsx @@ -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" />
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 72a598f0..2340cb2e 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -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) => { )} - {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && ( - <> - {toggleWatchlist ? ( - - - - ) : ( - - + + ) : ( + - {isUpdating ? ( - - ) : ( - - )} - - - )} - - )} + + + )} + + )} { +const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => { return ( <> { 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); diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index cbe04fe3..957a3390 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -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[] = []; diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 7f08044e..e936d98e 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -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" diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 018fa915..5e764ecb 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -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" diff --git a/src/components/RequestModal/CollectionRequestModal.tsx b/src/components/RequestModal/CollectionRequestModal.tsx index 28aae73a..0f83bea7 100644 --- a/src/components/RequestModal/CollectionRequestModal.tsx +++ b/src/components/RequestModal/CollectionRequestModal.tsx @@ -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" diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index 85af7aef..75638586 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -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( diff --git a/src/components/RequestModal/SearchByNameModal/index.tsx b/src/components/RequestModal/SearchByNameModal/index.tsx index 1b86b614..1aa55f3f 100644 --- a/src/components/RequestModal/SearchByNameModal/index.tsx +++ b/src/components/RequestModal/SearchByNameModal/index.tsx @@ -92,7 +92,7 @@ const SearchByNameModal = ({ {item.title} @@ -189,6 +191,7 @@ const TvRequestModal = ({ if (onUpdating) { onUpdating(true); + mutate('/api/v1/request/count'); } try { diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 82ac6840..d34edb64 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -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" />
{errors.botUsername && diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index fdb292d3..6daff08d 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -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" />
{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" />
{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 = () => {
- +
@@ -400,12 +420,7 @@ const NotificationsEmail = () => {
- +
@@ -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" /> {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" /> {errors.pgpPassword && diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index 6636c6b4..bfac39fe 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -245,7 +245,7 @@ const NotificationsTelegram = () => { as="field" id="botAPI" name="botAPI" - autoComplete="one-time-code" + type="text" /> {errors.botAPI && @@ -264,7 +264,15 @@ const NotificationsTelegram = () => {
- +
{errors.botUsername && touched.botUsername && @@ -294,7 +302,15 @@ const NotificationsTelegram = () => {
- +
{errors.chatId && touched.chatId && diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index fbeb2dec..b1ae42f7 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -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) => { 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) => { setIsValidated(false); setFieldValue('apiKey', e.target.value); diff --git a/src/components/Settings/SettingsLogs/index.tsx b/src/components/Settings/SettingsLogs/index.tsx index ef7a396f..4fbcafc0 100644 --- a/src/components/Settings/SettingsLogs/index.tsx +++ b/src/components/Settings/SettingsLogs/index.tsx @@ -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 stdout, or in {appDataPath}/logs/overseerr.log.', + 'You can also view these logs directly via stdout, or in {appDataPath}/logs/jellyseerr.log.', time: 'Timestamp', level: 'Severity', label: 'Label', diff --git a/src/components/Settings/SettingsPlex.tsx b/src/components/Settings/SettingsPlex.tsx index 780dc0ee..2a2c6167 100644 --- a/src/components/Settings/SettingsPlex.tsx +++ b/src/components/Settings/SettingsPlex.tsx @@ -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" />
{errors.tautulliUrlBase && @@ -929,7 +937,6 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => { as="field" id="tautulliApiKey" name="tautulliApiKey" - autoComplete="one-time-code" />
{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" /> {errors.tautulliExternalUrl && diff --git a/src/components/Settings/SettingsServices.tsx b/src/components/Settings/SettingsServices.tsx index fc058c0b..65180cac 100644 --- a/src/components/Settings/SettingsServices.tsx +++ b/src/components/Settings/SettingsServices.tsx @@ -119,6 +119,8 @@ const ServerInstance = ({

{name} @@ -147,6 +149,8 @@ const ServerInstance = ({ {internalUrl} @@ -159,7 +163,12 @@ const ServerInstance = ({ {profileName}

-
+ {isSonarr ? ( ) : ( diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx index fedea2a6..0728c54d 100644 --- a/src/components/Settings/SonarrModal/index.tsx +++ b/src/components/Settings/SonarrModal/index.tsx @@ -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) => { 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) => { setIsValidated(false); setFieldValue('apiKey', e.target.value); diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx index 6312994e..5bfb90e9 100644 --- a/src/components/TitleCard/index.tsx +++ b/src/components/TitleCard/index.tsx @@ -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)} - {showDetail && currentStatus !== MediaStatus.BLACKLISTED && ( -
- {toggleWatchlist ? ( - - ) : ( - - )} - {showHideButton && - currentStatus !== MediaStatus.PROCESSING && - currentStatus !== MediaStatus.AVAILABLE && - currentStatus !== MediaStatus.PARTIALLY_AVAILABLE && - currentStatus !== MediaStatus.PENDING && ( + {showDetail && + currentStatus !== MediaStatus.BLACKLISTED && + user?.userType !== UserType.PLEX && ( +
+ {toggleWatchlist ? ( + ) : ( + )} -
- )} + {showHideButton && + currentStatus !== MediaStatus.PROCESSING && + currentStatus !== MediaStatus.AVAILABLE && + currentStatus !== MediaStatus.PARTIALLY_AVAILABLE && + currentStatus !== MediaStatus.PENDING && ( + + )} +
+ )} {showDetail && showHideButton && currentStatus == MediaStatus.BLACKLISTED && ( diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 77349b5e..ec27fba1 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -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) => { )} - {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && ( - <> - {toggleWatchlist ? ( - - - - ) : ( - - + + ) : ( + - {isUpdating ? ( - - ) : ( - - )} - - - )} - - )} + + + )} + + )} { 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 = () => {
@@ -396,6 +399,10 @@ const UserList = () => { name="email" type="text" inputMode="email" + autoComplete="off" + data-1pignore="true" + data-lpignore="true" + data-bwignore="true" />
{errors.email && diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 4ee8a80f..d8f0ded0 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -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)) diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index ee94e092..a1b69ae2 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -10,7 +10,7 @@ export interface SettingsContextProps { const defaultSettings = { initialized: false, - applicationTitle: 'Overseerr', + applicationTitle: 'Jellyseerr', applicationUrl: '', hideAvailable: false, localLogin: true, diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index bd2ce864..73dfc7b8 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -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 stdout, or in {appDataPath}/logs/overseerr.log.", + "components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via stdout, or in {appDataPath}/logs/jellyseerr.log.", "components.Settings.SettingsLogs.message": "Message", "components.Settings.SettingsLogs.pauseLogs": "Pause", "components.Settings.SettingsLogs.resumeLogs": "Resume", diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3ab8ab13..9e87cbdf 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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 = ({ 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; + clearAppBadge?: () => Promise; + }; + + 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 = ; } else { diff --git a/src/utils/plex.ts b/src/utils/plex.ts index 448da94a..5c0d1906 100644 --- a/src/utils/plex.ts +++ b/src/utils/plex.ts @@ -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',