From f0a605577469248a2a7c2170be8310e106131c59 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Sun, 23 Feb 2025 11:25:25 +0100 Subject: [PATCH 1/6] fix: add email requirement for local users (#1389) * fix: add email requirement for local users Because of a misunderstanding, and the requirement to have a mandatory email for local users was removed, when it shouldn't have been. re #900 fix #1367 * fix: add missing check for Emby --- server/routes/user/usersettings.ts | 20 +------------------ src/components/UserList/index.tsx | 5 ++++- .../UserGeneralSettings/index.tsx | 4 +++- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 6ee0f893..ac89b048 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 }, diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 95a3c8a6..bc7c4441 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -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 = () => {
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)) From a790b1abccfa9c3f8272ade8cd055017905dd87f Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+fallenbagel@users.noreply.github.com> Date: Mon, 24 Feb 2025 05:35:12 +0800 Subject: [PATCH 2/6] feat(airdatebadge): convert airDate from UTC to local timezone (#1390) This PR will ensure that the airdate is in the user's local timezone so that its more user friendly and the relative time calculation would be consistent fix #1373 --- src/components/AirDateBadge/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 && ( From 27112be9330eb271a2030848ae3f530715300ab4 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Wed, 26 Feb 2025 06:08:00 +0100 Subject: [PATCH 3/6] refactor: rename some remaining Overseerr occurrences (#1397) * refactor: rename some remaining Overseerr occurrences This PR renames some remaining occurrences of Overseerr to Jellyseerr. This includes the OpenAPI specification, log file names and some variables and console messages. * fix(i18n): add missing translation --- CONTRIBUTING.md | 2 +- cypress/config/settings.cypress.json | 4 ++-- overseerr-api.yml => jellyseerr-api.yml | 16 ++++++++-------- package.json | 2 +- ...ound.png => jellyseerr_poster_not_found.png} | Bin ...jellyseerr_poster_not_found_logo_center.png} | Bin ...=> jellyseerr_poster_not_found_logo_top.png} | Bin server/api/github.ts | 8 ++++---- server/api/plexapi.ts | 6 +++--- server/index.ts | 4 ++-- server/lib/notifications/agents/slack.ts | 2 +- server/lib/watchlistsync.ts | 2 +- server/logger.ts | 6 +++--- server/routes/auth.ts | 10 +++++----- server/routes/index.ts | 6 +++--- server/routes/user/usersettings.ts | 2 +- src/components/Blacklist/index.tsx | 2 +- src/components/CollectionDetails/index.tsx | 2 +- src/components/IssueDetails/index.tsx | 2 +- src/components/IssueList/IssueItem/index.tsx | 2 +- src/components/MovieDetails/index.tsx | 2 +- src/components/PWAHeader/index.tsx | 2 +- src/components/RequestCard/index.tsx | 2 +- .../RequestList/RequestItem/index.tsx | 2 +- .../RequestModal/CollectionRequestModal.tsx | 2 +- .../RequestModal/SearchByNameModal/index.tsx | 2 +- src/components/Settings/SettingsLogs/index.tsx | 2 +- src/components/TitleCard/index.tsx | 2 +- src/components/TvDetails/index.tsx | 2 +- src/context/SettingsContext.tsx | 2 +- src/i18n/locale/en.json | 2 +- src/utils/plex.ts | 4 ++-- 32 files changed, 52 insertions(+), 52 deletions(-) rename overseerr-api.yml => jellyseerr-api.yml (99%) rename public/images/{overseerr_poster_not_found.png => jellyseerr_poster_not_found.png} (100%) rename public/images/{overseerr_poster_not_found_logo_center.png => jellyseerr_poster_not_found_logo_center.png} (100%) rename public/images/{overseerr_poster_not_found_logo_top.png => jellyseerr_poster_not_found_logo_top.png} (100%) 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/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/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/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/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/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 31c846ad..4e470831 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -158,7 +158,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, @@ -274,7 +274,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { if (user) { deviceId = user.jellyfinDeviceId ?? ''; } else { - deviceId = Buffer.from(`BOT_overseerr_${body.username ?? ''}`).toString( + deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString( 'base64' ); } @@ -446,7 +446,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { }); } else if (!user) { logger.info( - 'Sign-in attempt from Jellyfin user with access to the media server; creating new Overseerr user', + 'Sign-in attempt from Jellyfin user with access to the media server; creating new Jellyseerr user', { label: 'API', ip: req.ip, @@ -584,7 +584,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, @@ -674,7 +674,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/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 ac89b048..ab6bd737 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -419,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/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/IssueDetails/index.tsx b/src/components/IssueDetails/index.tsx index 5ceb5796..d41a6684 100644 --- a/src/components/IssueDetails/index.tsx +++ b/src/components/IssueDetails/index.tsx @@ -240,7 +240,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/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 72a598f0..3fb88ab3 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -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" diff --git a/src/components/PWAHeader/index.tsx b/src/components/PWAHeader/index.tsx index 0dde7e42..777b63e0 100644 --- a/src/components/PWAHeader/index.tsx +++ b/src/components/PWAHeader/index.tsx @@ -2,7 +2,7 @@ interface PWAHeaderProps { applicationTitle?: string; } -const PWAHeader = ({ applicationTitle = 'Overseerr' }: PWAHeaderProps) => { +const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => { return ( <> { 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..3d64941e 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -453,7 +453,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..4dd79524 100644 --- a/src/components/RequestModal/CollectionRequestModal.tsx +++ b/src/components/RequestModal/CollectionRequestModal.tsx @@ -441,7 +441,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/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}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/TitleCard/index.tsx b/src/components/TitleCard/index.tsx index 6312994e..8b6d6874 100644 --- a/src/components/TitleCard/index.tsx +++ b/src/components/TitleCard/index.tsx @@ -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 diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 77349b5e..b4f0389f 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -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" 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/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', From 4eddbaa71b7972b6db33976102501fb8b6333206 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Wed, 26 Feb 2025 10:47:11 +0100 Subject: [PATCH 4/6] fix(watchlist): disable Jellyseerr's watchlist for Plex users (#1398) This PR resolves a conflict between the Plex and Jellyseerr watchlists by deactivating the Jellyseerr watchlist for Plex. fix #1344 --- server/routes/discover.ts | 4 +- src/components/MovieDetails/index.tsx | 73 ++++++++++++++------------- src/components/TitleCard/index.tsx | 62 ++++++++++++----------- src/components/TvDetails/index.tsx | 73 ++++++++++++++------------- 4 files changed, 111 insertions(+), 101 deletions(-) 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/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 3fb88ab3..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'; @@ -594,42 +594,45 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { )} - {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && ( - <> - {toggleWatchlist ? ( - - - - ) : ( - - + + ) : ( + - {isUpdating ? ( - - ) : ( - - )} - - - )} - - )} + + + )} + + )}
- {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 b4f0389f..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'; @@ -636,42 +636,45 @@ const TvDetails = ({ tv }: TvDetailsProps) => { )} - {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && ( - <> - {toggleWatchlist ? ( - - - - ) : ( - - + + ) : ( + - {isUpdating ? ( - - ) : ( - - )} - - - )} - - )} + + + )} + + )} Date: Thu, 27 Feb 2025 16:40:13 +0800 Subject: [PATCH 5/6] docs: add gauthier-th as a contributor for maintenance (#1408) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 5468b39c..610aebc9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -338,7 +338,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4", "profile": "https://gauthierth.fr/", "contributions": [ - "code" + "code", + "maintenance" ] }, { diff --git a/README.md b/README.md index 6ed6bff7..4cc83f49 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

🌍 From 3b4d6bf5b8f3df295a4f204c56126aadd278e22f Mon Sep 17 00:00:00 2001 From: Gauthier Date: Thu, 27 Feb 2025 12:13:14 +0100 Subject: [PATCH 6/6] chore: merge upstream (#1404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(pushover): attach image to pushover notification payload (#3701) * fix: api language query parameter (#3720) * docs: add j0srisk as a contributor for code (#3745) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(tooltip): add tooltip to display exact time on date hover (#3773) Co-authored-by: Loetwiek * docs: add Loetwiek as a contributor for code (#3776) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix(ui): ensure title fits into the `view collection` box (#3696) * fix(docs): correct openapi docs minor issues (#3648) * docs: add Fuochi as a contributor for doc (#3826) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: translations update from Hosted Weblate (#3597) * feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (1232 of 1234 strings) Co-authored-by: Cleiton Carvalho Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (German) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (German) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1236 of 1236 strings) feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Anders Ecklon Co-authored-by: Hosted Weblate Co-authored-by: Kenneth Hansen Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/da/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Greek) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Greek) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: BeardedWatermelon Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/el/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 99.5% (1234 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Romanian) Currently translated at 37.1% (461 of 1240 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 37.0% (459 of 1240 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 34.8% (432 of 1240 strings) Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ro/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 57.4% (712 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 13.2% (164 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 4.8% (60 of 1240 strings) feat(lang): added translation using Weblate (Bulgarian) Co-authored-by: Hosted Weblate Co-authored-by: sct Co-authored-by: Димитър Мазнеков (Topper) Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/bg/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 97.9% (1215 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 82.0% (1017 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 72.9% (905 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 72.9% (905 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 71.3% (885 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 64.9% (805 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 64.4% (799 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 63.8% (792 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 63.7% (791 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 57.5% (714 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 49.9% (619 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 35.9% (446 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 35.9% (446 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 32.1% (399 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 24.6% (306 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 18.9% (235 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 17.5% (217 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 17.3% (215 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 8.0% (100 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 3.3% (41 of 1240 strings) feat(lang): added translation using Weblate (Ukrainian) Co-authored-by: Hosted Weblate Co-authored-by: Michael Michael Co-authored-by: sct Co-authored-by: Сергій Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/uk/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: dtalens Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Czech) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Czech) Currently translated at 99.6% (1236 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Karel Krýda Co-authored-by: Smexhy Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/cs/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Croatian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.8% (1238 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.8% (1238 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.6% (1236 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.5% (1235 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.5% (1235 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 97.5% (1210 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.5% (1185 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.6% (1182 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.6% (1182 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.2% (1177 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.2% (1177 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 94.3% (1166 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 91.7% (1134 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 91.7% (1134 of 1236 strings) Co-authored-by: Bruno Ševčenko Co-authored-by: Hosted Weblate Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Hungarian) Currently translated at 91.3% (1133 of 1240 strings) feat(lang): translated using Weblate (Hungarian) Currently translated at 89.3% (1108 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Levente Szajkó Co-authored-by: Nandor Rusz Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Hebrew) Currently translated at 13.9% (172 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: osh Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/he/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Polish) Currently translated at 99.1% (1225 of 1236 strings) Co-authored-by: Eryk Michalak Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pl/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Italian) Currently translated at 92.8% (1148 of 1236 strings) Co-authored-by: Francesco Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Fhd-pro Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ar/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: Kobe Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1236 of 1236 strings) feat(lang): translated using Weblate (French) Currently translated at 99.9% (1235 of 1236 strings) feat(lang): translated using Weblate (French) Currently translated at 99.9% (1235 of 1236 strings) Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Hosted Weblate Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Finnish) Currently translated at 2.6% (33 of 1240 strings) feat(lang): added translation using Weblate (Finnish) Co-authored-by: Eero Konttaniemi Co-authored-by: Hosted Weblate Co-authored-by: sct Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fi/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Serbian) Currently translated at 50.8% (630 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Milan Smudja Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Developer J Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ko/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Haohao Zhang Co-authored-by: Hosted Weblate Co-authored-by: lkw123 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Cleiton Carvalho Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Co-authored-by: Anders Ecklon Co-authored-by: Kenneth Hansen Co-authored-by: BeardedWatermelon Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: sct Co-authored-by: Michael Michael Co-authored-by: Сергій Co-authored-by: dtalens Co-authored-by: Karel Krýda Co-authored-by: Smexhy Co-authored-by: Bruno Ševčenko Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Co-authored-by: Levente Szajkó Co-authored-by: osh Co-authored-by: Eryk Michalak Co-authored-by: Francesco Co-authored-by: Fhd-pro Co-authored-by: Kobe Co-authored-by: gallegonovato Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Eero Konttaniemi Co-authored-by: Milan Smudja Co-authored-by: Developer J Co-authored-by: Haohao Zhang Co-authored-by: lkw123 * feat(lang): add lang config for Bulgarian, Finnish, Ukrainian, Indonesian, Slovak, Turkish and Maori (#3834) * fix: correct deeplinks on iPad (#3883) * feat(studios): add a24 to studios list (#3902) * docs: add demrich as a contributor for code (#3906) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(watchlist): Cache watchlist requests with matching E-Tags (#3901) * perf(watchlist): add E-Tag caching to Plex watchlist requests * refactor(watchlist): increase frequency of watchlist requests * fix: sync watchlist every 3 min instead of 3 sec * docs: add maxnatamo as a contributor for code (#3907) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(plex): refresh token schedule (#3875) * feat: refresh token schedule fix #3861 * fix(i18n): add i18n message * refactor(plextv): use randomUUID crypto instead custom function * docs: add DamsDev1 as a contributor for code (#3924) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix: correct icon showing on certain phones when not pulled (#3939) * feat: add support for requesting "Specials" for TV Shows (#3724) * feat: add support for requesting "Specials" for TV Shows This commit is responsible for adding support in Overseerr for requesting "Special" episodes for TV Shows. This request has become especially pertinent when you consider shows like "Doctor Who". These shows have Specials that are critical to understanding the plot of a TV show. fix #779 * chore(yarn.lock): undo inappropriate changes to yarn.lock I was informed by @sct in a comment on the #3724 PR that it was not appropriate to commit the changes that ended up being made to the yarn.lock file. This commit is responsible, then, for undoing the changes to the yarn.lock file that ended up being submitted. * refactor: change loose equality to strict equality I received a comment from OwsleyJr pointing out that we are using loose equality when we could alternatively just be using strict equality to increase the robustness of our code. This commit does exactly that by squashing out previous usages of loose equality in my commits and replacing them with strict equality * refactor: move 'Specials' string to a global message Owsley pointed out that we are redefining the 'Specials' string multiple times throughout this PR. Instead, we can just move it as a global message. This commit does exactly that. It squashes out and previous declarations of the 'Specials' string inside the src files, and moves it directly to the global messages file. * docs: add AhmedNSidd as a contributor for code (#3964) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(lang): Translations update from Hosted Weblate (#3835) * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Co-authored-by: Димитър Мазнеков (Topper) Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/bg/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Michael Michael Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/uk/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1241 of 1241 strings) Co-authored-by: Hosted Weblate Co-authored-by: dtalens Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Hungarian) Currently translated at 99.2% (1231 of 1240 strings) Co-authored-by: Dargo Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Polish) Currently translated at 98.8% (1227 of 1241 strings) Co-authored-by: Hosted Weblate Co-authored-by: senza Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pl/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1241 of 1241 strings) Co-authored-by: Hosted Weblate Co-authored-by: Robin Van de Vyvere Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1241 of 1241 strings) Co-authored-by: Frostar Co-authored-by: Hosted Weblate Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (French) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Nackophilz Co-authored-by: TayZ3r Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Per Erik Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Finnish) Currently translated at 2.9% (36 of 1240 strings) Co-authored-by: Oskari Lavinto Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fi/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Albanian) Currently translated at 95.8% (1189 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: W L Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sq/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Hyun Lee Co-authored-by: cutiekeek Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ko/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Portuguese (Portugal)) Currently translated at 98.4% (1221 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Rafael Souto Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Chinese (Traditional Han script)) Currently translated at 99.9% (1239 of 1240 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Chinese (Traditional Han script)) Currently translated at 98.2% (1219 of 1241 strings) Co-authored-by: Hosted Weblate Co-authored-by: Marc Lerno Co-authored-by: dtalens Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Norwegian Bokmål) Currently translated at 89.9% (1115 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: exentler Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Michael Michael Co-authored-by: dtalens Co-authored-by: Dargo Co-authored-by: senza Co-authored-by: Robin Van de Vyvere Co-authored-by: Frostar Co-authored-by: gallegonovato Co-authored-by: Nackophilz Co-authored-by: TayZ3r Co-authored-by: Per Erik Co-authored-by: Oskari Lavinto Co-authored-by: W L Co-authored-by: Hyun Lee Co-authored-by: cutiekeek Co-authored-by: Rafael Souto Co-authored-by: Marc Lerno Co-authored-by: exentler * feat(ui): prevent password manager interference & improve service links (#3989) * docs: add s0up4200 as a contributor for code (#4047) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix(ui): update Plex Logo (#3955) * docs: add JackW6809 as a contributor for code (#4048) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: requests/issues menu count (#3470) * feat: request and issue count added to sidebar/mobile menu * fix: added permission check for count visibility * refactor: modified badge design for count * fix: properly update issue and request counts in certain scenarios (#4051) * fix: center count badge on sidebar and mobile menu (#4052) * fix: request english trailers as a fallback when using other languages (#4009) Co-authored-by: Stancu Florin * docs: add StancuFlorin as a contributor for code (#4053) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: added the PWA badge indicator for requests pending (#3411) refactor: removed unnecessary code when sending web push notification fix: moved all notify user logic into webpush refactor: n refactor: remove all unnecessary prettier changes fix: n fix: n fix: n fix: n fix: increment sw version fix: n --------- Co-authored-by: Isaac M Co-authored-by: Joseph Risk Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Loetwiek <79059734+Loetwiek@users.noreply.github.com> Co-authored-by: Loetwiek Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Co-authored-by: Fuochi Co-authored-by: Weblate (bot) Co-authored-by: Cleiton Carvalho Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Co-authored-by: Anders Ecklon Co-authored-by: Kenneth Hansen Co-authored-by: BeardedWatermelon Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: sct Co-authored-by: Michael Michael Co-authored-by: Сергій Co-authored-by: dtalens Co-authored-by: Karel Krýda Co-authored-by: Smexhy Co-authored-by: Bruno Ševčenko Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Co-authored-by: Levente Szajkó Co-authored-by: osh Co-authored-by: Eryk Michalak Co-authored-by: Francesco Co-authored-by: Fhd-pro Co-authored-by: Kobe Co-authored-by: gallegonovato Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Eero Konttaniemi Co-authored-by: Milan Smudja Co-authored-by: Developer J Co-authored-by: Haohao Zhang Co-authored-by: lkw123 Co-authored-by: Jordan Jones Co-authored-by: Brandon Cohen Co-authored-by: David Emrich Co-authored-by: Max T. Kristiansen Co-authored-by: Damien Fajole <60252259+DamsDev1@users.noreply.github.com> Co-authored-by: Ahmed Siddiqui <36286128+AhmedNSidd@users.noreply.github.com> Co-authored-by: Dargo Co-authored-by: senza Co-authored-by: Robin Van de Vyvere Co-authored-by: Frostar Co-authored-by: Nackophilz Co-authored-by: TayZ3r Co-authored-by: Oskari Lavinto Co-authored-by: W L Co-authored-by: Hyun Lee Co-authored-by: cutiekeek Co-authored-by: Rafael Souto Co-authored-by: Marc Lerno Co-authored-by: exentler Co-authored-by: soup Co-authored-by: JackOXI <53652452+JackW6809@users.noreply.github.com> Co-authored-by: Stancu Florin Co-authored-by: Stancu Florin Co-authored-by: Brandon Cohen --- .all-contributorsrc | 84 ++++++++++- README.md | 4 +- public/sw.js | 21 ++- server/api/themoviedb/index.ts | 2 + server/lib/notifications/agents/agent.ts | 2 + server/lib/notifications/agents/webpush.ts | 133 +++++++++++++----- src/assets/services/plex.svg | 124 ++++++---------- .../Common/SensitiveInput/index.tsx | 4 + src/components/IssueDetails/index.tsx | 4 +- .../IssueModal/CreateIssueModal/index.tsx | 4 +- src/components/Layout/MobileMenu/index.tsx | 73 +++++++++- src/components/Layout/Sidebar/index.tsx | 73 ++++++++-- src/components/Layout/index.tsx | 30 +++- src/components/Login/LocalLogin.tsx | 3 + src/components/RequestBlock/index.tsx | 3 + src/components/RequestButton/index.tsx | 3 + src/components/RequestCard/index.tsx | 3 + .../RequestList/RequestItem/index.tsx | 5 +- .../RequestModal/CollectionRequestModal.tsx | 14 +- .../RequestModal/MovieRequestModal.tsx | 14 +- .../RequestModal/TvRequestModal.tsx | 3 + .../Notifications/NotificationsDiscord.tsx | 4 + .../Notifications/NotificationsEmail.tsx | 40 ++++-- .../Notifications/NotificationsTelegram.tsx | 22 ++- src/components/Settings/RadarrModal/index.tsx | 5 +- src/components/Settings/SettingsPlex.tsx | 13 +- src/components/Settings/SettingsServices.tsx | 11 +- src/components/Settings/SonarrModal/index.tsx | 5 +- src/components/UserList/index.tsx | 4 + src/pages/_app.tsx | 30 ++++ 30 files changed, 580 insertions(+), 160 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 610aebc9..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" ] }, { @@ -620,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/README.md b/README.md index 4cc83f49..8bb9c7c9 100644 --- a/README.md +++ b/README.md @@ -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/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/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/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/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/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/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', 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/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index b63a24dd..7c7494de 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -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); 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 f909d617..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 () => { diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 3d64941e..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 () => { diff --git a/src/components/RequestModal/CollectionRequestModal.tsx b/src/components/RequestModal/CollectionRequestModal.tsx index 4dd79524..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( [ 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/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 7480578d..0ef1afd1 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -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( @@ -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/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/UserList/index.tsx b/src/components/UserList/index.tsx index bc7c4441..ae76c6ad 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -399,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/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 {