diff --git a/.github/workflows/private_registery_push.yml b/.github/workflows/private_registery_push.yml new file mode 100644 index 00000000..376223c7 --- /dev/null +++ b/.github/workflows/private_registery_push.yml @@ -0,0 +1,39 @@ +name: 'create docker image on pull request and push to private registery' + +on: + pull_request: + branches: + - develop + workflow_dispatch: + +jobs: + build-image: + runs-on: self-hosted + steps: + - + name: Checkout + uses: actions/checkout@v2 + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - + name: Login to private registery + uses: docker/login-action@v2.0.0 + with: + registry: ${{ secrets.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} + push: true + tags: '${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:${{ github.sha }}' + cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache' + cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache,mode=max' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e36f8100..2ae294d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch jobs: semantic-release: name: Tag and release latest version - runs-on: ubuntu-20.04 + runs-on: self-hosted env: HUSKY: 0 steps: @@ -39,7 +39,7 @@ jobs: name: Send Discord Notification needs: semantic-release if: always() - runs-on: ubuntu-20.04 + runs-on: self-hosted steps: - name: Get Build Job Status uses: technote-space/workflow-conclusion-action@v2 diff --git a/.gitignore b/.gitignore index 7d606105..41a0481f 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ config/db/db.sqlite3-journal # VS Code .vscode/launch.json + +# Webstorm +.idea diff --git a/README.md b/README.md index f1c0a5cc..c195050a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,18 @@ With more features on the way! Check out our [issue tracker](https://github.com/ Check out our dockerhub for instructions on how to install and run Jellyseerr: https://hub.docker.com/r/fallenbagel/jellyseerr +### Launching Jellyseerr manually: + +```bash +yarn install +yarn run build +yarn start +``` + +### Packages: + +Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr) + ## Preview diff --git a/docs/extending-overseerr/fail2ban.md b/docs/extending-overseerr/fail2ban.md index fbfe6fa8..1cf9131f 100644 --- a/docs/extending-overseerr/fail2ban.md +++ b/docs/extending-overseerr/fail2ban.md @@ -8,7 +8,7 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you ``` [Definition] -failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":"" +failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"" ``` You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail. diff --git a/next.config.js b/next.config.js index f0a623d4..40d899f7 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,10 @@ module.exports = { env: { commitTag: process.env.COMMIT_TAG || 'local', }, + publicRuntimeConfig: { + // Will be available on both server and client + JELLYFIN_TYPE: process.env.JELLYFIN_TYPE, + }, images: { domains: ['image.tmdb.org'], }, diff --git a/overseerr-api.yml b/overseerr-api.yml index cd7034b0..551f7dd9 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -5815,6 +5815,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Issue' + + /issue/count: + get: + summary: Gets issue counts + description: | + Returns the number of open and closed issues, as well as the number of issues of each type. + tags: + - issue + responses: + '200': + description: Issue counts returned + content: + application/json: + schema: + type: object + properties: + total: + type: number + video: + type: number + audio: + type: number + subtitles: + type: number + others: + type: number + open: + type: number + closed: + type: number /issue/{issueId}: get: summary: Get issue diff --git a/package.json b/package.json index ab4b0eaf..3411e128 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "country-flag-icons": "^1.4.21", "csurf": "^1.11.0", "email-templates": "^8.0.10", + "email-validator": "^2.0.4", "express": "^4.17.3", "express-openapi-validator": "^4.13.6", "express-rate-limit": "^6.3.0", @@ -84,6 +85,7 @@ "@babel/cli": "^7.17.6", "@commitlint/cli": "^16.2.1", "@commitlint/config-conventional": "^16.2.1", + "@next/eslint-plugin-next": "^12.1.6", "@semantic-release/changelog": "^6.0.1", "@semantic-release/commit-analyzer": "^9.0.2", "@semantic-release/exec": "^6.0.3", diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index f6f6cb9d..5dd258c4 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -31,6 +31,7 @@ export interface JellyfinLibraryItem { Id: string; HasSubtitles: boolean; Type: 'Movie' | 'Episode' | 'Season' | 'Series'; + LocationType: 'FileSystem' | 'Offline' | 'Remote' | 'Virtual'; SeriesName?: string; SeriesId?: string; SeasonId?: string; @@ -205,7 +206,9 @@ class JellyfinAPI { `/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}` ); - return contents.data.Items; + return contents.data.Items.filter( + (item: JellyfinLibraryItem) => item.LocationType !== 'Virtual' + ); } catch (e) { logger.error( `Something went wrong while getting library content from the Jellyfin server: ${e.message}`, @@ -251,7 +254,9 @@ class JellyfinAPI { try { const contents = await this.axios.get(`/Shows/${seriesID}/Seasons`); - return contents.data.Items; + return contents.data.Items.filter( + (item: JellyfinLibraryItem) => item.LocationType !== 'Virtual' + ); } catch (e) { logger.error( `Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`, @@ -270,7 +275,9 @@ class JellyfinAPI { `/Shows/${seriesID}/Episodes?seasonId=${seasonID}` ); - return contents.data.Items; + return contents.data.Items.filter( + (item: JellyfinLibraryItem) => item.LocationType !== 'Virtual' + ); } catch (e) { logger.error( `Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`, diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index cf5e280c..b5060c03 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -129,7 +129,13 @@ class TheMovieDb extends ExternalAPI { }: SingleSearchOptions): Promise => { try { const data = await this.get('/search/movie', { - params: { query, page, include_adult: includeAdult, language, year }, + params: { + query, + page, + include_adult: includeAdult, + language, + primary_release_year: year, + }, }); return data; diff --git a/server/entity/User.ts b/server/entity/User.ts index 157e7f24..7fa6dc67 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -137,6 +137,8 @@ export class User { @UpdateDateColumn() public updatedAt: Date; + public warnings: string[] = []; + constructor(init?: Partial) { Object.assign(this, init); } diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index a3e132d6..d0a0ff9f 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -2,6 +2,7 @@ import { NotificationAgentKey } from '../../lib/settings'; export interface UserSettingsGeneralResponse { username?: string; + email?: string; discordId?: string; locale?: string; region?: string; diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index a1dd7e4e..cbed472f 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -13,6 +13,7 @@ import { NotificationAgentKey, } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import * as EmailValidator from 'email-validator'; class EmailAgent extends BaseAgent @@ -215,14 +216,23 @@ class EmailAgent this.getSettings(), payload.notifyUser.settings?.pgpKey ); - await email.send( - this.buildMessage( - type, - payload, - payload.notifyUser.email, - payload.notifyUser.displayName - ) - ); + if (EmailValidator.validate(payload.notifyUser.email)) { + await email.send( + this.buildMessage( + type, + payload, + payload.notifyUser.email, + payload.notifyUser.displayName + ) + ); + } else { + logger.warn('Invalid email address provided for user', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + }); + } } catch (e) { logger.error('Error sending email notification', { label: 'Notifications', @@ -268,9 +278,18 @@ class EmailAgent this.getSettings(), user.settings?.pgpKey ); - await email.send( - this.buildMessage(type, payload, user.email, user.displayName) - ); + if (EmailValidator.validate(user.email)) { + await email.send( + this.buildMessage(type, payload, user.email, user.displayName) + ); + } else { + logger.warn('Invalid email address provided for user', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + }); + } } catch (e) { logger.error('Error sending email notification', { label: 'Notifications', diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 6b167d7c..53fe864c 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -134,6 +134,7 @@ interface FullPublicSettings extends PublicSettings { enablePushRegistration: boolean; locale: string; emailEnabled: boolean; + userEmailRequired: boolean; newPlexLogin: boolean; } @@ -159,6 +160,7 @@ export interface NotificationAgentSlack extends NotificationAgentConfig { export interface NotificationAgentEmail extends NotificationAgentConfig { options: { + userEmailRequired: boolean; emailFrom: string; smtpHost: string; smtpPort: number; @@ -335,6 +337,7 @@ class Settings { email: { enabled: false, options: { + userEmailRequired: false, emailFrom: '', smtpHost: '', smtpPort: 587, @@ -342,7 +345,7 @@ class Settings { ignoreTls: false, requireTls: false, allowSelfSigned: false, - senderName: 'Overseerr', + senderName: 'Jellyseerr', }, }, discord: { @@ -529,6 +532,8 @@ class Settings { enablePushRegistration: this.data.notifications.agents.webpush.enabled, locale: this.data.main.locale, emailEnabled: this.data.notifications.agents.email.enabled, + userEmailRequired: + this.data.notifications.agents.email.options.userEmailRequired, newPlexLogin: this.data.main.newPlexLogin, }; } diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 1aacb69b..ba8926a3 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -9,6 +9,7 @@ import { Permission } from '../lib/permissions'; import { getSettings } from '../lib/settings'; import logger from '../logger'; import { isAuthenticated } from '../middleware/auth'; +import * as EmailValidator from 'email-validator'; const authRoutes = Router(); @@ -24,6 +25,16 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => { where: { id: req.user.id }, }); + // check if email is required in settings and if user has an valid email + const settings = await getSettings(); + if ( + settings.notifications.agents.email.options.userEmailRequired && + !EmailValidator.validate(user.email) + ) { + user.warnings.push('userEmailRequired'); + logger.warn(`User ${user.username} has no valid email address`); + } + return res.status(200).json(user); }); @@ -70,6 +81,9 @@ authRoutes.post('/plex', async (req, res, next) => { userType: UserType.PLEX, }); + settings.main.mediaServerType = MediaServerType.PLEX; + settings.save(); + await userRepository.save(user); } else { const mainUser = await userRepository.findOneOrFail({ @@ -196,10 +210,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => { settings.jellyfin.hostname !== '' ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); - } else if (!body.username || !body.password) { - return res - .status(500) - .json({ error: 'You must provide an username and a password' }); + } else if (!body.username) { + return res.status(500).json({ error: 'You must provide an username' }); } else if (settings.jellyfin.hostname !== '' && body.hostname) { return res .status(500) @@ -213,6 +225,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { settings.jellyfin.hostname !== '' ? settings.jellyfin.hostname : body.hostname; + const { externalHostname } = getSettings().jellyfin; // Try to find deviceId that corresponds to jellyfin user, else generate a new one let user = await userRepository.findOne({ @@ -229,6 +242,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => { } // First we need to attempt to log the user in to jellyfin const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); + const jellyfinHost = + externalHostname && externalHostname.length > 0 + ? externalHostname + : hostname; const account = await jellyfinserver.login(body.username, body.password); // Next let's see if the user already exists @@ -244,7 +261,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { // Update the users avatar with their jellyfin profile pic (incase it changed) if (account.User.PrimaryImageTag) { - user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; + user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; } else { user.avatar = '/os_logo_square.png'; } @@ -290,7 +307,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { jellyfinAuthToken: account.AccessToken, permissions: Permission.ADMIN, avatar: account.User.PrimaryImageTag - ? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` : '/os_logo_square.png', userType: UserType.JELLYFIN, }); @@ -319,7 +336,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { jellyfinAuthToken: account.AccessToken, permissions: settings.main.defaultPermissions, avatar: account.User.PrimaryImageTag - ? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` : '/os_logo_square.png', userType: UserType.JELLYFIN, }); @@ -327,7 +344,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { const passedExplicitPassword = body.password && body.password.length > 0; if (passedExplicitPassword) { - await user.setPassword(body.password); + await user.setPassword(body.password ?? ''); } await userRepository.save(user); } diff --git a/server/routes/issue.ts b/server/routes/issue.ts index c7db5232..07cf3277 100644 --- a/server/routes/issue.ts +++ b/server/routes/issue.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import { getRepository } from 'typeorm'; -import { IssueStatus } from '../constants/issue'; +import { IssueStatus, IssueType } from '../constants/issue'; import Issue from '../entity/Issue'; import IssueComment from '../entity/IssueComment'; import Media from '../entity/Media'; @@ -146,6 +146,68 @@ issueRoutes.post< } ); +issueRoutes.get('/count', async (req, res, next) => { + const issueRepository = getRepository(Issue); + + try { + const query = issueRepository.createQueryBuilder('issue'); + + const totalCount = await query.getCount(); + + const videoCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.VIDEO, + }) + .getCount(); + + const audioCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.AUDIO, + }) + .getCount(); + + const subtitlesCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.SUBTITLES, + }) + .getCount(); + + const othersCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.OTHER, + }) + .getCount(); + + const openCount = await query + .where('issue.status = :issueStatus', { + issueStatus: IssueStatus.OPEN, + }) + .getCount(); + + const closedCount = await query + .where('issue.status = :issueStatus', { + issueStatus: IssueStatus.RESOLVED, + }) + .getCount(); + + return res.status(200).json({ + total: totalCount, + video: videoCount, + audio: audioCount, + subtitles: subtitlesCount, + others: othersCount, + open: openCount, + closed: closedCount, + }); + } catch (e) { + logger.debug('Something went wrong retrieving issue counts.', { + label: 'API', + errorMessage: e.message, + }); + next({ status: 500, message: 'Unable to retrieve issue counts.' }); + } +}); + issueRoutes.get<{ issueId: string }>( '/:issueId', isAuthenticated( diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 8a5ef9a2..7ebff760 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -303,6 +303,11 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => { settingsRoutes.get('/jellyfin/users', async (req, res) => { const settings = getSettings(); + const { hostname, externalHostname } = getSettings().jellyfin; + const jellyfinHost = + externalHostname && externalHostname.length > 0 + ? externalHostname + : hostname; const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ @@ -321,7 +326,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => { username: user.Name, id: user.Id, thumb: user.PrimaryImageTag - ? `${settings.jellyfin.hostname}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90` + ? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90` : '/os_logo_square.png', email: user.Name, })); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 308cb14a..5811fc05 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -492,56 +492,46 @@ router.post( ); jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); - const jellyfinUsersResponse = await jellyfinClient.getUsers(); + //const jellyfinUsersResponse = await jellyfinClient.getUsers(); const createdUsers: User[] = []; - for (const account of jellyfinUsersResponse.users) { - if (account.Name) { - const user = await userRepository - .createQueryBuilder('user') - .where('user.jellyfinUserId = :id', { id: account.Id }) - .orWhere('user.email = :email', { - email: account.Name, - }) - .getOne(); + const { hostname, externalHostname } = getSettings().jellyfin; + const jellyfinHost = + externalHostname && externalHostname.length > 0 + ? externalHostname + : hostname; - const avatar = account.PrimaryImageTag - ? `${settings.jellyfin.hostname}/Users/${account.Id}/Images/Primary/?tag=${account.PrimaryImageTag}&quality=90` - : '/os_logo_square.png'; + jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); + const jellyfinUsers = await jellyfinClient.getUsers(); - if (user) { - // Update the user's avatar with their Jellyfin thumbnail, in case it changed - user.avatar = avatar; - user.email = account.Name; - user.jellyfinUsername = account.Name; + for (const jellyfinUserId of body.jellyfinUserIds) { + const jellyfinUser = jellyfinUsers.users.find( + (user) => user.Id === jellyfinUserId + ); - // In case the user was previously a local account - if (user.userType === UserType.LOCAL) { - user.userType = UserType.JELLYFIN; - user.jellyfinUserId = account.Id; - } - await userRepository.save(user); - } else if (!body || body.jellyfinUserIds.includes(account.Id)) { - // logger.error('CREATED USER', { - // label: 'API', - // }); + const user = await userRepository.findOne({ + select: ['id', 'jellyfinUserId'], + where: { jellyfinUserId: jellyfinUserId }, + }); - const newUser = new User({ - jellyfinUsername: account.Name, - jellyfinUserId: account.Id, - jellyfinDeviceId: Buffer.from( - `BOT_overseerr_${account.Name ?? ''}` - ).toString('base64'), - email: account.Name, - permissions: settings.main.defaultPermissions, - avatar, - userType: UserType.JELLYFIN, - }); - await userRepository.save(newUser); - createdUsers.push(newUser); - } + if (!user) { + const newUser = new User({ + jellyfinUsername: jellyfinUser?.Name, + jellyfinUserId: jellyfinUser?.Id, + jellyfinDeviceId: Buffer.from( + `BOT_jellyseerr_${jellyfinUser?.Name ?? ''}` + ).toString('base64'), + email: jellyfinUser?.Name, + permissions: settings.main.defaultPermissions, + avatar: jellyfinUser?.PrimaryImageTag + ? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90` + : '/os_logo_square.png', + userType: UserType.JELLYFIN, + }); + + await userRepository.save(newUser); + createdUsers.push(newUser); } } - return res.status(201).json(User.filterMany(createdUsers)); } catch (e) { next({ status: 500, message: e.message }); diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 0c53c94a..a05311a2 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -51,6 +51,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( return res.status(200).json({ username: user.username, + email: user.email, discordId: user.settings?.discordId, locale: user.settings?.locale, region: user.settings?.region, @@ -120,6 +121,7 @@ userSettingsRoutes.post< user.settings.locale = req.body.locale; user.settings.region = req.body.region; user.settings.originalLanguage = req.body.originalLanguage; + user.email = req.body.email ?? user.email; } await userRepository.save(user); @@ -130,6 +132,7 @@ userSettingsRoutes.post< locale: user.settings.locale, region: user.settings.region, originalLanguage: user.settings.originalLanguage, + email: user.email, }); } catch (e) { next({ status: 500, message: e.message }); diff --git a/src/components/Discover/NetworkSlider/index.tsx b/src/components/Discover/NetworkSlider/index.tsx index 44c776cf..61468a6f 100644 --- a/src/components/Discover/NetworkSlider/index.tsx +++ b/src/components/Discover/NetworkSlider/index.tsx @@ -110,6 +110,12 @@ const networks: Network[] = [ 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/nm8d7P7MJNiBLdgIzUK0gkuEA4r.png', url: '/discover/tv/network/16', }, + { + name: 'Paramount+', + image: + 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/fi83B1oztoS47xxcemFdPMhIzK.png', + url: '/discover/tv/network/4330', + }, { name: 'BBC One', image: diff --git a/src/components/IssueDetails/IssueComment/index.tsx b/src/components/IssueDetails/IssueComment/index.tsx index 99b26a44..11623f53 100644 --- a/src/components/IssueDetails/IssueComment/index.tsx +++ b/src/components/IssueDetails/IssueComment/index.tsx @@ -90,7 +90,7 @@ const IssueComment: React.FC = ({ diff --git a/src/components/IssueDetails/index.tsx b/src/components/IssueDetails/index.tsx index 90cfb494..3d670caf 100644 --- a/src/components/IssueDetails/index.tsx +++ b/src/components/IssueDetails/index.tsx @@ -35,6 +35,7 @@ import IssueComment from './IssueComment'; import IssueDescription from './IssueDescription'; import { MediaServerType } from '../../../server/constants/server'; import useSettings from '../../hooks/useSettings'; +import getConfig from 'next/config'; const messages = defineMessages({ openedby: '#{issueId} opened {relativeTime} by {username}', @@ -99,6 +100,7 @@ const IssueDetails: React.FC = () => { (opt) => opt.issueType === issueData?.issueType ); const settings = useSettings(); + const { publicRuntimeConfig } = getConfig(); if (!data && !error) { return ; @@ -267,7 +269,7 @@ const IssueDetails: React.FC = () => { > @@ -366,13 +368,18 @@ const IssueDetails: React.FC = () => { > - {intl.formatMessage(messages.playonplex, { - mediaServerName: - settings.currentSettings.mediaServerType === + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.playonplex, { + mediaServerName: 'Emby', + }) + : settings.currentSettings.mediaServerType === MediaServerType.PLEX - ? 'Plex' - : 'Jellyfin', - })} + ? intl.formatMessage(messages.playonplex, { + mediaServerName: 'Plex', + }) + : intl.formatMessage(messages.playonplex, { + mediaServerName: 'Jellyfin', + })} )} @@ -407,13 +414,18 @@ const IssueDetails: React.FC = () => { > - {intl.formatMessage(messages.play4konplex, { - mediaServerName: - settings.currentSettings.mediaServerType === + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.play4konplex, { + mediaServerName: 'Emby', + }) + : settings.currentSettings.mediaServerType === MediaServerType.PLEX - ? 'Plex' - : 'Jellyfin', - })} + ? intl.formatMessage(messages.play4konplex, { + mediaServerName: 'Plex', + }) + : intl.formatMessage(messages.play4konplex, { + mediaServerName: 'Jellyfin', + })} )} @@ -618,13 +630,18 @@ const IssueDetails: React.FC = () => { > - {intl.formatMessage(messages.playonplex, { - mediaServerName: - settings.currentSettings.mediaServerType === + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.playonplex, { + mediaServerName: 'Emby', + }) + : settings.currentSettings.mediaServerType === MediaServerType.PLEX - ? 'Plex' - : 'Jellyfin', - })} + ? intl.formatMessage(messages.playonplex, { + mediaServerName: 'Plex', + }) + : intl.formatMessage(messages.playonplex, { + mediaServerName: 'Jellyfin', + })} )} @@ -659,13 +676,18 @@ const IssueDetails: React.FC = () => { > - {intl.formatMessage(messages.play4konplex, { - mediaServerName: - settings.currentSettings.mediaServerType === + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.play4konplex, { + mediaServerName: 'Emby', + }) + : settings.currentSettings.mediaServerType === MediaServerType.PLEX - ? 'Plex' - : 'Jellyfin', - })} + ? intl.formatMessage(messages.play4konplex, { + mediaServerName: 'Plex', + }) + : intl.formatMessage(messages.play4konplex, { + mediaServerName: 'Jellyfin', + })} )} diff --git a/src/components/IssueList/IssueItem/index.tsx b/src/components/IssueList/IssueItem/index.tsx index 1695215a..e5cd058a 100644 --- a/src/components/IssueList/IssueItem/index.tsx +++ b/src/components/IssueList/IssueItem/index.tsx @@ -228,7 +228,7 @@ const IssueItem: React.FC = ({ issue }) => { {issue.createdBy.displayName} diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index 45716eeb..821a3d2d 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -14,6 +14,7 @@ import useClickOutside from '../../../hooks/useClickOutside'; import { Permission, useUser } from '../../../hooks/useUser'; import Transition from '../../Transition'; import VersionStatus from '../VersionStatus'; +import UserWarnings from '../UserWarnings'; const messages = defineMessages({ dashboard: 'Discover', @@ -177,6 +178,10 @@ const Sidebar: React.FC = ({ open, setClosed }) => { ); })} +
+ setClosed()} /> +
+ {hasPermission(Permission.ADMIN) && (
setClosed()} /> @@ -236,6 +241,9 @@ const Sidebar: React.FC = ({ open, setClosed }) => { ); })} +
+ +
{hasPermission(Permission.ADMIN) && (
diff --git a/src/components/Layout/UserDropdown/index.tsx b/src/components/Layout/UserDropdown/index.tsx index a9923117..e51fcabf 100644 --- a/src/components/Layout/UserDropdown/index.tsx +++ b/src/components/Layout/UserDropdown/index.tsx @@ -40,7 +40,7 @@ const UserDropdown: React.FC = () => { onClick={() => setDropdownOpen(true)} > diff --git a/src/components/Layout/UserWarnings/index.tsx b/src/components/Layout/UserWarnings/index.tsx new file mode 100644 index 00000000..fe621d2a --- /dev/null +++ b/src/components/Layout/UserWarnings/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import Link from 'next/link'; +import { ExclamationIcon } from '@heroicons/react/outline'; +import { defineMessages, useIntl } from 'react-intl'; +import { useUser } from '../../../hooks/useUser'; + +const messages = defineMessages({ + emailRequired: 'An email address is required.', + emailInvalid: 'Email address is invalid.', + passwordRequired: 'A password is required.', +}); + +interface UserWarningsProps { + onClick?: () => void; +} + +const UserWarnings: React.FC = ({ onClick }) => { + const intl = useIntl(); + const { user } = useUser(); + if (!user) { + return null; + } + + let res = null; + + //check if a user has warnings + if (user.warnings.length > 0) { + user.warnings.forEach((warning) => { + let link = ''; + let warningText = ''; + let warningTitle = ''; + switch (warning) { + case 'userEmailRequired': + link = '/profile/settings/'; + warningTitle = 'Profile is incomplete'; + warningText = intl.formatMessage(messages.emailRequired); + } + + res = ( + + { + if (e.key === 'Enter' && onClick) { + onClick(); + } + }} + role="button" + tabIndex={0} + className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400" + > + +
+ {warningTitle} + {warningText} +
+
+ + ); + }); + } + + return res; +}; + +export default UserWarnings; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index bde59277..b560c66e 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -50,6 +50,7 @@ const Layout: React.FC = ({ children }) => {
+ setSidebarOpen(false)} />
diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index 540fb6fd..e76a1650 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -1,19 +1,19 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import Button from '../Common/Button'; - -import { Field, Form, Formik } from 'formik'; -import * as Yup from 'yup'; -import axios from 'axios'; import { useToasts } from 'react-toast-notifications'; +import * as Yup from 'yup'; import useSettings from '../../hooks/useSettings'; +import Button from '../Common/Button'; +import getConfig from 'next/config'; const messages = defineMessages({ username: 'Username', password: 'Password', - host: 'Jellyfin URL', + host: '{mediaServerName} URL', email: 'Email', - validationhostrequired: 'Jellyfin URL required', + validationhostrequired: '{mediaServerName} URL required', validationhostformat: 'Valid URL required', validationemailrequired: 'Email required', validationemailformat: 'Valid email required', @@ -40,6 +40,7 @@ const JellyfinLogin: React.FC = ({ const toasts = useToasts(); const intl = useIntl(); const settings = useSettings(); + const { publicRuntimeConfig } = getConfig(); if (initial) { const LoginSchema = Yup.object().shape({ @@ -48,16 +49,19 @@ const JellyfinLogin: React.FC = ({ /^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/, intl.formatMessage(messages.validationhostformat) ) - .required(intl.formatMessage(messages.validationhostrequired)), + .required( + intl.formatMessage(messages.validationhostrequired, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + }) + ), email: Yup.string() .email(intl.formatMessage(messages.validationemailformat)) .required(intl.formatMessage(messages.validationemailrequired)), username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), - password: Yup.string().required( - intl.formatMessage(messages.validationpasswordrequired) - ), + password: Yup.string(), }); return ( = ({
@@ -105,7 +114,12 @@ const JellyfinLogin: React.FC = ({ id="host" name="host" type="text" - placeholder={intl.formatMessage(messages.host)} + placeholder={intl.formatMessage(messages.host, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + })} />
{errors.host && touched.host && ( @@ -185,9 +199,7 @@ const JellyfinLogin: React.FC = ({ username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), - password: Yup.string().required( - intl.formatMessage(messages.validationpasswordrequired) - ), + password: Yup.string(), }); return (
@@ -266,8 +278,11 @@ const JellyfinLogin: React.FC = ({ as="a" buttonType="ghost" href={ - settings.currentSettings.jellyfinHost + - '/web/#!/forgotpassword.html' + process.env.JELLYFIN_TYPE == 'emby' + ? settings.currentSettings.jellyfinHost + + '/web/index.html#!/startup/forgotpassword.html' + : settings.currentSettings.jellyfinHost + + '/web/index.html#!/forgotpassword.html' } > {intl.formatMessage(messages.forgotpassword)} diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 5d3e7a3d..eb8f368b 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -15,12 +15,13 @@ import PlexLoginButton from '../PlexLoginButton'; import Transition from '../Transition'; import JellyfinLogin from './JellyfinLogin'; import LocalLogin from './LocalLogin'; +import getConfig from 'next/config'; const messages = defineMessages({ signin: 'Sign In', signinheader: 'Sign in to continue', signinwithplex: 'Use your Plex account', - signinwithjellyfin: 'Use your Jellyfin account', + signinwithjellyfin: 'Use your {mediaServerName} account', signinwithoverseerr: 'Use your {applicationTitle} account', }); @@ -32,6 +33,7 @@ const Login: React.FC = () => { const { user, revalidate } = useUser(); const router = useRouter(); const settings = useSettings(); + const { publicRuntimeConfig } = getConfig(); // Effect that is triggered when the `authToken` comes back from the Plex OAuth // We take the token and attempt to sign in. If we get a success message, we will @@ -133,7 +135,12 @@ const Login: React.FC = () => { {settings.currentSettings.mediaServerType == MediaServerType.PLEX ? intl.formatMessage(messages.signinwithplex) - : intl.formatMessage(messages.signinwithjellyfin)} + : intl.formatMessage(messages.signinwithjellyfin, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + })}
diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index 8c6f2791..a1e9bab1 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -210,7 +210,7 @@ const ManageSlideOver: React.FC< {hasPermission(Permission.ADMIN) && (data.mediaInfo?.serviceUrl || data.mediaInfo?.tautulliUrl || - watchData?.data?.playCount) && ( + !!watchData?.data?.playCount) && (

{intl.formatMessage(messages.manageModalMedia)} @@ -272,7 +272,7 @@ const ManageSlideOver: React.FC< {user.displayName} @@ -325,7 +325,7 @@ const ManageSlideOver: React.FC< {hasPermission(Permission.ADMIN) && (data.mediaInfo?.serviceUrl4k || data.mediaInfo?.tautulliUrl4k || - watchData?.data4k?.playCount) && ( + !!watchData?.data4k?.playCount) && (

{intl.formatMessage(messages.manageModalMedia4k)} @@ -387,7 +387,7 @@ const ManageSlideOver: React.FC< {user.displayName} diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index fbbad4bd..3e33c4b8 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -48,6 +48,7 @@ import PersonCard from '../PersonCard'; import RequestButton from '../RequestButton'; import Slider from '../Slider'; import StatusBadge from '../StatusBadge'; +import getConfig from 'next/config'; const messages = defineMessages({ originaltitle: 'Original Title', @@ -95,6 +96,7 @@ const MovieDetails: React.FC = ({ movie }) => { const minStudios = 3; const [showMoreStudios, setShowMoreStudios] = useState(false); const [showIssueModal, setShowIssueModal] = useState(false); + const { publicRuntimeConfig } = getConfig(); const { data, @@ -130,10 +132,7 @@ const MovieDetails: React.FC = ({ movie }) => { if (data.mediaInfo?.mediaUrl) { mediaLinks.push({ - text: - settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN - ? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' }) - : intl.formatMessage(messages.play, { mediaServerName: 'Plex' }), + text: getAvalaibleMediaServerName(), url: data.mediaInfo?.mediaUrl, svg: , }); @@ -146,10 +145,7 @@ const MovieDetails: React.FC = ({ movie }) => { }) ) { mediaLinks.push({ - text: - settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN - ? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' }) - : intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }), + text: getAvalaible4kMediaServerName(), url: data.mediaInfo?.mediaUrl4k, svg: , }); @@ -228,6 +224,30 @@ const MovieDetails: React.FC = ({ movie }) => { data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) ?.flatrate ?? []; + function getAvalaibleMediaServerName() { + if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { + return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); + } + + if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { + return intl.formatMessage(messages.play, { mediaServerName: 'Plex' }); + } + + return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' }); + } + + function getAvalaible4kMediaServerName() { + if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { + return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); + } + + if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { + return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }); + } + + return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' }); + } + return (
= ({ request, onUpdate }) => { const intl = useIntl(); const [isUpdating, setIsUpdating] = useState(false); const [showEditModal, setShowEditModal] = useState(false); - const { profile, rootFolder, server } = useRequestOverride(request); + const { profile, rootFolder, server, languageProfile } = + useRequestOverride(request); const updateRequest = async (type: 'approve' | 'decline'): Promise => { setIsUpdating(true); @@ -209,7 +211,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => {

)} - {(server || profile !== null || rootFolder) && ( + {(server || profile || rootFolder || languageProfile) && ( <>
{intl.formatMessage(messages.requestoverrides)} @@ -223,12 +225,12 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { {server} )} - {profile !== null && ( + {profile && (
  • {intl.formatMessage(messages.profilechanged)} - ID {profile} + {profile}
  • )} {rootFolder && ( @@ -239,6 +241,14 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { {rootFolder} )} + {languageProfile && ( +
  • + + {intl.formatMessage(messages.languageprofile)} + + {languageProfile} +
  • + )} )} diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index cc52f155..4ac1bfe9 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -231,7 +231,7 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { {requestData.requestedBy.displayName} diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index b3c18e8f..6c98281e 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -336,7 +336,7 @@ const RequestItem: React.FC = ({ {requestData.requestedBy.displayName} @@ -390,7 +390,7 @@ const RequestItem: React.FC = ({ {requestData.modifiedBy.displayName} diff --git a/src/components/RequestModal/AdvancedRequester/index.tsx b/src/components/RequestModal/AdvancedRequester/index.tsx index d4cead4a..58bbe777 100644 --- a/src/components/RequestModal/AdvancedRequester/index.tsx +++ b/src/components/RequestModal/AdvancedRequester/index.tsx @@ -534,7 +534,7 @@ const AdvancedRequester: React.FC = ({ {selectedUser.displayName} @@ -584,7 +584,7 @@ const AdvancedRequester: React.FC = ({ {user.displayName} diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index 40308390..e20a1305 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -16,6 +16,7 @@ const messages = defineMessages({ validationSmtpHostRequired: 'You must provide a valid hostname or IP address', validationSmtpPortRequired: 'You must provide a valid port number', agentenabled: 'Enable Agent', + userEmailRequired: 'Require user email', emailsender: 'Sender Address', smtpHost: 'SMTP Host', smtpPort: 'SMTP Port', @@ -125,6 +126,7 @@ const NotificationsEmail: React.FC = () => { { await axios.post('/api/v1/settings/notifications/email', { enabled: values.enabled, options: { + userEmailRequired: values.userEmailRequired, emailFrom: values.emailFrom, smtpHost: values.smtpHost, smtpPort: Number(values.smtpPort), @@ -241,6 +244,18 @@ const NotificationsEmail: React.FC = () => {

    +
    + +
    + +
    +
    -
    - - - - https://github.com/sponsors/sct - - - {intl.formatMessage(messages.preferredmethod)} - - - - - https://patreon.com/overseerr - - - -
    diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index 9740762c..a8cd2627 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -12,30 +12,31 @@ import Badge from '../Common/Badge'; import Button from '../Common/Button'; import LoadingSpinner from '../Common/LoadingSpinner'; import LibraryItem from './LibraryItem'; +import getConfig from 'next/config'; const messages = defineMessages({ - jellyfinsettings: 'Jellyfin Settings', + jellyfinsettings: '{mediaServerName} Settings', jellyfinsettingsDescription: - 'Configure the settings for your Jellyfin server. Jellyfin scans your Jellyfin libraries to see what content is available.', + 'Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.', timeout: 'Timeout', save: 'Save Changes', saving: 'Saving…', - jellyfinlibraries: 'Jellyfin Libraries', + jellyfinlibraries: '{mediaServerName} Libraries', jellyfinlibrariesDescription: - 'The libraries Jellyfin scans for titles. Click the button below if no libraries are listed.', + 'The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.', jellyfinSettingsFailure: - 'Something went wrong while saving Jellyfin settings.', - jellyfinSettingsSuccess: 'Jellyfin settings saved successfully!', - jellyfinSettings: 'Jellyfin Settings', + 'Something went wrong while saving {mediaServerName} settings.', + jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!', + jellyfinSettings: '{mediaServerName} Settings', jellyfinSettingsDescription: - 'Optionally configure an external player endpoint for your jellyfin server that is different to the internal URL used during setup', + 'Optionally configure an external player endpoint for your {mediaServerName} server that is different to the internal URL used during setup', externalUrl: 'External URL', validationUrl: 'You must provide a valid URL', syncing: 'Syncing', syncJellyfin: 'Sync Libraries', manualscanJellyfin: 'Manual Library Scan', manualscanDescriptionJellyfin: - "Normally, this will only be run once every 24 hours. Jellyfin will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!", + "Normally, this will only be run once every 24 hours. Jellyseerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Jellyseerr, a one-time full manual library scan is recommended!", notrunning: 'Not Running', currentlibrary: 'Current Library: {name}', librariesRemaining: 'Libraries Remaining: {count}', @@ -80,6 +81,7 @@ const SettingsJellyfin: React.FC = ({ ); const intl = useIntl(); const { addToast } = useToasts(); + const { publicRuntimeConfig } = getConfig(); const JellyfinSettingsSchema = Yup.object().shape({ jellyfinExternalUrl: Yup.string().matches( @@ -161,10 +163,22 @@ const SettingsJellyfin: React.FC = ({ <>

    - + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.jellyfinlibraries, { + mediaServerName: 'Emby', + }) + : intl.formatMessage(messages.jellyfinlibraries, { + mediaServerName: 'Jellyfin', + })}

    - + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.jellyfinlibrariesDescription, { + mediaServerName: 'Emby', + }) + : intl.formatMessage(messages.jellyfinlibrariesDescription, { + mediaServerName: 'Jellyfin', + })}

    @@ -201,7 +215,13 @@ const SettingsJellyfin: React.FC = ({

    - + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.manualscanDescriptionJellyfin, { + mediaServerName: 'Emby', + }) + : intl.formatMessage(messages.manualscanDescriptionJellyfin, { + mediaServerName: 'Jellyfin', + })}

    @@ -305,10 +325,22 @@ const SettingsJellyfin: React.FC = ({ <>

    - {intl.formatMessage(messages.jellyfinSettings)} + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.jellyfinSettings, { + mediaServerName: 'Emby', + }) + : intl.formatMessage(messages.jellyfinSettings, { + mediaServerName: 'Jellyfin', + })}

    - {intl.formatMessage(messages.jellyfinSettingsDescription)} + {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? intl.formatMessage(messages.jellyfinSettingsDescription, { + mediaServerName: 'Emby', + }) + : intl.formatMessage(messages.jellyfinSettingsDescription, { + mediaServerName: 'Jellyfin', + })}

    = ({ externalHostname: values.jellyfinExternalUrl, } as JellyfinSettings); - addToast(intl.formatMessage(messages.jellyfinSettingsSuccess), { - autoDismiss: true, - appearance: 'success', - }); + addToast( + intl.formatMessage(messages.jellyfinSettingsSuccess, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + }), + { + autoDismiss: true, + appearance: 'success', + } + ); } catch (e) { - addToast(intl.formatMessage(messages.jellyfinSettingsFailure), { - autoDismiss: true, - appearance: 'error', - }); + addToast( + intl.formatMessage(messages.jellyfinSettingsFailure, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + }), + { + autoDismiss: true, + appearance: 'error', + } + ); } finally { revalidate(); } diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 35e445cb..cfe8c4bf 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -10,9 +10,11 @@ import { } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; +import { MediaServerType } from '../../../../server/constants/server'; import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces'; import { JobId } from '../../../../server/lib/settings'; import Spinner from '../../../assets/spinner.svg'; +import useSettings from '../../../hooks/useSettings'; import globalMessages from '../../../i18n/globalMessages'; import { formatBytes } from '../../../utils/numberHelpers'; import Badge from '../../Common/Badge'; @@ -102,6 +104,7 @@ const SettingsJobs: React.FC = () => { const [isSaving, setIsSaving] = useState(false); const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5); const [jobScheduleHours, setJobScheduleHours] = useState(1); + const settings = useSettings(); if (!data && !error) { return ; @@ -369,22 +372,33 @@ const SettingsJobs: React.FC = () => { - {cacheData?.map((cache) => ( - - {cache.name} - {intl.formatNumber(cache.stats.hits)} - {intl.formatNumber(cache.stats.misses)} - {intl.formatNumber(cache.stats.keys)} - {formatBytes(cache.stats.ksize)} - {formatBytes(cache.stats.vsize)} - - - - - ))} + {cacheData + ?.filter( + (cache) => + !( + settings.currentSettings.mediaServerType !== + MediaServerType.PLEX && cache.id === 'plexguid' + ) + ) + .map((cache) => ( + + {cache.name} + {intl.formatNumber(cache.stats.hits)} + {intl.formatNumber(cache.stats.misses)} + {intl.formatNumber(cache.stats.keys)} + {formatBytes(cache.stats.ksize)} + {formatBytes(cache.stats.vsize)} + + + + + ))}
    diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index f2fa9078..3de72f4a 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -1,5 +1,8 @@ +import getConfig from 'next/config'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { MediaServerType } from '../../../server/constants/server'; +import useSettings from '../../hooks/useSettings'; import globalMessages from '../../i18n/globalMessages'; import PageTitle from '../Common/PageTitle'; import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs'; @@ -8,7 +11,7 @@ const messages = defineMessages({ menuGeneralSettings: 'General', menuUsers: 'Users', menuPlexSettings: 'Plex', - menuJellyfinSettings: 'Jellyfin', + menuJellyfinSettings: '{mediaServerName}', menuServices: 'Services', menuNotifications: 'Notifications', menuLogs: 'Logs', @@ -18,7 +21,8 @@ const messages = defineMessages({ const SettingsLayout: React.FC = ({ children }) => { const intl = useIntl(); - + const { publicRuntimeConfig } = getConfig(); + const settings = useSettings(); const settingsRoutes: SettingsRoute[] = [ { text: intl.formatMessage(messages.menuGeneralSettings), @@ -30,16 +34,17 @@ const SettingsLayout: React.FC = ({ children }) => { route: '/settings/users', regex: /^\/settings\/users/, }, - { - text: intl.formatMessage(messages.menuPlexSettings), - route: '/settings/plex', - regex: /^\/settings\/plex/, - }, - { - text: intl.formatMessage(messages.menuJellyfinSettings), - route: '/settings/jellyfin', - regex: /^\/settings\/jellyfin/, - }, + settings.currentSettings.mediaServerType === MediaServerType.PLEX + ? { + text: intl.formatMessage(messages.menuPlexSettings), + route: '/settings/plex', + regex: /^\/settings\/plex/, + } + : { + text: getAvailableMediaServerName(), + route: '/settings/jellyfin', + regex: /^\/settings\/jellyfin/, + }, { text: intl.formatMessage(messages.menuServices), route: '/settings/services', @@ -76,6 +81,12 @@ const SettingsLayout: React.FC = ({ children }) => {
    {children}
    ); + function getAvailableMediaServerName() { + return intl.formatMessage(messages.menuJellyfinSettings, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin', + }); + } }; export default SettingsLayout; diff --git a/src/components/Settings/SettingsUsers/index.tsx b/src/components/Settings/SettingsUsers/index.tsx index d53a619b..89c89673 100644 --- a/src/components/Settings/SettingsUsers/index.tsx +++ b/src/components/Settings/SettingsUsers/index.tsx @@ -14,6 +14,7 @@ import LoadingSpinner from '../../Common/LoadingSpinner'; import PageTitle from '../../Common/PageTitle'; import PermissionEdit from '../../PermissionEdit'; import QuotaSelector from '../../QuotaSelector'; +import getConfig from 'next/config'; const messages = defineMessages({ users: 'Users', @@ -42,6 +43,7 @@ const SettingsUsers: React.FC = () => { mutate: revalidate, } = useSWR('/api/v1/settings/main'); const settings = useSettings(); + const { publicRuntimeConfig } = getConfig(); if (!data && !error) { return ; @@ -131,16 +133,20 @@ const SettingsUsers: React.FC = () => {
    +
    + +
    +
    + w === 'userEmailRequired') + ? 'border-2 border-red-400 focus:border-blue-600' + : '' + } + /> +
    + {errors.email && touched.email && ( +
    {errors.email}
    + )} +
    +