Compare commits

...

24 Commits

Author SHA1 Message Date
gauthier-th
2d802a5eff chore: merge develop 2024-07-29 22:04:36 +02:00
Gauthier
d5f817e734 fix: remove email requirement for the user, and use the username if no email provided (#900)
* fix: remove email requirement for the user, and use the username if no email provided

* fix: update translations

* fix: remove useless console.log

* test: fix user list test

* fix: disallow Plex users from changing their email
2024-07-29 21:27:31 +02:00
Gauthier
422085523e fix: resize header image in network and studio pages (#902) 2024-07-29 16:49:51 +02:00
Gauthier
fccfca6ed0 fix: enhance error messages when Fetch API fails (#893) 2024-07-27 01:43:16 +02:00
Gauthier
3fc14c9e22 fix: rewrite the rate limit utility (#896) 2024-07-26 21:12:41 +05:00
fallenbagel
e03edd2d1f refactor: use enums instead of numbers 2024-07-26 03:38:11 +05:00
fallenbagel
2d9530c0ed refactor(i18n): fix a typo 2024-07-25 22:02:34 +05:00
fallenbagel
1cc43cee93 refactor: use title case for servertype i18n message 2024-07-25 22:00:20 +05:00
fallenbagel
5bcbc810d6 style: decrease emby logo size in setup screen 2024-07-25 21:58:04 +05:00
fallenbagel
b8042ab700 refactor: change emby logo to full logo 2024-07-25 19:44:16 +05:00
fallenbagel
c3a6c7d4b2 fix: emby media server type migration 2024-07-25 19:39:25 +05:00
fallenbagel
6636aeef45 fix: scan jobs not running when media server type is emby 2024-07-25 19:09:03 +05:00
fallenbagel
6207a6a26d feat: migrate existing emby setups to use emby mediaServerType 2024-07-25 18:36:11 +05:00
fallenbagel
5875b2b5c2 Merge branch 'develop' into feat-server-type-setup 2024-07-25 18:24:35 +05:00
Gauthier
635a5f019c chore: merge develop 2024-07-15 18:34:58 +02:00
Gauthier
900e6110ad feat: add server type step to setup 2024-06-04 12:25:39 +02:00
fallenbagel
19e20749c1 revert: removed conditional render of the auto-request permission
reverts the conditional render toshow the auto-request permission if the mediaServerType was set to
Plex as this should be handled in a different PR and Cypress tests should be modified
accordingly(currently cypress test would fail if this conditional check is there)
2024-03-14 04:07:03 +05:00
fallenbagel
1aac3c5f09 refactor: cleaner way of handling serverType change using MediaServerType instead of strings
instead of using strings now it will use MediaServerType enums for serverType
2024-03-14 03:44:34 +05:00
fallenbagel
1bc4bd3d69 fix: issue page formatMessage for 4k media 2024-03-14 02:41:46 +05:00
fallenbagel
7a505813d5 refactor(auth): jellyfin/emby authentication to set MediaServerType 2024-03-14 01:19:09 +05:00
fallenbagel
45dbf84d7e refactor: use enums for serverType and rename selectedservice to serverType 2024-03-14 01:18:03 +05:00
fallenbagel
a4f1f1203a feat(api): add severType to the api
BREAKING CHANGE: This adds a serverType to the `/auth/jellyfin` which requires a serverType to be
set (`jellyfin`/`emby`)
2024-03-14 00:52:16 +05:00
fallenbagel
504d8bd5fe Merge branch 'develop' into feat-server-type-setup 2024-03-14 00:48:00 +05:00
fallenbagel
395a91c2f0 feat: add Media Server Selection to Setup Page
Introduce the ability to select the media server type on the setup page. Users can now choose their
preferred media server (e.g., Plex through the Plex sign-in or Emby/Jellyfin sign-in to select
either Emby or Jellyfin). The selected media server type is then reflected in the application
settings. This enhancement provides users with increased flexibility and customization options
during the initial setup process, eliminating the need to rely on environment variables (which
cannot be set if using platforms like snaps). Existing Emby users, who use the environment variable,
should log out and log back in after updating to set their mediaServerType to Emby.

BREAKING CHANGE: This commit deprecates the JELLYFIN_TYPE variable to identify Emby media server and
instead rely on the mediaServerType that is set in the `settings.json`. Existing environment
variable users can log out and log back in to set the mediaServerType to `3` (Emby).
2023-11-18 02:20:05 +05:00
48 changed files with 1037 additions and 443 deletions

View File

@@ -1,5 +1,5 @@
const testUser = {
displayName: 'Test User',
username: 'Test User',
emailAddress: 'test@seeerr.dev',
password: 'test1234',
};
@@ -32,7 +32,7 @@ describe('User List', () => {
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
cy.get('#displayName').type(testUser.displayName);
cy.get('#username').type(testUser.username);
cy.get('#email').type(testUser.emailAddress);
cy.get('#password').type(testUser.password);

View File

@@ -3586,6 +3586,8 @@ paths:
type: string
email:
type: string
serverType:
type: number
required:
- username
- password

View File

@@ -68,7 +68,10 @@ class ExternalAPI {
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
@@ -106,6 +109,15 @@ class ExternalAPI {
},
body: JSON.stringify(data),
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const resData = await this.getDataFromResponse(response);
if (this.cache) {
@@ -141,6 +153,15 @@ class ExternalAPI {
},
body: JSON.stringify(data),
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const resData = await this.getDataFromResponse(response);
if (this.cache) {
@@ -163,6 +184,15 @@ class ExternalAPI {
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
return data;
@@ -197,6 +227,17 @@ class ExternalAPI {
...config?.headers,
},
}).then(async (response) => {
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${
text ? ': ' + text : ''
}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
});
@@ -212,6 +253,15 @@ class ExternalAPI {
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
if (this.cache) {
@@ -250,13 +300,13 @@ class ExternalAPI {
}
private async getDataFromResponse(response: Response) {
const contentType = response.headers.get('Content-Type')?.split(';')[0];
if (contentType === 'application/json') {
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return await response.json();
} else if (
contentType === 'application/xml' ||
contentType === 'text/html' ||
contentType === 'text/plain'
contentType?.includes('application/xml') ||
contentType?.includes('text/html') ||
contentType?.includes('text/plain')
) {
return await response.text();
} else {

View File

@@ -152,7 +152,7 @@ class JellyfinAPI extends ExternalAPI {
try {
return await authenticate(false);
} catch (e) {
const status = e.response?.status;
const status = e.cause?.status;
const networkErrorCodes = new Set([
'ECONNREFUSED',
@@ -190,7 +190,7 @@ class JellyfinAPI extends ExternalAPI {
return systemInfoResponse;
} catch (e) {
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -207,7 +207,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
throw new ApiError(e.cause?.status, ApiErrorCode.Unknown);
}
}
@@ -222,7 +222,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -238,7 +238,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -317,7 +317,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -338,7 +338,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -353,7 +353,7 @@ class JellyfinAPI extends ExternalAPI {
return itemResponse;
} catch (e) {
if (availabilitySync.running) {
if (e.response && e.response.status === 500) {
if (e.cause?.status === 500) {
return undefined;
}
}
@@ -362,7 +362,7 @@ class JellyfinAPI extends ExternalAPI {
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -377,7 +377,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
@@ -402,7 +402,7 @@ class JellyfinAPI extends ExternalAPI {
{ label: 'Jellyfin API' }
);
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
}
}
}

View File

@@ -179,13 +179,20 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
}
return data;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error(
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
{
label: 'Radarr',
errorMessage: e.message,
options,
response: e?.response?.data,
response: errorData,
}
);
throw new Error('Failed to add movie to Radarr');

View File

@@ -257,11 +257,18 @@ class SonarrAPI extends ServarrBase<{
return createdSeriesData;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Something went wrong while adding a series to Sonarr.', {
label: 'Sonarr API',
errorMessage: e.message,
options,
response: e?.response?.data,
response: errorData,
});
throw new Error('Failed to add series');
}

View File

@@ -4,3 +4,8 @@ export enum MediaServerType {
EMBY,
NOT_CONFIGURED,
}
export enum ServerType {
JELLYFIN = 'Jellyfin',
EMBY = 'Emby',
}

View File

@@ -2,4 +2,5 @@ export enum UserType {
PLEX = 1,
LOCAL = 2,
JELLYFIN = 3,
EMBY = 4,
}

View File

@@ -211,9 +211,10 @@ class Media {
}
} else {
const pageName =
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
getSettings().main.mediaServerType == MediaServerType.EMBY
? 'item'
: 'details';
const { serverId, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname

View File

@@ -291,7 +291,7 @@ class DiscordAgent
}
}
await fetch(settings.options.webhookUrl, {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -305,15 +305,25 @@ class DiscordAgent
content: userMentions.join(' '),
} as DiscordWebhookPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
return true;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Discord notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -132,22 +132,32 @@ class GotifyAgent
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
const notificationPayload = this.getNotificationPayload(type, payload);
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(notificationPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
return true;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Gotify notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -100,7 +100,7 @@ class LunaSeaAgent
});
try {
await fetch(settings.options.webhookUrl, {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: settings.options.profileName
? {
@@ -114,15 +114,25 @@ class LunaSeaAgent
},
body: JSON.stringify(this.buildPayload(type, payload)),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
return true;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -122,7 +122,7 @@ class PushbulletAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -133,13 +133,23 @@ class PushbulletAgent
channel_tag: settings.options.channelTag,
}),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Pushbullet notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;
@@ -164,7 +174,7 @@ class PushbulletAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -172,14 +182,24 @@ class PushbulletAgent
},
body: JSON.stringify(notificationPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Pushbullet notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;
@@ -215,7 +235,7 @@ class PushbulletAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -223,14 +243,24 @@ class PushbulletAgent
},
body: JSON.stringify(notificationPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Pushbullet notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -52,6 +52,9 @@ class PushoverAgent
): Promise<Partial<PushoverImagePayload>> {
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString('base64');
const contentType = (
@@ -64,10 +67,17 @@ class PushoverAgent
attachment_type: contentType,
};
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error getting image payload', {
label: 'Notifications',
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return {};
}
@@ -200,7 +210,7 @@ class PushoverAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -212,13 +222,23 @@ class PushoverAgent
sound: settings.options.sound,
} as PushoverPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Pushover notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;
@@ -246,7 +266,7 @@ class PushoverAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -258,14 +278,24 @@ class PushoverAgent
sound: payload.notifyUser.settings.pushoverSound,
} as PushoverPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Pushover notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;
@@ -302,7 +332,7 @@ class PushoverAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -313,14 +343,24 @@ class PushoverAgent
user: user.settings.pushoverUserKey,
} as PushoverPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Pushover notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -237,22 +237,32 @@ class SlackAgent
subject: payload.subject,
});
try {
await fetch(settings.options.webhookUrl, {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.buildEmbed(type, payload)),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
return true;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Slack notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -174,7 +174,7 @@ class TelegramAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -185,13 +185,23 @@ class TelegramAgent
disable_notification: !!settings.options.sendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Telegram notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;
@@ -215,7 +225,7 @@ class TelegramAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -227,14 +237,24 @@ class TelegramAgent
!!payload.notifyUser.settings.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Telegram notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;
@@ -268,7 +288,7 @@ class TelegramAgent
});
try {
await fetch(endpoint, {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -279,14 +299,24 @@ class TelegramAgent
disable_notification: !!user.settings?.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending Telegram notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -177,7 +177,7 @@ class WebhookAgent
});
try {
await fetch(settings.options.webhookUrl, {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -187,15 +187,25 @@ class WebhookAgent
},
body: JSON.stringify(this.buildPayload(type, payload)),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
return true;
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
logger.error('Error sending webhook notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
response: errorData,
});
return false;

View File

@@ -567,7 +567,10 @@ class JellyfinScanner {
public async run(): Promise<void> {
const settings = getSettings();
if (settings.main.mediaServerType != MediaServerType.JELLYFIN) {
if (
settings.main.mediaServerType != MediaServerType.JELLYFIN &&
settings.main.mediaServerType != MediaServerType.EMBY
) {
return;
}

View File

@@ -0,0 +1,17 @@
import { MediaServerType } from '@server/constants/server';
import type { AllSettings } from '@server/lib/settings';
const migrateHostname = (settings: any): AllSettings => {
const oldMediaServerType = settings.main.mediaServerType;
console.log('Migrating media server type', oldMediaServerType);
if (
oldMediaServerType === MediaServerType.JELLYFIN &&
process.env.JELLYFIN_TYPE === 'emby'
) {
settings.main.mediaServerType = MediaServerType.EMBY;
}
return settings;
};
export default migrateHostname;

View File

@@ -1,7 +1,7 @@
import JellyfinAPI from '@server/api/jellyfin';
import PlexTvAPI from '@server/api/plextv';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import { MediaServerType, ServerType } from '@server/constants/server';
import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
@@ -227,15 +227,20 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
urlBase?: string;
useSsl?: boolean;
email?: string;
serverType?: number;
};
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
//Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured
if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
settings.main.mediaServerType !== MediaServerType.EMBY &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED &&
settings.jellyfin.ip !== ''
) {
return res.status(500).json({ error: 'Jellyfin login is disabled' });
} else if (!body.username) {
}
if (!body.username) {
return res.status(500).json({ error: 'You must provide an username' });
} else if (settings.jellyfin.ip !== '' && body.hostname) {
return res
@@ -273,7 +278,8 @@ 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 jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
@@ -317,20 +323,47 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
);
// User doesn't exist, and there are no users in the database, we'll create the user
// with admin permission
settings.main.mediaServerType = MediaServerType.JELLYFIN;
user = new User({
email: body.email,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
userType: UserType.JELLYFIN,
});
// with admin permissions
switch (body.serverType) {
case MediaServerType.EMBY:
settings.main.mediaServerType = MediaServerType.EMBY;
user = new User({
email: body.email || account.User.Name,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
}),
userType: UserType.EMBY,
});
break;
case MediaServerType.JELLYFIN:
settings.main.mediaServerType = MediaServerType.JELLYFIN;
user = new User({
email: body.email || account.User.Name,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
}),
userType: UserType.JELLYFIN,
});
break;
default:
throw new Error('select_server_type');
}
const serverName = await jellyfinserver.getServerName();
@@ -350,12 +383,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
logger.info(
`Found matching ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby'
? ServerType.JELLYFIN
: ServerType.EMBY
} user; updating user with ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby'
? ServerType.JELLYFIN
: ServerType.EMBY
}`,
{
label: 'API',
@@ -371,7 +404,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
if (account.User.PrimaryImageTag) {
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else {
user.avatar = gravatarUrl(user.email, {
user.avatar = gravatarUrl(user.email || account.User.Name, {
default: 'mm',
size: 200,
});
@@ -413,10 +446,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
}
);
if (!body.email) {
throw new Error('add_email');
}
user = new User({
email: body.email,
jellyfinUsername: account.User.Name,
@@ -426,8 +455,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email, { default: 'mm', size: 200 }),
userType: UserType.JELLYFIN,
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
}),
userType:
settings.main.mediaServerType === MediaServerType.JELLYFIN
? UserType.JELLYFIN
: UserType.EMBY,
});
//initialize Jellyfin/Emby users with local login
const passedExplicitPassword = body.password && body.password.length > 0;

View File

@@ -41,7 +41,19 @@ router.get('/', async (req, res, next) => {
break;
case 'displayname':
query = query.orderBy(
"(CASE WHEN (user.username IS NULL OR user.username = '') THEN (CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN user.email ELSE LOWER(user.plexUsername) END) ELSE LOWER(user.username) END)",
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
user.email
ELSE
LOWER(user.jellyfinUsername)
END)
ELSE
LOWER(user.jellyfinUsername)
END)
ELSE
LOWER(user.username)
END`,
'ASC'
);
break;
@@ -90,12 +102,13 @@ router.post(
const settings = getSettings();
const body = req.body;
const email = body.email || body.username;
const userRepository = getRepository(User);
const existingUser = await userRepository
.createQueryBuilder('user')
.where('user.email = :email', {
email: body.email.toLowerCase(),
email: email.toLowerCase(),
})
.getOne();
@@ -108,7 +121,7 @@ router.post(
}
const passedExplicitPassword = body.password && body.password.length > 0;
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
const avatar = gravatarUrl(email, { default: 'mm', size: 200 });
if (
!passedExplicitPassword &&
@@ -118,9 +131,9 @@ router.post(
}
const user = new User({
email,
avatar: body.avatar ?? avatar,
username: body.username,
email: body.email,
password: body.password,
permissions: settings.main.defaultPermissions,
plexToken: '',

View File

@@ -98,7 +98,9 @@ userSettingsRoutes.post<
}
user.username = req.body.username;
user.email = req.body.email ?? user.email;
if (user.jellyfinUsername) {
user.email = req.body.email || user.jellyfinUsername || user.email;
}
// Update quota values only if the user has the correct permissions
if (

View File

@@ -7,9 +7,10 @@ type RateLimiteState<T extends (...args: Parameters<T>) => Promise<U>, U> = {
queue: {
args: Parameters<T>;
resolve: (value: U) => void;
reject: (reason?: unknown) => void;
}[];
activeRequests: number;
timer: NodeJS.Timeout | null;
lastTimestamps: number[];
timeout: ReturnType<typeof setTimeout>;
};
const rateLimitById: Record<string, unknown> = {};
@@ -27,46 +28,40 @@ export default function rateLimit<
>(fn: T, options: RateLimitOptions): (...args: Parameters<T>) => Promise<U> {
const state: RateLimiteState<T, U> = (rateLimitById[
options.id || ''
] as RateLimiteState<T, U>) || { queue: [], activeRequests: 0, timer: null };
] as RateLimiteState<T, U>) || { queue: [], lastTimestamps: [] };
if (options.id) {
rateLimitById[options.id] = state;
}
const processQueue = () => {
if (state.queue.length === 0) {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
return;
}
// remove old timestamps
state.lastTimestamps = state.lastTimestamps.filter(
(timestamp) => Date.now() - timestamp < 1000
);
while (state.activeRequests < options.maxRPS) {
state.activeRequests++;
if (state.lastTimestamps.length < options.maxRPS) {
// process requests if RPS not exceeded
const item = state.queue.shift();
if (!item) break;
const { args, resolve } = item;
if (!item) return;
state.lastTimestamps.push(Date.now());
const { args, resolve, reject } = item;
fn(...args)
.then(resolve)
.finally(() => {
state.activeRequests--;
if (state.queue.length > 0) {
if (!state.timer) {
state.timer = setInterval(processQueue, 1000);
}
} else {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
}
});
.catch(reject);
processQueue();
} else {
// rerun once the oldest item in queue is older than 1s
if (state.timeout) clearTimeout(state.timeout);
state.timeout = setTimeout(
processQueue,
1000 - (Date.now() - state.lastTimestamps[0])
);
}
};
return (...args: Parameters<T>): Promise<U> => {
return new Promise<U>((resolve) => {
state.queue.push({ args, resolve });
return new Promise<U>((resolve, reject) => {
state.queue.push({ args, resolve, reject });
processQueue();
});
};

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg2"
viewBox="0 0 712.60077 712.5481"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
id="rect249"
width="712.60077"
height="712.5481"
x="-0.00071160076"
y="2.0223413e-11" />
<rect
style="fill:#ffffff"
id="rect289"
width="230.18982"
height="229.82355"
x="241.20476"
y="241.36227" />
<g
id="layer1"
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
<path
id="path3427"
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
style="fill:#52b54b;fill-opacity:1;stroke:none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,46 +1,131 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg2"
viewBox="0 0 712.60077 712.5481"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
id="rect249"
width="712.60077"
height="712.5481"
x="-0.00071160076"
y="2.0223413e-11" />
<rect
style="fill:#ffffff"
id="rect289"
width="230.18982"
height="229.82355"
x="241.20476"
y="241.36227" />
<g
id="layer1"
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
<path
id="path3427"
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
style="fill:#52b54b;fill-opacity:1;stroke:none" />
</g>
</svg>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" width="100%" viewBox="0 0 617 188" enable-background="new 0 0 617 188" xml:space="preserve">
<path fill="#52B54B" opacity="1.000000" stroke="none" d="
M89.583336,1.000000
C90.189529,1.685005 90.166168,2.574803 90.599510,3.025271
C103.718315,16.662701 116.882103,30.256845 129.948212,43.764053
C130.577850,43.523941 130.916519,43.491173 131.111343,43.306595
C138.657471,36.157455 138.655273,36.156090 146.005478,43.505203
C159.538589,57.036308 173.016449,70.623535 186.654617,84.047913
C189.264145,86.616562 189.414017,88.253456 186.716782,90.895164
C174.709808,102.655037 162.893280,114.609337 151.008514,126.493958
C146.073502,131.428925 146.076691,131.427155 151.017944,136.523712
C151.698944,137.226120 152.340485,137.966812 153.259171,138.973434
C151.947098,140.380035 150.766312,141.712204 149.516266,142.975861
C134.544815,158.110641 119.563087,173.235260 104.792023,188.681274
C103.611107,189.000000 102.222221,189.000000 100.624634,188.681274
C86.361732,174.796494 72.307518,161.230438 57.702755,147.132965
C56.157101,149.136856 54.135899,151.757263 51.994804,154.533112
C35.932781,138.457108 20.569420,123.048477 5.141897,107.704361
C3.997114,106.565773 2.391420,105.890610 1.000000,105.000000
C1.000000,103.611107 1.000000,102.222221 1.318741,100.624641
C15.203506,86.361694 28.769531,72.307434 42.867004,57.702602
C40.863205,56.156994 38.242813,54.135792 35.425343,51.962570
C51.518696,35.908516 66.939468,20.557360 82.295547,5.141749
C83.434830,3.998048 84.109390,2.391417 85.000000,0.999999
C86.388893,1.000000 87.777779,1.000000 89.583336,1.000000
M73.196465,79.500702
C73.196465,96.254150 73.196465,113.007599 73.196465,130.872055
C94.273178,118.764557 114.417175,107.192863 135.221664,95.241745
C114.247169,83.251732 94.091187,71.729622 73.196594,59.785294
C73.196594,66.631348 73.196594,72.566254 73.196465,79.500702
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M618.000000,60.571537
C617.004395,62.042580 615.613281,62.912964 615.073181,64.153824
C608.143372,80.073746 601.328613,96.043816 594.498169,112.006920
C586.973572,129.592300 579.343018,147.133865 571.999390,164.794601
C568.632385,172.892075 568.893372,173.002594 560.133972,172.999832
C555.470825,172.998367 550.807617,172.994385 546.144592,172.969360
C545.841980,172.967712 545.540466,172.775543 544.836609,172.534256
C548.592896,163.531219 551.714905,154.222061 556.286133,145.689255
C559.733765,139.253830 559.138794,134.062668 556.454224,127.695969
C546.360352,103.757523 536.803345,79.592712 526.837830,55.000847
C534.817078,55.000847 542.437622,54.725182 550.003540,55.244331
C551.436218,55.342628 553.169678,58.412052 553.885010,60.423309
C558.720520,74.018005 563.307556,87.700912 568.003784,101.345413
C569.107483,104.551987 570.321045,107.720764 571.976196,112.255157
C573.889587,107.365631 575.415283,103.375916 577.007935,99.413109
C582.693298,85.266724 588.344238,71.105591 594.218018,57.037624
C594.650513,56.001743 596.734497,55.132927 598.079773,55.089733
C604.401855,54.886726 610.734131,54.999401 617.531372,54.999699
C618.000000,56.714359 618.000000,58.428715 618.000000,60.571537
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M430.000122,99.002235
C430.000122,112.477097 430.000122,125.452438 430.000122,138.713440
C423.048126,138.713440 416.308685,138.713440 408.999878,138.713440
C408.999878,129.350739 409.120758,119.916939 408.962219,110.487823
C408.832153,102.753624 409.088898,94.909142 407.866791,87.324188
C406.440887,78.474220 401.302399,74.201607 394.304291,74.000290
C387.617249,73.807938 380.317963,79.297188 378.047363,86.438652
C377.420715,88.409592 377.055725,90.550858 377.044647,92.616508
C376.962494,107.913475 377.000122,123.211082 377.000122,138.753479
C369.630646,138.753479 362.559692,138.753479 354.999878,138.753479
C354.999878,123.256836 355.044769,107.816956 354.977661,92.377571
C354.951050,86.251518 352.748199,80.799278 347.911346,77.066116
C339.239685,70.373154 327.811401,74.635170 324.084412,84.471092
C322.793915,87.876816 322.147491,91.713402 322.090881,95.366882
C321.868958,109.685005 322.000122,124.008591 322.000122,138.665009
C314.823853,138.665009 307.760773,138.665009 300.346558,138.665009
C300.346558,111.006645 300.346558,83.281189 300.346558,55.001301
C306.163818,55.001301 312.104645,54.855133 318.024780,55.139343
C319.060455,55.189068 320.450378,56.891682 320.882477,58.112110
C321.380768,59.519447 320.998291,61.238617 320.998291,64.136040
C328.715179,54.407440 338.407898,52.804527 348.408875,54.206123
C356.403381,55.326527 361.770447,57.638248 366.682190,66.544373
C372.325470,62.972542 377.601440,58.269657 383.771973,56.014080
C396.273407,51.444298 408.602570,53.673611 419.067657,61.818150
C426.629364,67.703125 429.037811,76.770744 429.932556,86.011482
C430.332214,90.138710 430.000122,94.336792 430.000122,99.002235
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M462.000427,35.006332
C462.000427,44.815434 462.000427,54.126144 462.000427,64.132019
C468.844696,58.319965 476.100769,54.654530 484.669739,53.656227
C496.686127,52.256294 507.565582,54.979622 516.927185,62.503853
C534.236755,76.416115 535.360107,106.231667 523.651062,123.341644
C516.745056,133.433182 506.539673,139.485458 493.555267,140.111023
C483.836304,140.579254 474.670624,139.889420 466.610413,133.799713
C465.039795,132.613068 463.390686,131.530289 461.957214,130.525391
C461.633789,132.375305 461.105469,135.397171 460.522095,138.733841
C454.446686,138.733841 448.017822,138.733841 441.292542,138.733841
C441.292542,99.722672 441.292542,60.652122 441.292542,21.290209
C447.943787,21.290209 454.684204,21.290209 462.000427,21.290209
C462.000427,25.636984 462.000427,30.072460 462.000427,35.006332
M480.890228,119.974937
C485.426086,119.681152 490.365997,120.444260 494.421356,118.893707
C506.182587,114.396866 510.858643,104.919495 509.036591,92.234833
C507.422546,80.997993 496.539307,71.772278 483.551605,73.864754
C469.724976,76.092384 464.376770,85.538391 463.152863,96.752327
C462.120667,106.209480 469.961761,116.189537 480.890228,119.974937
z" />
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
M234.797928,54.654831
C244.856339,52.605957 254.504562,52.040043 264.239868,54.923946
C279.600891,59.474377 286.402191,68.163963 289.768585,81.937614
C291.530579,89.146889 290.954620,96.927589 291.469940,105.005005
C269.550385,105.005005 248.375092,105.005005 227.094437,105.005005
C229.577957,116.288628 239.741562,120.764336 248.594757,121.034813
C256.790771,121.285217 264.390472,119.882645 271.081848,114.731178
C271.774902,114.197632 273.962708,114.659111 274.786041,115.402222
C278.726318,118.958458 282.435333,122.770882 286.509888,126.770363
C281.309174,132.968170 274.787445,135.946014 267.542938,138.175064
C253.746231,142.420120 240.209259,142.317459 227.237503,135.935410
C212.712891,128.789368 205.730453,116.523628 204.973831,100.473404
C204.537735,91.222557 205.503754,82.283119 210.008469,74.017265
C215.396210,64.131088 223.372589,57.511646 234.797928,54.654831
M266.971497,78.708908
C259.384399,70.789909 249.920425,70.480316 240.489548,73.410858
C234.405487,75.301414 229.437546,79.631561 227.800247,86.722244
C242.152313,86.722244 256.002747,86.722244 270.947815,86.722244
C269.410950,83.870155 268.228943,81.676651 266.971497,78.708908
z" />
<path fill="#FCFEFC" opacity="1.000000" stroke="none" d="
M73.196533,79.000931
C73.196594,72.566254 73.196594,66.631348 73.196594,59.785294
C94.091187,71.729622 114.247169,83.251732 135.221664,95.241745
C114.417175,107.192863 94.273178,118.764557 73.196465,130.872055
C73.196465,113.007599 73.196465,96.254150 73.196533,79.000931
z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -48,11 +48,11 @@ const DiscoverTvNetwork = () => {
<div className="mt-1 mb-5">
<Header>
{firstResultData?.network.logoPath ? (
<div className="mb-6 flex justify-center">
<div className="relative mb-6 flex h-24 justify-center sm:h-32">
<Image
src={`https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
alt={firstResultData.network.name}
className="max-h-24 sm:max-h-32"
className="object-contain"
fill
/>
</div>

View File

@@ -48,11 +48,11 @@ const DiscoverMovieStudio = () => {
<div className="mt-1 mb-5">
<Header>
{firstResultData?.studio.logoPath ? (
<div className="mb-6 flex justify-center">
<div className="relative mb-6 flex h-24 justify-center sm:h-32">
<Image
src={`https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
alt={firstResultData.studio.name}
className="max-h-24 sm:max-h-32"
className="object-contain"
fill
/>
</div>

View File

@@ -11,7 +11,6 @@ import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
interface ExternalLinkBlockProps {
mediaType: 'movie' | 'tv';
@@ -31,7 +30,6 @@ const ExternalLinkBlock = ({
mediaUrl,
}: ExternalLinkBlockProps) => {
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { locale } = useLocale();
return (
@@ -45,7 +43,8 @@ const ExternalLinkBlock = ({
>
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
<PlexLogo />
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
) : settings.currentSettings.mediaServerType ===
MediaServerType.EMBY ? (
<EmbyLogo />
) : (
<JellyfinLogo />

View File

@@ -28,7 +28,6 @@ import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -108,7 +107,6 @@ const IssueDetails = () => {
(opt) => opt.issueType === issueData?.issueType
);
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -390,7 +388,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
@@ -437,7 +436,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})
@@ -662,7 +662,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
@@ -708,7 +709,8 @@ const IssueDetails = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})

View File

@@ -90,9 +90,11 @@ const UserDropdown = () => {
<span className="truncate text-xl font-semibold text-gray-200">
{user?.displayName}
</span>
<span className="truncate text-sm text-gray-400">
{user?.email}
</span>
{user?.displayName?.toLowerCase() !== user?.email && (
<span className="truncate text-sm text-gray-400">
{user?.email}
</span>
)}
</div>
</div>
{user && <MiniQuotaDisplay userId={user?.id} />}

View File

@@ -4,9 +4,9 @@ import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType, ServerType } from '@server/constants/server';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import { useIntl } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup';
@@ -26,6 +26,7 @@ const messages = defineMessages('components.Login', {
validationemailformat: 'Valid email required',
validationusernamerequired: 'Username required',
validationpasswordrequired: 'Password required',
validationservertyperequired: 'Please select a server type',
validationHostnameRequired: 'You must provide a valid hostname or IP address',
validationPortRequired: 'You must provide a valid port number',
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
@@ -40,42 +41,51 @@ const messages = defineMessages('components.Login', {
initialsigningin: 'Connecting…',
initialsignin: 'Connect',
forgotpassword: 'Forgot Password?',
servertype: 'Server Type',
back: 'Go back',
});
interface JellyfinLoginProps {
revalidate: () => void;
initial?: boolean;
serverType?: MediaServerType;
onCancel?: () => void;
}
const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
revalidate,
initial,
serverType,
onCancel,
}) => {
const toasts = useToasts();
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const mediaServerFormatValues = {
mediaServerName:
serverType === MediaServerType.JELLYFIN
? ServerType.JELLYFIN
: serverType === MediaServerType.EMBY
? ServerType.EMBY
: 'Media Server',
};
if (initial) {
const LoginSchema = Yup.object().shape({
hostname: Yup.string().required(
intl.formatMessage(messages.validationhostrequired, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})
intl.formatMessage(
messages.validationhostrequired,
mediaServerFormatValues
)
),
port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired)
),
urlBase: Yup.string()
.matches(
/^(\/[^/].*[^/]$)/,
intl.formatMessage(messages.validationUrlBaseLeadingSlash)
)
.matches(
/^(.*[^/])$/,
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
),
urlBase: Yup.string().matches(
/^(.*[^/])$/,
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
),
email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)),
@@ -85,11 +95,6 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
password: Yup.string(),
});
const mediaServerFormatValues = {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
};
return (
<Formik
initialValues={{
@@ -104,6 +109,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
// Check if serverType is either 'Jellyfin' or 'Emby'
// if (serverType !== 'Jellyfin' && serverType !== 'Emby') {
// throw new Error('Invalid serverType'); // You can customize the error message
// }
const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
@@ -117,12 +127,20 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
useSsl: values.useSsl,
urlBase: values.urlBase,
email: values.email,
serverType: serverType,
}),
});
if (!res.ok) throw new Error();
if (!res.ok) throw new Error(res.statusText, { cause: res });
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
let errorMessage = null;
switch (e.response?.data?.message) {
switch (errorData?.message) {
case ApiErrorCode.InvalidUrl:
errorMessage = messages.invalidurlerror;
break;
@@ -305,7 +323,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-end">
<div className="flex flex-row-reverse justify-between">
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
@@ -317,6 +335,13 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
: intl.formatMessage(messages.signin)}
</Button>
</span>
{onCancel && (
<span className="inline-flex rounded-md shadow-sm">
<Button buttonType="default" onClick={() => onCancel()}>
<FormattedMessage {...messages.back} />
</Button>
</span>
)}
</div>
</div>
</Form>
@@ -422,7 +447,8 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
jellyfinForgotPasswordUrl
? `${jellyfinForgotPasswordUrl}`
: `${baseUrl}/web/index.html#!/${
process.env.JELLYFIN_TYPE === 'emby'
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'startup/'
: ''
}forgotpassword.html`

View File

@@ -10,7 +10,6 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { useRouter } from 'next/dist/client/router';
import Image from 'next/image';
import { useEffect, useState } from 'react';
@@ -34,7 +33,6 @@ const Login = () => {
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
@@ -50,14 +48,21 @@ const Login = () => {
},
body: JSON.stringify({ authToken }),
});
if (!res.ok) throw new Error();
if (!res.ok) throw new Error(res.statusText, { cause: res });
const data = await res.json();
if (data?.id) {
revalidate();
}
} catch (e) {
setError(e.response.data.message);
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
setError(errorData?.message);
setAuthToken(undefined);
setProcessing(false);
}
@@ -81,6 +86,15 @@ const Login = () => {
revalidateOnFocus: false,
});
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return (
<div className="relative flex min-h-screen flex-col bg-gray-900 py-14">
<PageTitle title={intl.formatMessage(messages.signin)} />
@@ -147,12 +161,10 @@ const Login = () => {
{settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(messages.signinwithjellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
: intl.formatMessage(
messages.signinwithjellyfin,
mediaServerFormatValues
)}
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8">

View File

@@ -26,7 +26,6 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useIntl } from 'react-intl';
@@ -95,7 +94,6 @@ const ManageSlideOver = ({
const { user: currentUser, hasPermission } = useUser();
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { data: watchData } = useSWR<MediaWatchDataResponse>(
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
data.mediaInfo &&
@@ -661,7 +659,8 @@ const ManageSlideOver = ({
mediaType === 'movie' ? messages.movie : messages.tvshow
),
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX

View File

@@ -50,7 +50,6 @@ import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash';
import getConfig from 'next/config';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
@@ -112,7 +111,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const minStudios = 3;
const [showMoreStudios, setShowMoreStudios] = useState(false);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -264,7 +262,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
?.flatrate ?? [];
function getAvalaibleMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
@@ -276,8 +274,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
}
function getAvalaible4kMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' });
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {

View File

@@ -2,13 +2,14 @@ import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import LibraryItem from '@app/components/Settings/LibraryItem';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import type { JellyfinSettings } from '@server/lib/settings';
import { Field, Formik } from 'formik';
import getConfig from 'next/config';
import { useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -100,7 +101,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
);
const intl = useIntl();
const { addToast } = useToasts();
const { publicRuntimeConfig } = getConfig();
const settings = useSettings();
const JellyfinSettingsSchema = Yup.object().shape({
hostname: Yup.string()
@@ -173,11 +174,18 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
const res = await fetch(
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error();
if (!res.ok) throw new Error(res.statusText, { cause: res });
setIsSyncing(false);
revalidate();
} catch (e) {
if (e.response.data.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
if (errorData?.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
toasts.addToast(
intl.formatMessage(
messages.jellyfinSyncFailedAutomaticGroupedFolders
@@ -187,7 +195,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
appearance: 'warning',
}
);
} else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') {
} else if (errorData?.message === 'SYNC_ERROR_NO_LIBRARIES') {
toasts.addToast(
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
{
@@ -275,26 +283,29 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
return <LoadingSpinner />;
}
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return (
<>
<div className="mb-6">
<h3 className="heading">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Jellyfin',
})}
{intl.formatMessage(
messages.jellyfinlibraries,
mediaServerFormatValues
)}
</h3>
<p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Jellyfin',
})}
{intl.formatMessage(
messages.jellyfinlibrariesDescription,
mediaServerFormatValues
)}
</p>
</div>
<div className="section">
@@ -331,13 +342,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<FormattedMessage {...messages.manualscanJellyfin} />
</h3>
<p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Jellyfin',
})}
{intl.formatMessage(
messages.manualscanDescriptionJellyfin,
mediaServerFormatValues
)}
</p>
</div>
<div className="section">
@@ -441,22 +449,16 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<>
<div className="mt-10 mb-6">
<h3 className="heading">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Jellyfin',
})}
{intl.formatMessage(
messages.jellyfinSettings,
mediaServerFormatValues
)}
</h3>
<p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Jellyfin',
})}
{intl.formatMessage(
messages.jellyfinSettingsDescription,
mediaServerFormatValues
)}
</p>
</div>
<Formik
@@ -485,12 +487,13 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
} as JellyfinSettings),
});
if (!res.ok) throw new Error();
if (!res.ok) throw new Error(res.statusText, { cause: res });
addToast(
intl.formatMessage(messages.jellyfinSettingsSuccess, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}),
@@ -500,14 +503,19 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
}
);
} catch (e) {
if (e.response?.data?.message === ApiErrorCode.InvalidUrl) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
if (errorData?.message === ApiErrorCode.InvalidUrl) {
addToast(
intl.formatMessage(messages.invalidurlerror, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
}),
intl.formatMessage(
messages.invalidurlerror,
mediaServerFormatValues
),
{
autoDismiss: true,
appearance: 'error',
@@ -515,12 +523,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
);
} else {
addToast(
intl.formatMessage(messages.jellyfinSettingsFailure, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
}),
intl.formatMessage(
messages.jellyfinSettingsFailure,
mediaServerFormatValues
),
{
autoDismiss: true,
appearance: 'error',

View File

@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import Table from '@app/components/Common/Table';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { formatBytes } from '@app/utils/numberHelpers';
@@ -57,8 +58,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
'plex-recently-added-scan': 'Plex Recently Added Scan',
'plex-full-scan': 'Plex Full Library Scan',
'plex-watchlist-sync': 'Plex Watchlist Sync',
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
'jellyfin-full-scan': 'Jellyfin Full Library Scan',
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
'availability-sync': 'Media Availability Sync',
'radarr-scan': 'Radarr Scan',
'sonarr-scan': 'Sonarr Scan',
@@ -167,6 +168,20 @@ const SettingsJobs = () => {
const [isSaving, setIsSaving] = useState(false);
const settings = useSettings();
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
messages['jellyfin-recently-added-scan'] = {
id: 'jellyfin-recently-added-scan',
defaultMessage: 'Emby Recently Added Scan',
};
}
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
messages['jellyfin-full-scan'] = {
id: 'jellyfin-full-scan',
defaultMessage: 'Emby Full Library Scan',
};
}
if (!data && !error) {
return <LoadingSpinner />;
}

View File

@@ -5,7 +5,6 @@ import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { useIntl } from 'react-intl';
const messages = defineMessages('components.Settings', {
@@ -26,7 +25,6 @@ type SettingsLayoutProps = {
const SettingsLayout = ({ children }: SettingsLayoutProps) => {
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const settings = useSettings();
const settingsRoutes: SettingsRoute[] = [
{
@@ -89,7 +87,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
function getAvailableMediaServerName() {
return intl.formatMessage(messages.menuJellyfinSettings, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
});
}
};

View File

@@ -10,7 +10,6 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { MediaServerType } from '@server/constants/server';
import type { MainSettings } from '@server/lib/settings';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
@@ -42,12 +41,20 @@ const SettingsUsers = () => {
mutate: revalidate,
} = useSWR<MainSettings>('/api/v1/settings/main');
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
}
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return (
<>
<PageTitle
@@ -121,16 +128,10 @@ const SettingsUsers = () => {
<label htmlFor="localLogin" className="checkbox-label">
{intl.formatMessage(messages.localLogin)}
<span className="label-tip">
{intl.formatMessage(messages.localLoginTip, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: settings.currentSettings.mediaServerType ===
MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby',
})}
{intl.formatMessage(
messages.localLoginTip,
mediaServerFormatValues
)}
</span>
</label>
<div className="form-input-area">
@@ -146,25 +147,15 @@ const SettingsUsers = () => {
</div>
<div className="form-row">
<label htmlFor="newPlexLogin" className="checkbox-label">
{intl.formatMessage(messages.newPlexLogin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
{intl.formatMessage(
messages.newPlexLogin,
mediaServerFormatValues
)}
<span className="label-tip">
{intl.formatMessage(messages.newPlexLoginTip, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
{intl.formatMessage(
messages.newPlexLoginTip,
mediaServerFormatValues
)}
</span>
</label>
<div className="form-input-area">

View File

@@ -1,32 +1,39 @@
import Accordion from '@app/components/Common/Accordion';
import Button from '@app/components/Common/Button';
import JellyfinLogin from '@app/components/Login/JellyfinLogin';
import PlexLoginButton from '@app/components/PlexLoginButton';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import { useEffect, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { FormattedMessage } from 'react-intl';
const messages = defineMessages('components.Setup', {
welcome: 'Welcome to Jellyseerr',
signinMessage: 'Get started by signing in',
signinWithJellyfin: 'Use your {mediaServerName} account',
signinWithPlex: 'Use your Plex account',
signin: 'Sign in to your account',
signinWithJellyfin: 'Enter your Jellyfin details',
signinWithEmby: 'Enter your Emby details',
signinWithPlex: 'Enter your Plex details',
back: 'Go back',
});
interface LoginWithMediaServerProps {
onComplete: (onComplete: MediaServerType) => void;
serverType: MediaServerType;
onCancel: () => void;
onComplete: () => void;
}
const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
const SetupLogin: React.FC<LoginWithMediaServerProps> = ({
serverType,
onCancel,
onComplete,
}) => {
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
const [mediaServerType, setMediaServerType] = useState<MediaServerType>(
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.
@@ -56,71 +63,60 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
useEffect(() => {
if (user) {
onComplete(mediaServerType);
onComplete();
}
}, [user, mediaServerType, onComplete]);
return (
<div>
<div className="p-4">
<div className="mb-2 flex justify-center text-xl font-bold">
<FormattedMessage {...messages.welcome} />
<FormattedMessage {...messages.signin} />
</div>
<div className="mb-2 flex justify-center pb-6 text-sm">
<FormattedMessage {...messages.signinMessage} />
</div>
<Accordion single atLeastOne>
{({ openIndexes, handleClick, AccordionContent }) => (
<>
<button
className={`w-full cursor-default bg-gray-900 py-2 text-center text-sm text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none sm:rounded-t-lg ${
openIndexes.includes(0) && 'text-indigo-500'
} ${openIndexes.includes(1) && 'border-b border-gray-500'}`}
onClick={() => handleClick(0)}
>
<FormattedMessage {...messages.signinWithPlex} />
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div
className="px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<PlexLoginButton
onAuthToken={(authToken) => {
setMediaServerType(MediaServerType.PLEX);
setAuthToken(authToken);
}}
/>
</div>
</AccordionContent>
<div>
<button
className={`w-full cursor-default bg-gray-900 py-2 text-center text-sm text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none ${
openIndexes.includes(1)
? 'text-indigo-500'
: 'sm:rounded-b-lg'
}`}
onClick={() => handleClick(1)}
>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Jellyfin',
})}
</button>
<AccordionContent isOpen={openIndexes.includes(1)}>
<div
className="rounded-b-lg px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<JellyfinLogin initial={true} revalidate={revalidate} />
</div>
</AccordionContent>
</div>
</>
{serverType === MediaServerType.JELLYFIN ? (
<FormattedMessage {...messages.signinWithJellyfin} />
) : serverType === MediaServerType.EMBY ? (
<FormattedMessage {...messages.signinWithEmby} />
) : (
<FormattedMessage {...messages.signinWithPlex} />
)}
</Accordion>
</div>
{serverType === MediaServerType.PLEX && (
<>
<div
className="px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<PlexLoginButton
onAuthToken={(authToken) => {
setMediaServerType(MediaServerType.PLEX);
setAuthToken(authToken);
}}
/>
</div>
<div className="mt-4">
<Button buttonType="default" onClick={() => onCancel()}>
<FormattedMessage {...messages.back} />
</Button>
</div>
</>
)}
{serverType === MediaServerType.JELLYFIN && (
<JellyfinLogin
initial={true}
revalidate={revalidate}
serverType={serverType}
onCancel={onCancel}
/>
)}
{serverType === MediaServerType.EMBY && (
<JellyfinLogin
initial={true}
revalidate={revalidate}
serverType={serverType}
onCancel={onCancel}
/>
)}
</div>
);
};

View File

@@ -1,3 +1,6 @@
import EmbyLogo from '@app/assets/services/emby.svg';
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import AppDataWarning from '@app/components/AppDataWarning';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
@@ -19,10 +22,16 @@ import useSWR, { mutate } from 'swr';
import SetupLogin from './SetupLogin';
const messages = defineMessages('components.Setup', {
welcome: 'Welcome to Jellyseerr',
subtitle: 'Get started by choosing your media server',
configjellyfin: 'Configure Jellyfin',
configplex: 'Configure Plex',
configemby: 'Configure Emby',
setup: 'Setup',
finish: 'Finish Setup',
finishing: 'Finishing…',
continue: 'Continue',
servertype: 'Choose Server Type',
signin: 'Sign In',
configuremediaserver: 'Configure Media Server',
configureservices: 'Configure Services',
@@ -101,35 +110,103 @@ const Setup = () => {
>
<SetupSteps
stepNumber={1}
description={intl.formatMessage(messages.signin)}
description={intl.formatMessage(messages.servertype)}
active={currentStep === 1}
completed={currentStep > 1}
/>
<SetupSteps
stepNumber={2}
description={intl.formatMessage(messages.configuremediaserver)}
description={intl.formatMessage(messages.signin)}
active={currentStep === 2}
completed={currentStep > 2}
/>
<SetupSteps
stepNumber={3}
description={intl.formatMessage(messages.configureservices)}
description={intl.formatMessage(messages.configuremediaserver)}
active={currentStep === 3}
completed={currentStep > 3}
/>
<SetupSteps
stepNumber={4}
description={intl.formatMessage(messages.configureservices)}
active={currentStep === 4}
isLastStep
/>
</ul>
</nav>
<div className="mt-10 w-full rounded-md border border-gray-600 bg-gray-800 bg-opacity-50 p-4 text-white">
{currentStep === 1 && (
<SetupLogin
onComplete={(mServerType) => {
setMediaServerType(mServerType);
setCurrentStep(2);
}}
/>
<div className="flex flex-col items-center pb-6">
<div className="mb-2 flex justify-center text-xl font-bold">
{intl.formatMessage(messages.welcome)}
</div>
<div className="mb-2 flex justify-center pb-6 text-sm">
{intl.formatMessage(messages.subtitle)}
</div>
<div className="grid grid-cols-3">
<div className="flex flex-col divide-y divide-gray-600 rounded-l border border-gray-600 py-2">
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
<JellyfinLogo className="h-10" />
</div>
<div className="px-2 pt-2">
<button
onClick={() => {
setMediaServerType(MediaServerType.JELLYFIN);
setCurrentStep(2);
}}
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
>
{intl.formatMessage(messages.configjellyfin)}
</button>
</div>
</div>
<div className="flex flex-col divide-y divide-gray-600 border-y border-gray-600 py-2">
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
<PlexLogo className="h-8" />
</div>
<div className="px-2 pt-2">
<button
onClick={() => {
setMediaServerType(MediaServerType.PLEX);
setCurrentStep(2);
}}
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
>
{intl.formatMessage(messages.configplex)}
</button>
</div>
</div>
<div className="flex flex-col divide-y divide-gray-600 rounded-r border border-gray-600 py-2">
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
<EmbyLogo className="h-9" />
</div>
<div className="px-2 pt-2">
<button
onClick={() => {
setMediaServerType(MediaServerType.EMBY);
setCurrentStep(2);
}}
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
>
{intl.formatMessage(messages.configemby)}
</button>
</div>
</div>
</div>
</div>
)}
{currentStep === 2 && (
<div>
<SetupLogin
serverType={mediaServerType}
onCancel={() => {
setMediaServerType(MediaServerType.NOT_CONFIGURED);
setCurrentStep(1);
}}
onComplete={() => setCurrentStep(3)}
/>
)}
{currentStep === 3 && (
<div className="p-2">
{mediaServerType === MediaServerType.PLEX ? (
<SettingsPlex
onComplete={() => setMediaServerSettingsComplete(true)}
@@ -152,7 +229,7 @@ const Setup = () => {
<Button
buttonType="primary"
disabled={!mediaServerSettingsComplete}
onClick={() => setCurrentStep(3)}
onClick={() => setCurrentStep(4)}
>
{intl.formatMessage(messages.continue)}
</Button>
@@ -161,7 +238,7 @@ const Setup = () => {
</div>
</div>
)}
{currentStep === 3 && (
{currentStep === 4 && (
<div>
<SettingsServices />
<div className="actions">

View File

@@ -9,7 +9,6 @@ import defineMessages from '@app/utils/defineMessages';
import { MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import type { DownloadingItem } from '@server/lib/downloadtracker';
import getConfig from 'next/config';
import { useIntl } from 'react-intl';
const messages = defineMessages('components.StatusBadge', {
@@ -47,7 +46,6 @@ const StatusBadge = ({
const intl = useIntl();
const { hasPermission } = useUser();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
let mediaLink: string | undefined;
let mediaLinkDescription: string | undefined;
@@ -85,7 +83,7 @@ const StatusBadge = ({
mediaLink = plexUrl;
mediaLinkDescription = intl.formatMessage(messages.playonplex, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: settings.currentSettings.mediaServerType === MediaServerType.PLEX
? 'Plex'

View File

@@ -50,7 +50,6 @@ import type { Crew } from '@server/models/common';
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
import getConfig from 'next/config';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
@@ -106,7 +105,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
router.query.manage == '1' ? true : false
);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -279,7 +277,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
?.flatrate ?? [];
function getAvalaibleMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
@@ -291,15 +289,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
}
function getAvalaible4kMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' });
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' });
}
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
}
return (

View File

@@ -3,8 +3,8 @@ import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import getConfig from 'next/config';
import Image from 'next/image';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -36,7 +36,6 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
}) => {
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const [isImporting, setImporting] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -78,7 +77,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
}),
});
if (!res.ok) throw new Error();
const { data: createdUsers } = await res.json();
const createdUsers = await res.json();
if (!createdUsers.length) {
throw new Error('No users were imported from Jellyfin.');
@@ -89,7 +88,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
userCount: createdUsers.length,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
@@ -104,7 +105,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
addToast(
intl.formatMessage(messages.importfromJellyfinerror, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
@@ -142,7 +145,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
loading={!data && !error}
title={intl.formatMessage(messages.importfromJellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
})}
onOk={() => {
importUsers();
@@ -159,7 +164,8 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<Alert
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
strong: (msg: React.ReactNode) => (
@@ -278,7 +284,9 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<Alert
title={intl.formatMessage(messages.noJellyfinuserstoimport, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin',
})}
type="info"
/>

View File

@@ -28,7 +28,6 @@ import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -68,14 +67,15 @@ const messages = defineMessages('components.UserList', {
usercreatedfailedexisting:
'The provided email address is already in use by another user.',
usercreatedsuccess: 'User created successfully!',
displayName: 'Display Name',
username: 'Username',
email: 'Email Address',
password: 'Password',
passwordinfodescription:
'Configure an application URL and enable email notifications to allow automatic password generation.',
autogeneratepassword: 'Automatically Generate Password',
autogeneratepasswordTip: 'Email a server-generated password to the user',
validationEmail: 'You must provide a valid email address',
validationUsername: 'You must provide an username',
validationEmail: 'Email required',
sortCreated: 'Join Date',
sortDisplayName: 'Display Name',
sortRequests: 'Request Count',
@@ -89,7 +89,6 @@ const UserList = () => {
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');
@@ -208,9 +207,10 @@ const UserList = () => {
}
const CreateUserSchema = Yup.object().shape({
email: Yup.string()
.required(intl.formatMessage(messages.validationEmail))
.email(intl.formatMessage(messages.validationEmail)),
username: Yup.string().required(
intl.formatMessage(messages.validationUsername)
),
email: Yup.string().email(intl.formatMessage(messages.validationEmail)),
password: Yup.lazy((value) =>
!value
? Yup.string()
@@ -258,7 +258,7 @@ const UserList = () => {
setDeleteModal({ isOpen: false, user: deleteModal.user })
}
title={intl.formatMessage(messages.deleteuser)}
subTitle={deleteModal.user?.displayName}
subTitle={deleteModal.user?.username}
>
{intl.formatMessage(messages.deleteconfirm)}
</Modal>
@@ -276,7 +276,7 @@ const UserList = () => {
>
<Formik
initialValues={{
displayName: '',
username: '',
email: '',
password: '',
genpassword: false,
@@ -290,21 +290,28 @@ const UserList = () => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.displayName,
username: values.username,
email: values.email,
password: values.genpassword ? null : values.password,
}),
});
if (!res.ok) throw new Error();
if (!res.ok) throw new Error(res.statusText, { cause: res });
addToast(intl.formatMessage(messages.usercreatedsuccess), {
appearance: 'success',
autoDismiss: true,
});
setCreateModal({ isOpen: false });
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
addToast(
intl.formatMessage(
e.response.data.errors?.includes('USER_EXISTS')
errorData.errors?.includes('USER_EXISTS')
? messages.usercreatedfailedexisting
: messages.usercreatedfailed
),
@@ -363,23 +370,24 @@ const UserList = () => {
)}
<Form className="section">
<div className="form-row">
<label htmlFor="displayName" className="text-label">
{intl.formatMessage(messages.displayName)}
<label htmlFor="username" className="text-label">
{intl.formatMessage(messages.username)}
<span className="label-required">*</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="displayName"
name="displayName"
type="text"
/>
<Field id="username" name="username" type="text" />
</div>
{errors.username &&
touched.username &&
typeof errors.username === 'string' && (
<div className="error">{errors.username}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
<span className="label-required">*</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
@@ -525,7 +533,8 @@ const UserList = () => {
>
<InboxArrowDownIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
{settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Emby',
})
@@ -638,9 +647,16 @@ const UserList = () => {
className="text-base font-bold leading-5 transition duration-300 hover:underline"
data-testid="user-list-username-link"
>
{user.displayName}
{user.username ||
user.jellyfinUsername ||
user.plexUsername ||
user.email}
</Link>
{user.displayName.toLowerCase() !== user.email && (
{(
user.username ||
user.jellyfinUsername ||
user.plexUsername
)?.toLowerCase() !== user.email && (
<div className="text-sm leading-5 text-gray-300">
{user.email}
</div>
@@ -673,7 +689,7 @@ const UserList = () => {
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
) : user.userType === UserType.EMBY ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Emby',

View File

@@ -16,7 +16,6 @@ import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -69,7 +68,6 @@ const messages = defineMessages(
const UserGeneralSettings = () => {
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const { locale, setLocale } = useLocale();
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
@@ -93,9 +91,14 @@ const UserGeneralSettings = () => {
);
const UserGeneralSettingsSchema = Yup.object().shape({
email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)),
email:
user?.id === 1
? Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired))
: Yup.string().email(
intl.formatMessage(messages.validationemailformat)
),
discordId: Yup.string()
.nullable()
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
@@ -134,7 +137,7 @@ const UserGeneralSettings = () => {
<Formik
initialValues={{
displayName: data?.username ?? '',
email: data?.email ?? '',
email: data?.email?.includes('@') ? data.email : '',
discordId: data?.discordId ?? '',
locale: data?.locale,
region: data?.region,
@@ -157,7 +160,8 @@ const UserGeneralSettings = () => {
},
body: JSON.stringify({
username: values.displayName,
email: values.email,
email:
values.email || user?.jellyfinUsername || user?.plexUsername,
discordId: values.discordId,
locale: values.locale,
region: values.region,
@@ -223,7 +227,7 @@ const UserGeneralSettings = () => {
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
) : user?.userType === UserType.EMBY ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Emby',
@@ -264,7 +268,9 @@ const UserGeneralSettings = () => {
name="displayName"
type="text"
placeholder={
user?.plexUsername ? user.plexUsername : user?.email
user?.username ||
user?.jellyfinUsername ||
user?.plexUsername
}
/>
</div>
@@ -289,6 +295,7 @@ const UserGeneralSettings = () => {
name="email"
type="text"
placeholder="example@domain.com"
disabled={user?.plexUsername}
className={
user?.warnings.find((w) => w === 'userEmailRequired')
? 'border-2 border-red-400 focus:border-blue-600'

View File

@@ -12,6 +12,7 @@ export interface User {
id: number;
warnings: string[];
plexUsername?: string;
jellyfinUsername?: string;
username?: string;
displayName: string;
email: string;

View File

@@ -220,6 +220,7 @@
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
"components.Login.adminerror": "You must use an admin account to sign in.",
"components.Login.back": "Go back",
"components.Login.credentialerror": "The username or password is incorrect.",
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
"components.Login.email": "Email Address",
@@ -235,6 +236,7 @@
"components.Login.port": "Port",
"components.Login.save": "Add",
"components.Login.saving": "Adding…",
"components.Login.servertype": "Server Type",
"components.Login.signin": "Sign In",
"components.Login.signingin": "Signing In…",
"components.Login.signinheader": "Sign in to continue",
@@ -256,6 +258,7 @@
"components.Login.validationhostformat": "Valid URL required",
"components.Login.validationhostrequired": "{mediaServerName} URL required",
"components.Login.validationpasswordrequired": "You must provide a password",
"components.Login.validationservertyperequired": "Please select a server type",
"components.Login.validationusernamerequired": "Username required",
"components.ManageSlideOver.alltime": "All Time",
"components.ManageSlideOver.downloadstatus": "Downloads",
@@ -1033,17 +1036,24 @@
"components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app",
"components.Settings.webhook": "Webhook",
"components.Settings.webpush": "Web Push",
"components.Setup.back": "Go back",
"components.Setup.configemby": "Configure Emby",
"components.Setup.configjellyfin": "Configure Jellyfin",
"components.Setup.configplex": "Configure Plex",
"components.Setup.configuremediaserver": "Configure Media Server",
"components.Setup.configureservices": "Configure Services",
"components.Setup.continue": "Continue",
"components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing…",
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithJellyfin": "Use your {mediaServerName} account",
"components.Setup.signinWithPlex": "Use your Plex account",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
"components.Setup.signinWithPlex": "Enter your Plex details",
"components.Setup.subtitle": "Get started by choosing your media server",
"components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Jellyseerr",
"components.StatusBadge.managemedia": "Manage {mediaType}",
@@ -1111,7 +1121,6 @@
"components.UserList.creating": "Creating…",
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All of their request data will be permanently removed.",
"components.UserList.deleteuser": "Delete User",
"components.UserList.displayName": "Display Name",
"components.UserList.edituser": "Edit User Permissions",
"components.UserList.email": "Email Address",
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
@@ -1145,9 +1154,11 @@
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
"components.UserList.userfail": "Something went wrong while saving user permissions.",
"components.UserList.userlist": "User List",
"components.UserList.username": "Username",
"components.UserList.users": "Users",
"components.UserList.userssaved": "User permissions saved successfully!",
"components.UserList.validationEmail": "You must provide a valid email address",
"components.UserList.validationEmail": "Email required",
"components.UserList.validationUsername": "You must provide an username",
"components.UserList.validationpasswordminchars": "Password is too short; should be a minimum of 8 characters",
"components.UserProfile.ProfileHeader.joindate": "Joined {joindate}",
"components.UserProfile.ProfileHeader.profile": "View Profile",

View File

@@ -83,6 +83,16 @@
background: #f19a30;
}
.server-type-button {
@apply rounded-md border border-gray-500 bg-gray-700 px-4 py-2 text-white transition duration-150 ease-in-out hover:bg-gray-500;
}
.jellyfin-server svg {
@apply h-6 w-6;
}
.emby-server svg {
@apply h-7 w-7;
}
ul.cards-vertical,
ul.cards-horizontal {
@apply grid gap-4;