Compare commits

...

67 Commits

Author SHA1 Message Date
Fallenbagel
4ccc956c35 Fixed formatting 2022-06-20 23:45:55 +05:00
Fallenbagel
5af3a7e71b Add in much needed information
upcoming feature explanation and the reason for fork
2022-06-20 22:07:49 +05:00
Fallenbagel
304b82b594 docs(readme): added information about upcoming/unique features
added information about upcoming/unique features
2022-06-19 23:34:01 +05:00
Fallenbagel
8810c20fc1 fix(setup&login): fix a description error in the manual scan in setup and add emby to login page
Manual scan in setup says "Jellyfin will scan your Jellyfin's server" and same with emby, so I
replaced jellyfin with jellyseerr. And also added emby to login page
2022-06-19 22:27:38 +05:00
Nicolai Van der Storm
63b7be0a38 Update private_registery_push.yml 2022-06-18 22:55:07 +02:00
Nicolai Van der Storm
d3cea69011 Update private_registery_push.yml 2022-06-18 22:48:47 +02:00
Nicolai Van der Storm
31072f4758 Update private_registery_push.yml 2022-06-18 22:45:43 +02:00
Nicolai Van der Storm
89e8825b61 fixes indent issues 2022-06-18 22:39:24 +02:00
Fallenbagel
7956ed8466 Merge pull request #144 from NicolaiVdS/private_registery_workflow
Create private_registery_push.yml
2022-06-19 01:34:08 +05:00
Nicolai Van der Storm
e1081a7bc2 Create private_registery_push.yml 2022-06-18 22:30:15 +02:00
Fallenbagel
fe3495705f Merge pull request #136 from NicolaiVdS/email-validation-and-requirement
feat(userprofile): email requirement and validation + import user button overhaul
2022-06-15 01:29:47 +05:00
Nicolai Van der Storm
29478fc195 fix(import all): fis for import all 2022-06-13 23:13:22 +02:00
Nicolai Van der Storm
f48286043e Merge pull request #139 from Fallenbagel/translation-issue
fix(ui): fixed translation issue where it showed as import {mediaServerName} user
2022-06-13 21:54:07 +02:00
Fallenbagel
d417fcafa1 fix(ui): replaced {mediaServerName} in the plex variable in NL locale
replaced {mediaServerName} in the plex variable in NL locale
2022-06-14 00:51:44 +05:00
Fallenbagel
819190ce98 fix(ui): fixed translation issue where it showed as import {mediaServerName} user
Fixed translation issue where it showed as import {mediaServerName} user as it was using the same
variable both inside the plex import modal and also outside in userlist on the button
2022-06-14 00:48:17 +05:00
Nicolai Van der Storm
a483ca9837 fix(jellyfinimportmodal): fix for importing all jellyfin users 2022-06-13 21:34:51 +02:00
Nicolai Van der Storm
d835336d33 feat(email validation): email requirement and validation + better importer 2022-06-13 14:21:05 +02:00
Nicolai Van der Storm
cc69f66ba9 chore(.idea folder): removed .idea folder and added it to the .gitignore 2022-06-10 12:30:14 +02:00
Nicolai Van der Storm
543859e6f3 feat(uesrprofile): email requirement and validation 2022-06-10 12:19:52 +02:00
Fallenbagel
4fd42874b7 Merge pull request #133 from Fallenbagel/fix-translation-errors
fix(ui): fix translation errors for all locales in the import plex user button
2022-06-09 16:51:56 +05:00
Fallenbagel
0fb5803eb9 fix(ui): fix translation errors for all locales in the import plex user button
fix translation errors for all locales in the import plex user button as it currently shows as
{mediaServerName}
2022-06-09 16:38:18 +05:00
Fallenbagel
00c08b3d67 Merge pull request #132 from notfakie/develop
fix: fix mediaServerType for plex users not being set properly
2022-06-09 16:25:48 +05:00
notfakie
94ade93e16 fix: fix mediaServerType not set for Plex which leads to Plex users seeing Jellyfin settings 2022-06-09 17:08:00 +12:00
Fallenbagel
caa713a968 Merge pull request #128 from NicolaiVdS/feature-add-email-field
feat: add email field in the profile settings
2022-06-05 21:53:53 +05:00
Nicolai Van der Storm
23779f4c7b style: removed .idea folder 2022-06-05 18:43:52 +02:00
Nicolai Van der Storm
5f2ebfe662 Revert "feat(tv): tv seasons"
This reverts commit c117b37cd9.
2022-06-05 18:37:50 +02:00
Nicolai Van der Storm
b22f20b6fa feat(user settings): added email field to user profiel settings
#122
2022-06-05 18:25:15 +02:00
Nicolai Van der Storm
a8bc0c068b feat: email
#122
2022-06-05 18:11:20 +02:00
Nicolai Van der Storm
30c48f16ca feat(user email setting): added field to save user email
fix #122
2022-06-05 17:46:26 +02:00
Fallenbagel
3748f64ce4 Merge pull request #127 from sambartik/fix-sync-errors
fix(jellyfin): fixes sync errors re-introduced in previous commits
2022-06-05 20:27:00 +05:00
Samuel Bartík
d1dbd6e3b9 fix(jellyfin): sync errors 2022-06-05 17:07:27 +02:00
Fallenbagel
6458c054c0 Merge pull request #126 from sambartik/fix-virtual-location
fix(jellyfin): ignore additional items with virtual location type
2022-06-05 18:25:01 +05:00
Samuel Bartík
c81154800f fix(jellyfin): ignore additional items with virtual location type 2022-06-05 12:41:34 +02:00
Fallenbagel
a1cd354691 Merge pull request #125 from sambartik/fix-scan
fix(scan): ignore virtual seasons
2022-06-05 12:35:13 +05:00
Samuel Bartík
6574e18516 fix(scan): ignore virtual seasons
Virtual seasons appeared as available / partially available, even though they were not even shown in the Jellyfin web UI. For more info see #119
2022-06-04 18:07:14 +02:00
Fallenbagel
5298e5fd90 Merge pull request #121 from notfakie/develop
Hide Overseerr settings when running in Jellyfin/Emby mode
2022-06-02 14:42:49 +05:00
notfakie
7450138ac1 fix: hide plex guid cache settings from ui when running in jellyfin/emby mode 2022-06-02 18:47:27 +12:00
notfakie
4b7bdd3d7d fix: remove internal Overseerr sponsor link, this is remaining on the main github page instead 2022-06-02 18:47:25 +12:00
notfakie
739f5f9c9a fix: only show mediaserver settings for current active mediaserver 2022-06-02 18:47:22 +12:00
Nicolai Van der Storm
c117b37cd9 feat(tv): tv seasons
tv seasons
2022-06-01 14:48:05 +02:00
Fallenbagel
3e7d64eb47 Merge pull request #120 from Fallenbagel/embySupport
fix(ui): fix emby ui elements not reflecting the env variable #98
2022-05-30 00:31:05 +05:00
Fallenbagel
b9546e6daa feat(ui): add emby user badge to the userProfile
adds emby user badge to the userProfile general page
2022-05-30 00:21:04 +05:00
Fallenbagel
722dda5856 fix(ui): fix ui elements not reflecting the env variable
Fix emby ui elements not reflecting the emby env variable set during runtime
2022-05-29 23:58:54 +05:00
Fallenbagel
c67ca34111 Merge pull request #118 from jab416171/patch-2
fix aur url
2022-05-29 05:50:09 +05:00
jab416171
16311808b1 fix aur url 2022-05-28 18:34:50 -06:00
Fallenbagel
509c43e552 Merge pull request #117 from jab416171/patch-1
Add manual install steps and aur package info
2022-05-29 05:29:49 +05:00
jab416171
84a97675dc Add manual install steps and aur package info 2022-05-28 18:14:57 -06:00
Fallenbagel
4615286f49 Merge pull request #115 from CyferShepard/JellyfinNoPasswordFix
feat:Remove Requirement for Jellyfin Passwords (#108 #31)
2022-05-28 12:32:02 +05:00
Fallenbagel
7b7354d006 Merge remote-tracking branch 'upstream/develop' into develop 2022-05-28 12:20:26 +05:00
CyferShepard
ad7b3590d7 Move auth.ts to correct folder 2022-05-28 08:50:51 +02:00
CyferShepard
bda7858b66 Delete auth.ts 2022-05-28 08:50:23 +02:00
CyferShepard
d600a45559 feat:Remove Requirement for Jellyfin Passwords 2022-05-28 08:29:45 +02:00
Fallenbagel
1dcfe49b1b Merge pull request #112 from Fallenbagel/forgotPasswordFix
fix: fixes jellyfin forgot password and adds emby support to the forgort password link
2022-05-28 07:29:46 +05:00
Fallenbagel
1dbc565a2e Merge pull request #103 from boring-dragon/develop
feat: conditional media server name to add emby
2022-05-28 07:29:38 +05:00
Fallenbagel
973a3e826f Merge pull request #111 from Fallenbagel/AvatarUrlFix
fix(ui): fix Avatar being broken when setup using internal ip
2022-05-28 07:22:31 +05:00
Fallenbagel
6a6bfe0c68 feat(ui): add emby as a mediaServerType to the import user button
Add emby as a mediaServerType to the import user button and replace any reference to Jellyfin with
mediaServerName
2022-05-27 05:35:00 +05:00
Fallenbagel
410b536c94 feat(ui): add emby user badge to the user list and fix local user badge
add emby user badge to the user list and fix local user badge which was previously not showing
2022-05-27 04:58:24 +05:00
Fallenbagel
18d8d969f1 style(ui): conditional mediaServerName to add emby to setup/login page 2022-05-26 09:39:19 +05:00
Fallenbagel
f8a239b1b8 style(ui): conditional media server name to add emby to settings
Conditionaly media server name to replace every reference of jellyfin with emby in settings tab when
environmental variable set
2022-05-26 08:52:16 +05:00
Fallenbagel
377a4fd85b feat(ui): conditional media server name to add emby to issuedetails play on button 2022-05-26 08:39:39 +05:00
Fallenbagel
14d293799b feat(ui): conditional media server name to add emby to moviedetails 2022-05-26 08:37:51 +05:00
Fallenbagel
ddd773c03f fix: conditional media server name for 4k url to add emby to tvdetails 2022-05-26 08:30:21 +05:00
Fallenbagel
e75b71b816 feat: conditional media server name to add emby to tvdetails 2022-05-26 05:54:57 +05:00
Fallenbagel
ff3e3ce841 feat: conditional media server name to add emby to tvdetails 2022-05-26 05:49:33 +05:00
Fallenbagel
01e81a73a3 fix(ui): fix Avatar being broken when setup using internal ip
allow avatar url to use externalHostname when setup using local ip

fix #110
2022-05-26 02:24:31 +05:00
Samuel Bartík
db05172d8b fix(ui): rectangular avatars getting stretched (#2782) 2022-05-25 20:42:17 +04:00
Mohamed jinas
2bfdf02c79 feat: conditional media server name 2022-05-22 22:12:05 +05:00
64 changed files with 745 additions and 285 deletions

View File

@@ -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'

3
.gitignore vendored
View File

@@ -53,3 +53,6 @@ config/db/db.sqlite3-journal
# VS Code
.vscode/launch.json
# Webstorm
.idea

View File

@@ -9,12 +9,16 @@
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
## Current Features
- Jellyfin Support
- Emby Support
(Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!)
Along with all the existing Overseerr features:
- Full Plex integration. Authenticate and manage user access with Plex!
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
- Plex library scan, to keep track of the titles which are already available.
@@ -31,6 +35,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
<img src="./public/preview.jpg">
@@ -55,4 +71,4 @@ Our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/COD
## Contributing
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.

View File

@@ -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'],
},

View File

@@ -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",

View File

@@ -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<any>(`/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}`,

View File

@@ -137,6 +137,8 @@ export class User {
@UpdateDateColumn()
public updatedAt: Date;
public warnings: string[] = [];
constructor(init?: Partial<User>) {
Object.assign(this, init);
}

View File

@@ -2,6 +2,7 @@ import { NotificationAgentKey } from '../../lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
email?: string;
discordId?: string;
locale?: string;
region?: string;

View File

@@ -13,6 +13,7 @@ import {
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import * as EmailValidator from 'email-validator';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
@@ -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',

View File

@@ -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,
@@ -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,
};
}

View File

@@ -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);
}

View File

@@ -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,
}));

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -90,7 +90,7 @@ const IssueComment: React.FC<IssueCommentProps> = ({
<img
src={comment.user.avatar}
alt=""
className="h-10 w-10 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>

View File

@@ -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 <LoadingSpinner />;
@@ -267,7 +269,7 @@ const IssueDetails: React.FC = () => {
>
<a className="group ml-1 inline-flex h-full items-center xl:ml-1.5">
<img
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
src={issueData.createdBy.avatar}
alt=""
/>
@@ -366,13 +368,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{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',
})}
</span>
</Button>
)}
@@ -407,13 +414,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{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',
})}
</span>
</Button>
)}
@@ -618,13 +630,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{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',
})}
</span>
</Button>
)}
@@ -659,13 +676,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{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',
})}
</span>
</Button>
)}

View File

@@ -228,7 +228,7 @@ const IssueItem: React.FC<IssueItemProps> = ({ issue }) => {
<img
src={issue.createdBy.avatar}
alt=""
className="avatar-sm ml-1.5"
className="avatar-sm ml-1.5 object-cover"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{issue.createdBy.displayName}

View File

@@ -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<SidebarProps> = ({ open, setClosed }) => {
);
})}
</nav>
<div className="px-2">
<UserWarnings onClick={() => setClosed()} />
</div>
{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus onClick={() => setClosed()} />
@@ -236,6 +241,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
);
})}
</nav>
<div className="px-2">
<UserWarnings />
</div>
{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus />

View File

@@ -40,7 +40,7 @@ const UserDropdown: React.FC = () => {
onClick={() => setDropdownOpen(true)}
>
<img
className="h-8 w-8 rounded-full sm:h-10 sm:w-10"
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar}
alt=""
/>

View File

@@ -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<UserWarningsProps> = ({ 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 = (
<Link href={link}>
<a
onClick={onClick}
onKeyDown={(e) => {
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"
>
<ExclamationIcon className="h-6 w-6" />
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
<span className="font-bold">{warningTitle}</span>
<span className="truncate">{warningText}</span>
</div>
</a>
</Link>
);
});
}
return res;
};
export default UserWarnings;

View File

@@ -50,6 +50,7 @@ const Layout: React.FC = ({ children }) => {
<div className="absolute top-0 h-64 w-full bg-gradient-to-bl from-gray-800 to-gray-900">
<div className="relative inset-0 h-full w-full bg-gradient-to-t from-gray-900 to-transparent" />
</div>
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">

View File

@@ -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<JellyfinLoginProps> = ({
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<JellyfinLoginProps> = ({
/^(?:(?:(?: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 (
<Formik
@@ -97,7 +101,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
<Form>
<div className="sm:border-t sm:border-gray-800">
<label htmlFor="host" className="text-label">
{intl.formatMessage(messages.host)}
{intl.formatMessage(messages.host, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
@@ -105,7 +114,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
id="host"
name="host"
type="text"
placeholder={intl.formatMessage(messages.host)}
placeholder={intl.formatMessage(messages.host, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
/>
</div>
{errors.host && touched.host && (
@@ -185,9 +199,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string().required(
intl.formatMessage(messages.validationpasswordrequired)
),
password: Yup.string(),
});
return (
<div>

View File

@@ -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',
})}
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8">

View File

@@ -272,7 +272,7 @@ const ManageSlideOver: React.FC<
<img
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>
@@ -387,7 +387,7 @@ const ManageSlideOver: React.FC<
<img
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>

View File

@@ -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<MovieDetailsProps> = ({ 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<MovieDetailsProps> = ({ 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: <PlayIcon />,
});
@@ -146,10 +145,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ 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: <PlayIcon />,
});
@@ -228,6 +224,30 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ 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 (
<div
className="media-page"

View File

@@ -231,7 +231,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
<img
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm"
className="avatar-sm object-cover"
/>
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
{requestData.requestedBy.displayName}

View File

@@ -336,7 +336,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
<img
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm ml-1.5"
className="avatar-sm ml-1.5 object-cover"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{requestData.requestedBy.displayName}
@@ -390,7 +390,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
<img
src={requestData.modifiedBy.avatar}
alt=""
className="avatar-sm ml-1.5"
className="avatar-sm ml-1.5 object-cover"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{requestData.modifiedBy.displayName}

View File

@@ -534,7 +534,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
<img
src={selectedUser.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full"
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
/>
<span className="ml-3 block">
{selectedUser.displayName}
@@ -584,7 +584,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
<img
src={user.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full"
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
/>
<span className="ml-3 block flex-shrink-0">
{user.displayName}

View File

@@ -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 = () => {
<Formik
initialValues={{
enabled: data.enabled,
userEmailRequired: data.options.userEmailRequired,
emailFrom: data.options.emailFrom,
smtpHost: data.options.smtpHost,
smtpPort: data.options.smtpPort ?? 587,
@@ -148,6 +150,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 = () => {
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="userEmailRequired" className="checkbox-label">
{intl.formatMessage(messages.userEmailRequired)}
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="userEmailRequired"
name="userEmailRequired"
/>
</div>
</div>
<div className="form-row">
<label htmlFor="senderName" className="text-label">
{intl.formatMessage(messages.senderName)}

View File

@@ -188,35 +188,6 @@ const SettingsAbout: React.FC = () => {
</List.Item>
</List>
</div>
<div className="section">
<List title={intl.formatMessage(messages.supportoverseerr)}>
<List.Item
title={`${intl.formatMessage(messages.helppaycoffee)} ☕️`}
>
<a
href="https://github.com/sponsors/sct"
target="_blank"
rel="noreferrer"
className="text-indigo-500 transition duration-300 hover:underline"
>
https://github.com/sponsors/sct
</a>
<Badge className="ml-2">
{intl.formatMessage(messages.preferredmethod)}
</Badge>
</List.Item>
<List.Item title="">
<a
href="https://patreon.com/overseerr"
target="_blank"
rel="noreferrer"
className="text-indigo-500 transition duration-300 hover:underline"
>
https://patreon.com/overseerr
</a>
</List.Item>
</List>
</div>
<div className="section">
<Releases currentVersion={data.version} />
</div>

View File

@@ -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 {mediaServerName}, 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<SettingsJellyfinProps> = ({
);
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<SettingsJellyfinProps> = ({
<>
<div className="mb-6">
<h3 className="heading">
<FormattedMessage {...messages.jellyfinlibraries} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Jellyfin',
})}
</h3>
<p className="description">
<FormattedMessage {...messages.jellyfinlibrariesDescription} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Jellyfin',
})}
</p>
</div>
<div className="section">
@@ -201,7 +215,13 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<FormattedMessage {...messages.manualscanJellyfin} />
</h3>
<p className="description">
<FormattedMessage {...messages.manualscanDescriptionJellyfin} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Jellyfin',
})}
</p>
</div>
<div className="section">
@@ -305,10 +325,22 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<>
<div className="mt-10 mb-6">
<h3 className="heading">
{intl.formatMessage(messages.jellyfinSettings)}
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Jellyfin',
})}
</h3>
<p className="description">
{intl.formatMessage(messages.jellyfinSettingsDescription)}
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Jellyfin',
})}
</p>
</div>
<Formik
@@ -322,15 +354,31 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
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();
}

View File

@@ -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 <LoadingSpinner />;
@@ -369,22 +372,33 @@ const SettingsJobs: React.FC = () => {
</tr>
</thead>
<Table.TBody>
{cacheData?.map((cache) => (
<tr key={`cache-list-${cache.id}`}>
<Table.TD>{cache.name}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.misses)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.keys)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
<Table.TD alignText="right">
<Button buttonType="danger" onClick={() => flushCache(cache)}>
<TrashIcon />
<span>{intl.formatMessage(messages.flushcache)}</span>
</Button>
</Table.TD>
</tr>
))}
{cacheData
?.filter(
(cache) =>
!(
settings.currentSettings.mediaServerType !==
MediaServerType.PLEX && cache.id === 'plexguid'
)
)
.map((cache) => (
<tr key={`cache-list-${cache.id}`}>
<Table.TD>{cache.name}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.misses)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.keys)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushCache(cache)}
>
<TrashIcon />
<span>{intl.formatMessage(messages.flushcache)}</span>
</Button>
</Table.TD>
</tr>
))}
</Table.TBody>
</Table>
</div>

View File

@@ -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 }) => {
<div className="mt-10 text-white">{children}</div>
</>
);
function getAvailableMediaServerName() {
return intl.formatMessage(messages.menuJellyfinSettings, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin',
});
}
};
export default SettingsLayout;

View File

@@ -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<MainSettings>('/api/v1/settings/main');
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -131,16 +133,20 @@ const SettingsUsers: React.FC = () => {
<label htmlFor="newPlexLogin" className="checkbox-label">
{intl.formatMessage(messages.newPlexLogin, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
<span className="label-tip">
{intl.formatMessage(messages.newPlexLoginTip, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}

View File

@@ -5,7 +5,7 @@ import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
const messages = defineMessages({
welcome: 'Welcome to Overseerr',
welcome: 'Welcome to Jellyseerr',
signinMessage: 'Get started by signing in with your Plex account',
});

View File

@@ -3,14 +3,15 @@ import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
import JellyfinLogin from '../Login/JellyfinLogin';
import axios from 'axios';
import { defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Accordion from '../Common/Accordion';
import { MediaServerType } from '../../../server/constants/server';
import getConfig from 'next/config';
const messages = defineMessages({
welcome: 'Welcome to Overseerr',
welcome: 'Welcome to Jellyseerr',
signinMessage: 'Get started by signing in',
signinWithJellyfin: 'Use your Jellyfin account',
signinWithJellyfin: 'Use your {mediaServerName} account',
signinWithPlex: 'Use your Plex account',
});
@@ -24,7 +25,8 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
MediaServerType.NOT_CONFIGURED
);
const { user, revalidate } = useUser();
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to login. If we get a success message, we will
// ask swr to revalidate the user which _shouid_ come back with a valid user.
@@ -91,7 +93,13 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
}`}
onClick={() => handleClick(1)}
>
<FormattedMessage {...messages.signinWithJellyfin} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Jellyfin',
})}
</button>
<AccordionContent isOpen={openIndexes.includes(1)}>
<div

View File

@@ -44,6 +44,7 @@ import RequestButton from '../RequestButton';
import RequestModal from '../RequestModal';
import Slider from '../Slider';
import StatusBadge from '../StatusBadge';
import getConfig from 'next/config';
const messages = defineMessages({
firstAirDate: 'First Air Date',
@@ -85,6 +86,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
router.query.manage == '1' ? true : false
);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -124,10 +126,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
})
) {
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: <PlayIcon />,
});
@@ -141,10 +140,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
})
) {
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: <PlayIcon />,
});
@@ -228,6 +224,30 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
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 (
<div
className="media-page"

View File

@@ -8,6 +8,8 @@ import useSettings from '../../hooks/useSettings';
import globalMessages from '../../i18n/globalMessages';
import Alert from '../Common/Alert';
import Modal from '../Common/Modal';
import getConfig from 'next/config';
import { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
interface JellyfinImportProps {
onCancel?: () => void;
@@ -15,23 +17,25 @@ interface JellyfinImportProps {
}
const messages = defineMessages({
importfromJellyfin: 'Import Jellyfin Users',
importfromJellyfin: 'Import {mediaServerName} Users',
importfromJellyfinerror:
'Something went wrong while importing Jellyfin users.',
'Something went wrong while importing {mediaServerName} users.',
importedfromJellyfin:
'<strong>{userCount}</strong> Jellyfin {userCount, plural, one {user} other {users}} imported successfully!',
'<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!',
user: 'User',
noJellyfinuserstoimport: 'There are no Jellyfin users to import.',
noJellyfinuserstoimport: 'There are no {mediaServerName} users to import.',
newJellyfinsigninenabled:
'The <strong>Enable New Jellyfin Sign-In</strong> setting is currently enabled. Jellyfin users with library access do not need to be imported in order to sign in.',
'The <strong>Enable New {mediaServerName} Sign-In</strong> setting is currently enabled. {mediaServerName} users with library access do not need to be imported in order to sign in.',
});
const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
onCancel,
onComplete,
children,
}) => {
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const [isImporting, setImporting] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -47,6 +51,18 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
revalidateOnMount: true,
});
const { data: existingUsers } = useSWR<UserResultsResponse>(
`/api/v1/user?take=${children}`
);
data?.forEach((user, pos) => {
if (
existingUsers?.results.some((data) => data.jellyfinUserId === user.id)
) {
data?.splice(pos, 1);
}
});
const importUsers = async () => {
setImporting(true);
@@ -66,6 +82,8 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
}),
{
autoDismiss: true,
@@ -77,10 +95,16 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
onComplete();
}
} catch (e) {
addToast(intl.formatMessage(messages.importfromJellyfinerror), {
autoDismiss: true,
appearance: 'error',
});
addToast(
intl.formatMessage(messages.importfromJellyfinerror, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
}),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
setImporting(false);
}
@@ -110,7 +134,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
return (
<Modal
loading={!data && !error}
title={intl.formatMessage(messages.importfromJellyfin)}
title={intl.formatMessage(messages.importfromJellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})}
iconSvg={<InboxInIcon />}
onOk={() => {
importUsers();
@@ -126,6 +153,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
{settings.currentSettings.newPlexLogin && (
<Alert
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
strong: function strong(msg) {
return (
<strong className="font-semibold text-white">{msg}</strong>
@@ -240,7 +271,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
</>
) : (
<Alert
title={intl.formatMessage(messages.noJellyfinuserstoimport)}
title={intl.formatMessage(messages.noJellyfinuserstoimport, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})}
type="info"
/>
)}

View File

@@ -9,6 +9,7 @@ import {
} from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
@@ -40,7 +41,7 @@ import PlexImportModal from './PlexImportModal';
const messages = defineMessages({
users: 'Users',
userlist: 'User List',
importfromplex: 'Import {mediaServerName} Users',
importfrommediaserver: 'Import {mediaServerName} Users',
user: 'User',
totalrequests: 'Requests',
accounttype: 'Type',
@@ -87,6 +88,7 @@ const UserList: React.FC = () => {
const intl = useIntl();
const router = useRouter();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
@@ -480,7 +482,9 @@ const UserList: React.FC = () => {
setShowImportModal(false);
revalidate();
}}
/>
>
{data.pageInfo.results}
</JellyfinImportModal>
)}
</Transition>
@@ -503,13 +507,18 @@ const UserList: React.FC = () => {
>
<InboxInIcon />
<span>
{intl.formatMessage(messages.importfromplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Jellyfin',
})}
</span>
</Button>
</div>
@@ -596,7 +605,7 @@ const UserList: React.FC = () => {
<Link href={`/users/${user.id}`}>
<a className="h-10 w-10 flex-shrink-0">
<img
className="h-10 w-10 rounded-full"
className="h-10 w-10 rounded-full object-cover"
src={user.avatar}
alt=""
/>
@@ -636,17 +645,23 @@ const UserList: React.FC = () => {
<Badge badgeType="warning">
{intl.formatMessage(messages.plexuser)}
</Badge>
) : (
) : user.userType === UserType.LOCAL ? (
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
mediaServerName: 'Emby',
})}
</Badge>
)}
) : user.userType === UserType.JELLYFIN ? (
<Badge badgeType="default">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Jellyfin',
})}
</Badge>
) : null}
</Table.TD>
<Table.TD>
{user.id === 1

View File

@@ -44,7 +44,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
<div className="flex-shrink-0">
<div className="relative">
<img
className="h-24 w-24 rounded-full bg-gray-600 ring-1 ring-gray-700"
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
src={user.avatar}
alt=""
/>

View File

@@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { MediaServerType } from '../../../../../server/constants/server';
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import {
availableLanguages,
@@ -25,11 +24,13 @@ import PageTitle from '../../../Common/PageTitle';
import LanguageSelector from '../../../LanguageSelector';
import QuotaSelector from '../../../QuotaSelector';
import RegionSelector from '../../../RegionSelector';
import getConfig from 'next/config';
const messages = defineMessages({
general: 'General',
generalsettings: 'General Settings',
displayName: 'Display Name',
email: 'Email',
save: 'Save Changes',
saving: 'Saving…',
mediaServerUser: '{mediaServerName} User',
@@ -59,7 +60,7 @@ const messages = defineMessages({
const UserGeneralSettings: React.FC = () => {
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const { locale, setLocale } = useLocale();
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
@@ -120,8 +121,9 @@ const UserGeneralSettings: React.FC = () => {
</div>
<Formik
initialValues={{
displayName: data?.username,
discordId: data?.discordId,
displayName: data?.username ?? '',
email: data?.email ?? '',
discordId: data?.discordId ?? '',
locale: data?.locale,
region: data?.region,
originalLanguage: data?.originalLanguage,
@@ -136,6 +138,7 @@ const UserGeneralSettings: React.FC = () => {
try {
await axios.post(`/api/v1/user/${user?.id}/settings/main`, {
username: values.displayName,
email: values.email,
discordId: values.discordId,
locale: values.locale,
region: values.region,
@@ -189,19 +192,25 @@ const UserGeneralSettings: React.FC = () => {
<div className="flex max-w-lg items-center">
{user?.userType === UserType.PLEX ? (
<Badge badgeType="warning">
{intl.formatMessage(messages.plexuser)}
</Badge>
) : user?.userType === UserType.LOCAL ? (
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : (
<Badge badgeType="default">
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
mediaServerName: 'Emby',
})}
</Badge>
)}
) : user?.userType === UserType.JELLYFIN ? (
<Badge badgeType="default">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Jellyfin',
})}
</Badge>
) : null}
</div>
</div>
</div>
@@ -239,6 +248,32 @@ const UserGeneralSettings: React.FC = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
{user?.warnings.find((w) => w === 'userEmailRequired') && (
<span className="label-required">*</span>
)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="email"
name="email"
type="text"
placeholder="example@domain.com"
className={
user?.warnings.find((w) => w === 'userEmailRequired')
? 'border-2 border-red-400 focus:border-blue-600'
: ''
}
/>
</div>
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="discordId" className="text-label">
{intl.formatMessage(messages.discordId)}

View File

@@ -13,6 +13,7 @@ export type { PermissionCheckOptions };
export interface User {
id: number;
warnings: string[];
plexUsername?: string;
username?: string;
displayName: string;

View File

@@ -686,7 +686,8 @@
"components.UserList.nouserstoimport": "No hi ha usuaris nous de Plex a importar.",
"components.UserList.localuser": "Usuari local",
"components.UserList.importfromplexerror": "S'ha produït un error en importar usuaris de Plex.",
"components.UserList.importfromplex": "Importeu usuaris de {mediaServerName}",
"components.UserList.importfrommediaserver": "Importeu usuaris de {mediaServerName}",
"components.UserList.importfromplex": "Importeu usuaris de Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuari} other {usuaris}} de Plex importat correctament!",
"components.TvDetails.watchtrailer": "Veure el tràiler",
"components.TvDetails.viewfullcrew": "Mostreu equip complet",

View File

@@ -850,7 +850,8 @@
"components.UserList.nouserstoimport": "Ingen nye brugere som kan importeres fra Plex.",
"components.UserList.edituser": "Redigér Brugertilladelser",
"components.UserList.email": "Email Adresse",
"components.UserList.importfromplex": "Importér Brugere fra {mediaServerName}",
"components.UserList.importfrommediaserver": "Importér Brugere fra {mediaServerName}",
"components.UserList.importfromplex": "Importér Brugere fra Plex",
"components.UserList.owner": "Ejer",
"components.UserList.password": "Kodeord",
"components.UserList.passwordinfodescription": "Konfigurér en applikations-URL og aktivér emailnotifikationer for at tillade automatisk kodeordsgenerering.",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.latestversion": "Neuste",
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
"components.UserList.importfromplexerror": "Beim Importieren von Plex-Benutzern ist etwas schief gelaufen.",
"components.UserList.importfromplex": "{mediaServerName}-Benutzer importieren",
"components.UserList.importfrommediaserver": "{mediaServerName}-Benutzer importieren",
"components.UserList.importfromplex": "Plex-Benutzer importieren",
"components.TvDetails.viewfullcrew": "Komplette Crew anzeigen",
"components.TvDetails.TvCrew.fullseriescrew": "Komplette Serien-Crew",
"components.PersonDetails.crewmember": "Crew",

View File

@@ -602,7 +602,8 @@
"components.UserList.localuser": "Τοπικός χρήστης",
"components.UserList.localLoginDisabled": "Η ρύθμιση <strong>Ενεργοποίηση τοπικής σύνδεσης</strong> είναι προς το παρόν απενεργοποιημένη.",
"components.UserList.importfromplexerror": "Κάτι πήγε στραβά κατά την εισαγωγή χρηστών από το Plex.",
"components.UserList.importfromplex": "Εισαγωγή χρηστών από το {mediaServerName}",
"components.UserList.importfrommediaserver": "Εισαγωγή χρηστών από το {mediaServerName}",
"components.UserList.importfromplex": "Εισαγωγή χρηστών από το Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# νέου χρήστη} other {#νέοι χρήστες}} εισήχθησαν απο το Plex επιτυχώς!",
"components.UserList.email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"components.UserList.edituser": "Επεξεργασία δικαιωμάτων χρήστη",

View File

@@ -861,7 +861,8 @@
"components.UserList.edituser": "Edit User Permissions",
"components.UserList.email": "Email Address",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} imported successfully!",
"components.UserList.importfromplex": "Import {mediaServerName} Users",
"components.UserList.importfrommediaserver": "Import {mediaServerName} Users",
"components.UserList.importfromplex": "Import Plex Users",
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
"components.UserList.localuser": "Local User",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.currentversion": "Actual",
"components.MovieDetails.studio": "{studioCount, plural, one {Estudio} other {Estudios}}",
"components.UserList.importfromplexerror": "Algo salió mal importando usuarios de Plex.",
"components.UserList.importfromplex": "Importar Usuarios de {mediaServerName}",
"components.UserList.importfrommediaserver": "Importar Usuarios de {mediaServerName}",
"components.UserList.importfromplex": "Importar Usuarios de Plex",
"components.UserList.importedfromplex": "¡{userCount, plural, one {# nuevo usuario} other {# nuevos usuarios}} importado/s de Plex con éxito!",
"components.TvDetails.viewfullcrew": "Ver Equipo Completo",
"components.TvDetails.firstAirDate": "Primera fecha de emisión",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.latestversion": "Dernière version",
"components.Settings.SettingsAbout.Releases.currentversion": "Actuelle",
"components.UserList.importfromplexerror": "Une erreur s'est produite durant l'importation des utilisateurs de Plex.",
"components.UserList.importfromplex": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.importfrommediaserver": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.importfromplex": "Importer les utilisateurs de Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {utilisateur} other {utilisateurs}} importé(s) depuis Plex avec succès !",
"components.TvDetails.viewfullcrew": "Voir l'équipe complète de la série",
"components.TvDetails.TvCrew.fullseriescrew": "Équipe complète de la série",

View File

@@ -165,7 +165,8 @@
"components.UserList.password": "Jelszó",
"components.UserList.localuser": "Helyi felhasználó",
"components.UserList.importfromplexerror": "Hiba történt a felhasználók Plex-ről történő importálása közben.",
"components.UserList.importfromplex": "Felhasználók importálása {mediaServerName}-ről",
"components.UserList.importfrommediaserver": "Felhasználók importálása {mediaServerName}-ről",
"components.UserList.importfromplex": "Felhasználók importálása Plex-ről",
"components.UserList.importedfromplex": "{userCount, plural, =0 {Nem lett új} one {# új} other {# új}} felhasználó importálva Plex-ről!",
"components.UserList.email": "E-mail-cím",
"components.UserList.deleteuser": "Felhasználó törlése",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.latestversion": "Versione più recente",
"components.Settings.SettingsAbout.Releases.currentversion": "Versione attuale",
"components.UserList.importfromplexerror": "Qualcosa è andato storto nell'importare gli utenti Plex.",
"components.UserList.importfromplex": "Importa utenti {mediaServerName}",
"components.UserList.importfrommediaserver": "Importa utenti {mediaServerName}",
"components.UserList.importfromplex": "Importa utenti Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {utente} other {utenti}} Plex {userCount, plural, one {importato} other {importati}} correttamente!",
"components.TvDetails.viewfullcrew": "Vedi troupe completa",
"components.TvDetails.TvCrew.fullseriescrew": "Troupe completa serie",

View File

@@ -231,7 +231,8 @@
"components.TvDetails.watchtrailer": "予告編を見る",
"components.MovieDetails.watchtrailer": "予告編を見る",
"components.UserList.importfromplexerror": "Plexからユーザーをインポート中に問題が発生しました。",
"components.UserList.importfromplex": "{mediaServerName}からユーザーをインポート",
"components.UserList.importfrommediaserver": "{mediaServerName}からユーザーをインポート",
"components.UserList.importfromplex": "Plexからユーザーをインポート",
"components.UserList.importedfromplex": "Plex から新ユーザー {userCount} 名をインポートしました。",
"components.TvDetails.viewfullcrew": "フルクルーを表示",
"components.TvDetails.firstAirDate": "初放送日",

View File

@@ -194,7 +194,8 @@
"components.UserList.userssaved": "Brukertillatelsene ble lagret!",
"components.UserList.users": "Brukere",
"components.UserList.importfromplexerror": "Noe gikk galt ved importering av brukere fra Plex.",
"components.UserList.importfromplex": "Importer brukere fra {mediaServerName}",
"components.UserList.importfrommediaserver": "Importer brukere fra {mediaServerName}",
"components.UserList.importfromplex": "Importer brukere fra Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {ny bruker} other {nye brukere}} ble importert fra Plex!",
"components.Settings.menuUsers": "Brukere",
"components.Settings.SettingsUsers.users": "Brukere",

View File

@@ -214,7 +214,8 @@
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.",
"components.UserList.userdeleted": "Gebruiker succesvol verwijderd!",
"components.UserList.importfromplexerror": "Er is iets misgegaan bij het importeren van Plex-gebruikers.",
"components.UserList.importfromplex": "{mediaServerName}-gebruikers importeren",
"components.UserList.importfrommediaserver": "{mediaServerName}-gebruikers importeren",
"components.UserList.importfromplex": "Plex-gebruikers importeren",
"components.UserList.deleteuser": "Gebruiker verwijderen",
"components.UserList.deleteconfirm": "Weet je zeker dat je deze gebruiker wilt verwijderen? Al hun bestaande aanvraaggegevens zullen worden verwijderd.",
"components.TvDetails.watchtrailer": "Trailer bekijken",

View File

@@ -962,7 +962,8 @@
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Wyślij po cichu",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Nie udało się zapisać ustawień powiadomień telegram.",
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "<FindDiscordIdLink>Wielocyfrowy numer ID</FindDiscordIdLink> powiązany z Twoim kontem użytkownika",
"components.UserList.importfromplex": "Importuj użytkowników {mediaServerName}",
"components.UserList.importfrommediaserver": "Importuj użytkowników {mediaServerName}",
"components.UserList.importfromplex": "Importuj użytkowników Plex",
"i18n.available": "Dostępny",
"components.UserList.sortDisplayName": "Wyświetlana nazwa",
"components.UserList.totalrequests": "Prośby",

View File

@@ -228,7 +228,8 @@
"components.MovieDetails.viewfullcrew": "Ver Equipe Técnica Completa",
"components.MovieDetails.MovieCrew.fullcrew": "Equipe Técnica Completa",
"components.UserList.importfromplexerror": "Algo deu errado ao importar usuários do Plex.",
"components.UserList.importfromplex": "Importar Usuários do {mediaServerName}",
"components.UserList.importfrommediaserver": "Importar Usuários do {mediaServerName}",
"components.UserList.importfromplex": "Importar Usuários do Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuário Plex importado} other {usuários Plex importados}} com sucesso!",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Habilitar Agente",
"components.RequestList.RequestItem.failedretry": "Algo deu errado ao retentar fazer a solicitação.",

View File

@@ -199,7 +199,8 @@
"components.UserList.passwordinfodescription": "Configurar um URL de aplicação e ativar as notificações por e-mail para permitir a geração automática de palavra-passe.",
"components.UserList.localuser": "Utilizador Local",
"components.UserList.importfromplexerror": "Ocorreu um erro ao importar utilizadores do Plex.",
"components.UserList.importfromplex": "Importar Utilizadores do {mediaServerName}",
"components.UserList.importfrommediaserver": "Importar Utilizadores do {mediaServerName}",
"components.UserList.importfromplex": "Importar Utilizadores do Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# novo utilizador} other {# novos utilizadores}} importados do Plex com sucesso!",
"components.UserList.email": "Endereço de E-mail",
"components.UserList.deleteuser": "Apagar Utilizador",

View File

@@ -793,7 +793,8 @@
"components.UserList.usercreatedfailed": "Что-то пошло не так при создании пользователя.",
"components.UserList.passwordinfodescription": "Настройте URL-адрес приложения и включите уведомления по электронной почте, чтобы обеспечить возможность автоматической генерации пароля.",
"components.UserList.importfromplexerror": "Что-то пошло не так при импорте пользователей из Plex.",
"components.UserList.importfromplex": "Импортировать пользователей из {mediaServerName}",
"components.UserList.importfrommediaserver": "Импортировать пользователей из {mediaServerName}",
"components.UserList.importfromplex": "Импортировать пользователей из Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из Plex!",
"components.UserList.edituser": "Изменить разрешения пользователя",
"components.UserList.displayName": "Отображаемое имя",

View File

@@ -1006,7 +1006,8 @@
"components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Diçka shkoi keq duke ruajtur cilësimet.",
"components.Settings.webAppUrlTip": "Në mënyrë opsionale drejto përdoruesit në aplikacionin web në serverin tënd në vend të atij web",
"components.TvDetails.episodeRuntimeMinutes": "{runtime} minuta",
"components.UserList.importfromplex": "Importoni përdoruesit {mediaServerName}",
"components.UserList.importfrommediaserver": "Importoni përdoruesit {mediaServerName}",
"components.UserList.importfromplex": "Importoni përdoruesit Plex",
"components.UserList.importfromplexerror": "Diçka shkoi keq duke importuar përdoruesit Plex.",
"components.TvDetails.firstAirDate": "Data e parë e transmetimit",
"components.UserList.email": "Adresa email",

View File

@@ -222,7 +222,8 @@
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Versionsdata är för närvarande inte tillgänglig.",
"components.Settings.SettingsAbout.Releases.latestversion": "Senaste Versionen",
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
"components.UserList.importfromplex": "Importera {mediaServerName}användare",
"components.UserList.importfrommediaserver": "Importera {mediaServerName}användare",
"components.UserList.importfromplex": "Importera Plexanvändare",
"components.UserList.importfromplexerror": "Något gick fel när Plexanvändare importerades.",
"components.TvDetails.watchtrailer": "Kolla Trailer",
"components.Settings.Notifications.allowselfsigned": "Tillåt Självsignerade Certifikat",

View File

@@ -24,7 +24,8 @@
"components.UserList.localuser": "本地用户",
"components.UserList.localLoginDisabled": "<strong>允许本地登录</strong>的设置目前被禁用。",
"components.UserList.importfromplexerror": "导入 Plex 用户时出错。",
"components.UserList.importfromplex": "导入 {mediaServerName} 用户",
"components.UserList.importfrommediaserver": "导入 {mediaServerName} 用户",
"components.UserList.importfromplex": "导入 Plex 用户",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} 成功导入!",
"components.UserList.email": "电子邮件地址",
"components.UserList.edituser": "编辑用户权限",

View File

@@ -52,7 +52,8 @@
"components.Settings.radarrsettings": "Radarr 設定",
"components.Settings.menuPlexSettings": "Plex",
"components.UserList.importfromplexerror": "匯入 Plex 使用者時出了點問題。",
"components.UserList.importfromplex": "匯入 {mediaServerName} 使用者",
"components.UserList.importfrommediaserver": "匯入 {mediaServerName} 使用者",
"components.UserList.importfromplex": "匯入 Plex 使用者",
"components.UserList.importedfromplex": "匯入 <strong>{userCount}</strong> 個 Plex 使用者成功!",
"components.UserList.localuser": "本地使用者",
"components.UserList.creating": "創建中…",

View File

@@ -2,6 +2,7 @@
const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = {
important: true,
mode: 'jit',
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
theme: {

View File

@@ -1622,6 +1622,13 @@
dependencies:
glob "7.1.7"
"@next/eslint-plugin-next@^12.1.6":
version "12.1.6"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e"
integrity sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw==
dependencies:
glob "7.1.7"
"@next/swc-android-arm64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
@@ -4780,6 +4787,11 @@ email-templates@^8.0.10:
nodemailer "^6.7.2"
preview-email "^3.0.5"
email-validator@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==
emoji-regex@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.1.tgz#77180edb279b99510a21b79b19e1dc283d8f3991"