Compare commits
11 Commits
v2.2.2
...
preview-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a061a66946 | ||
|
|
81d7473c05 | ||
|
|
f718cec23f | ||
|
|
ac908026db | ||
|
|
d67ec571c5 | ||
|
|
f3ebf6028b | ||
|
|
465d42dd60 | ||
|
|
2f0e493257 | ||
|
|
ebe7d11a53 | ||
|
|
7e94ad7210 | ||
|
|
814a7357c0 |
@@ -15,7 +15,7 @@
|
|||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
||||||
It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
|
It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring additional support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
|
||||||
|
|
||||||
## Current Features
|
## Current Features
|
||||||
|
|
||||||
|
|||||||
@@ -293,6 +293,14 @@ class ExternalAPI {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected removeCache(endpoint: string, params?: Record<string, string>) {
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
...this.params,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
this.cache?.del(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
private formatUrl(
|
private formatUrl(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
params?: Record<string, string>,
|
params?: Record<string, string>,
|
||||||
|
|||||||
@@ -230,6 +230,23 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public clearCache = ({
|
||||||
|
tmdbId,
|
||||||
|
externalId,
|
||||||
|
}: {
|
||||||
|
tmdbId?: number | null;
|
||||||
|
externalId?: number | null;
|
||||||
|
}) => {
|
||||||
|
if (tmdbId) {
|
||||||
|
this.removeCache('/movie/lookup', {
|
||||||
|
term: `tmdb:${tmdbId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (externalId) {
|
||||||
|
this.removeCache(`/movie/${externalId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RadarrAPI;
|
export default RadarrAPI;
|
||||||
|
|||||||
@@ -353,6 +353,30 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public clearCache = ({
|
||||||
|
tvdbId,
|
||||||
|
externalId,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
tvdbId?: number | null;
|
||||||
|
externalId?: number | null;
|
||||||
|
title?: string | null;
|
||||||
|
}) => {
|
||||||
|
if (tvdbId) {
|
||||||
|
this.removeCache('/series/lookup', {
|
||||||
|
term: `tvdb:${tvdbId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (externalId) {
|
||||||
|
this.removeCache(`/series/${externalId}`);
|
||||||
|
}
|
||||||
|
if (title) {
|
||||||
|
this.removeCache('/series/lookup', {
|
||||||
|
term: title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SonarrAPI;
|
export default SonarrAPI;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export enum ApiErrorCode {
|
|||||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||||
InvalidEmail = 'INVALID_EMAIL',
|
InvalidEmail = 'INVALID_EMAIL',
|
||||||
NotAdmin = 'NOT_ADMIN',
|
NotAdmin = 'NOT_ADMIN',
|
||||||
|
NoAdminUser = 'NO_ADMIN_USER',
|
||||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||||
Unknown = 'UNKNOWN',
|
Unknown = 'UNKNOWN',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
|
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
@@ -207,28 +208,50 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply overrides if the user is not an admin or has the "auto approve" permission
|
// Apply overrides if the user is not an admin or has the "advanced request" permission
|
||||||
const useOverrides = !user.hasPermission(
|
const useOverrides = !user.hasPermission([Permission.MANAGE_REQUESTS], {
|
||||||
[
|
type: 'or',
|
||||||
requestBody.is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE,
|
});
|
||||||
Permission.MANAGE_REQUESTS,
|
|
||||||
],
|
|
||||||
{ type: 'or' }
|
|
||||||
);
|
|
||||||
|
|
||||||
let rootFolder = requestBody.rootFolder;
|
let rootFolder = requestBody.rootFolder;
|
||||||
let profileId = requestBody.profileId;
|
let profileId = requestBody.profileId;
|
||||||
let tags = requestBody.tags;
|
let tags = requestBody.tags;
|
||||||
|
|
||||||
if (useOverrides) {
|
if (useOverrides) {
|
||||||
|
const defaultRadarrId = requestBody.is4k
|
||||||
|
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||||
|
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||||
|
const defaultSonarrId = requestBody.is4k
|
||||||
|
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
|
||||||
|
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
|
||||||
|
|
||||||
const overrideRuleRepository = getRepository(OverrideRule);
|
const overrideRuleRepository = getRepository(OverrideRule);
|
||||||
const overrideRules = await overrideRuleRepository.find({
|
const overrideRules = await overrideRuleRepository.find({
|
||||||
where:
|
where:
|
||||||
requestBody.mediaType === MediaType.MOVIE
|
requestBody.mediaType === MediaType.MOVIE
|
||||||
? { radarrServiceId: requestBody.serverId }
|
? { radarrServiceId: defaultRadarrId }
|
||||||
: { sonarrServiceId: requestBody.serverId },
|
: { sonarrServiceId: defaultSonarrId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const appliedOverrideRules = overrideRules.filter((rule) => {
|
const appliedOverrideRules = overrideRules.filter((rule) => {
|
||||||
|
const hasAnimeKeyword =
|
||||||
|
'results' in tmdbMedia.keywords &&
|
||||||
|
tmdbMedia.keywords.results.some(
|
||||||
|
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip override rules if the media is an anime TV show as anime TV
|
||||||
|
// is handled by default and override rules do not explicitly include
|
||||||
|
// the anime keyword
|
||||||
|
if (
|
||||||
|
requestBody.mediaType === MediaType.TV &&
|
||||||
|
hasAnimeKeyword &&
|
||||||
|
(!rule.keywords ||
|
||||||
|
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
rule.users &&
|
rule.users &&
|
||||||
!rule.users
|
!rule.users
|
||||||
@@ -257,31 +280,59 @@ export class MediaRequest {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
rule.keywords &&
|
||||||
|
!rule.keywords.split(',').some((keywordId) => {
|
||||||
|
let keywordList: TmdbKeyword[] = [];
|
||||||
|
|
||||||
|
if ('keywords' in tmdbMedia.keywords) {
|
||||||
|
keywordList = tmdbMedia.keywords.keywords;
|
||||||
|
} else if ('results' in tmdbMedia.keywords) {
|
||||||
|
keywordList = tmdbMedia.keywords.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keywordList
|
||||||
|
.map((keyword: TmdbKeyword) => keyword.id)
|
||||||
|
.includes(Number(keywordId));
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const overrideRootFolder = appliedOverrideRules.find(
|
// hacky way to prioritize rules
|
||||||
(rule) => rule.rootFolder
|
// TODO: make this better
|
||||||
)?.rootFolder;
|
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
|
||||||
if (overrideRootFolder) {
|
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
|
||||||
rootFolder = overrideRootFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
const overrideProfileId = appliedOverrideRules.find(
|
const aSpecificity = keys.filter((key) => a[key] !== null).length;
|
||||||
(rule) => rule.profileId
|
const bSpecificity = keys.filter((key) => b[key] !== null).length;
|
||||||
)?.profileId;
|
|
||||||
if (overrideProfileId) {
|
|
||||||
profileId = overrideProfileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const overrideTags = appliedOverrideRules.find((rule) => rule.tags)?.tags;
|
// Take the rule with the most specific condition first
|
||||||
if (overrideTags) {
|
return bSpecificity - aSpecificity;
|
||||||
tags = [
|
})[0];
|
||||||
...new Set([
|
|
||||||
...(tags || []),
|
if (prioritizedRule) {
|
||||||
...overrideTags.split(',').map((tag) => Number(tag)),
|
if (prioritizedRule.rootFolder) {
|
||||||
]),
|
rootFolder = prioritizedRule.rootFolder;
|
||||||
];
|
}
|
||||||
|
if (prioritizedRule.profileId) {
|
||||||
|
profileId = prioritizedRule.profileId;
|
||||||
|
}
|
||||||
|
if (prioritizedRule.tags) {
|
||||||
|
tags = [
|
||||||
|
...new Set([
|
||||||
|
...(tags || []),
|
||||||
|
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Override rule applied.', {
|
||||||
|
label: 'Media Request',
|
||||||
|
overrides: prioritizedRule,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,14 +386,14 @@ export class MediaRequest {
|
|||||||
const tmdbMediaShow = tmdbMedia as Awaited<
|
const tmdbMediaShow = tmdbMedia as Awaited<
|
||||||
ReturnType<typeof tmdb.getTvShow>
|
ReturnType<typeof tmdb.getTvShow>
|
||||||
>;
|
>;
|
||||||
const requestedSeasons =
|
let requestedSeasons =
|
||||||
requestBody.seasons === 'all'
|
requestBody.seasons === 'all'
|
||||||
? settings.main.enableSpecialEpisodes
|
? tmdbMediaShow.seasons.map((season) => season.season_number)
|
||||||
? tmdbMediaShow.seasons.map((season) => season.season_number)
|
|
||||||
: tmdbMediaShow.seasons
|
|
||||||
.map((season) => season.season_number)
|
|
||||||
.filter((sn) => sn > 0)
|
|
||||||
: (requestBody.seasons as number[]);
|
: (requestBody.seasons as number[]);
|
||||||
|
if (!settings.main.enableSpecialEpisodes) {
|
||||||
|
requestedSeasons = requestedSeasons.filter((sn) => sn > 0);
|
||||||
|
}
|
||||||
|
|
||||||
let existingSeasons: number[] = [];
|
let existingSeasons: number[] = [];
|
||||||
|
|
||||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||||
@@ -668,10 +719,15 @@ export class MediaRequest {
|
|||||||
// Do not update the status if the item is already partially available or available
|
// Do not update the status if the item is already partially available or available
|
||||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
||||||
media[this.is4k ? 'status4k' : 'status'] !==
|
media[this.is4k ? 'status4k' : 'status'] !==
|
||||||
MediaStatus.PARTIALLY_AVAILABLE
|
MediaStatus.PARTIALLY_AVAILABLE &&
|
||||||
|
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
|
||||||
) {
|
) {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
const statusField = this.is4k ? 'status4k' : 'status';
|
||||||
mediaRepository.save(media);
|
|
||||||
|
await mediaRepository.update(
|
||||||
|
{ id: this.media.id },
|
||||||
|
{ [statusField]: MediaStatus.PROCESSING }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -954,6 +1010,14 @@ export class MediaRequest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
radarr.clearCache({
|
||||||
|
tmdbId: movie.id,
|
||||||
|
externalId: this.is4k
|
||||||
|
? media.externalServiceId4k
|
||||||
|
: media.externalServiceId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
logger.info('Sent request to Radarr', {
|
logger.info('Sent request to Radarr', {
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
@@ -1211,19 +1275,23 @@ export class MediaRequest {
|
|||||||
throw new Error('Media data not found');
|
throw new Error('Media data not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
const updateFields = {
|
||||||
sonarrSeries.id;
|
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
|
||||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
sonarrSeries.id,
|
||||||
sonarrSeries.titleSlug;
|
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
sonarrSeries.titleSlug,
|
||||||
|
[this.is4k ? 'serviceId4k' : 'serviceId']: sonarrSettings?.id,
|
||||||
|
};
|
||||||
|
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
await requestRepository.update(
|
||||||
await requestRepository.save(this);
|
{ id: this.id },
|
||||||
|
{ status: MediaRequestStatus.FAILED }
|
||||||
|
);
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||||
@@ -1236,6 +1304,15 @@ export class MediaRequest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
sonarr.clearCache({
|
||||||
|
tvdbId,
|
||||||
|
externalId: this.is4k
|
||||||
|
? media.externalServiceId4k
|
||||||
|
: media.externalServiceId,
|
||||||
|
title: series.name,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
logger.info('Sent request to Sonarr', {
|
logger.info('Sent request to Sonarr', {
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class SonarrScanner
|
|||||||
const filteredSeasons = sonarrSeries.seasons.filter(
|
const filteredSeasons = sonarrSeries.seasons.filter(
|
||||||
(sn) =>
|
(sn) =>
|
||||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) &&
|
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) &&
|
||||||
(!settings.main.partialRequestsEnabled ? sn.seasonNumber !== 0 : true)
|
(!settings.main.enableSpecialEpisodes ? sn.seasonNumber !== 0 : true)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const season of filteredSeasons) {
|
for (const season of filteredSeasons) {
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
body.serverType !== MediaServerType.JELLYFIN &&
|
body.serverType !== MediaServerType.JELLYFIN &&
|
||||||
body.serverType !== MediaServerType.EMBY
|
body.serverType !== MediaServerType.EMBY
|
||||||
) {
|
) {
|
||||||
throw new Error('select_server_type');
|
throw new ApiError(500, ApiErrorCode.NoAdminUser);
|
||||||
}
|
}
|
||||||
settings.main.mediaServerType = body.serverType;
|
settings.main.mediaServerType = body.serverType;
|
||||||
|
|
||||||
@@ -533,6 +533,22 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
message: e.errorCode,
|
message: e.errorCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case ApiErrorCode.NoAdminUser:
|
||||||
|
logger.warn(
|
||||||
|
'Failed login attempt from user without admin permissions and no admin user exists',
|
||||||
|
{
|
||||||
|
label: 'Auth',
|
||||||
|
account: {
|
||||||
|
ip: req.ip,
|
||||||
|
email: body.username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return next({
|
||||||
|
status: e.statusCode,
|
||||||
|
message: e.errorCode,
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.error(e.message, { label: 'Auth' });
|
logger.error(e.message, { label: 'Auth' });
|
||||||
return next({
|
return next({
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ router.get('/', async (req, res, next) => {
|
|||||||
query = query
|
query = query
|
||||||
.addSelect((subQuery) => {
|
.addSelect((subQuery) => {
|
||||||
return subQuery
|
return subQuery
|
||||||
.select('COUNT(request.id)', 'requestCount')
|
.select('COUNT(request.id)', 'request_count')
|
||||||
.from(MediaRequest, 'request')
|
.from(MediaRequest, 'request')
|
||||||
.where('request.requestedBy.id = user.id');
|
.where('request.requestedBy.id = user.id');
|
||||||
}, 'requestCount')
|
}, 'request_count')
|
||||||
.orderBy('requestCount', 'DESC');
|
.orderBy('request_count', 'DESC');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
query = query.orderBy('user.id', 'ASC');
|
query = query.orderBy('user.id', 'ASC');
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const messages = defineMessages('components.Login', {
|
|||||||
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
||||||
loginerror: 'Something went wrong while trying to sign in.',
|
loginerror: 'Something went wrong while trying to sign in.',
|
||||||
adminerror: 'You must use an admin account to sign in.',
|
adminerror: 'You must use an admin account to sign in.',
|
||||||
|
noadminerror: 'No admin user found on the server.',
|
||||||
credentialerror: 'The username or password is incorrect.',
|
credentialerror: 'The username or password is incorrect.',
|
||||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||||
signingin: 'Signing in…',
|
signingin: 'Signing in…',
|
||||||
@@ -157,6 +158,9 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
case ApiErrorCode.NotAdmin:
|
case ApiErrorCode.NotAdmin:
|
||||||
errorMessage = messages.adminerror;
|
errorMessage = messages.adminerror;
|
||||||
break;
|
break;
|
||||||
|
case ApiErrorCode.NoAdminUser:
|
||||||
|
errorMessage = messages.noadminerror;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
errorMessage = messages.loginerror;
|
errorMessage = messages.loginerror;
|
||||||
break;
|
break;
|
||||||
@@ -388,14 +392,35 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
email: values.username,
|
email: values.username,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
let errorData;
|
||||||
|
try {
|
||||||
|
errorData = await e.cause?.text();
|
||||||
|
errorData = JSON.parse(errorData);
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
let errorMessage = null;
|
||||||
|
switch (errorData?.message) {
|
||||||
|
case ApiErrorCode.InvalidUrl:
|
||||||
|
errorMessage = messages.invalidurlerror;
|
||||||
|
break;
|
||||||
|
case ApiErrorCode.InvalidCredentials:
|
||||||
|
errorMessage = messages.credentialerror;
|
||||||
|
break;
|
||||||
|
case ApiErrorCode.NotAdmin:
|
||||||
|
errorMessage = messages.adminerror;
|
||||||
|
break;
|
||||||
|
case ApiErrorCode.NoAdminUser:
|
||||||
|
errorMessage = messages.noadminerror;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = messages.loginerror;
|
||||||
|
break;
|
||||||
|
}
|
||||||
toasts.addToast(
|
toasts.addToast(
|
||||||
intl.formatMessage(
|
intl.formatMessage(errorMessage, mediaServerFormatValues),
|
||||||
e.message == 'Request failed with status code 401'
|
|
||||||
? messages.credentialerror
|
|
||||||
: messages.loginerror
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
|
|||||||
@@ -220,8 +220,8 @@ const RequestList = () => {
|
|||||||
</select>
|
</select>
|
||||||
<Tooltip content={intl.formatMessage(messages.sortDirection)}>
|
<Tooltip content={intl.formatMessage(messages.sortDirection)}>
|
||||||
<Button
|
<Button
|
||||||
buttonType="ghost"
|
buttonType="default"
|
||||||
className="z-40 mr-2 rounded-l-none"
|
className="z-40 mr-2 rounded-l-none border !border-gray-500 !bg-gray-800 !px-3 !text-gray-500 hover:!bg-gray-400 hover:!text-white"
|
||||||
buttonSize="md"
|
buttonSize="md"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentSortDirection(
|
setCurrentSortDirection(
|
||||||
@@ -230,9 +230,9 @@ const RequestList = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{currentSortDirection === 'asc' ? (
|
{currentSortDirection === 'asc' ? (
|
||||||
<ArrowUpIcon className="h-3" />
|
<ArrowUpIcon className="h-6 w-6" />
|
||||||
) : (
|
) : (
|
||||||
<ArrowDownIcon className="h-3" />
|
<ArrowDownIcon className="h-6 w-6" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -256,8 +256,8 @@ const TvRequestModal = ({
|
|||||||
let allSeasons = (data?.seasons ?? []).filter(
|
let allSeasons = (data?.seasons ?? []).filter(
|
||||||
(season) => season.episodeCount !== 0
|
(season) => season.episodeCount !== 0
|
||||||
);
|
);
|
||||||
if (!settings.currentSettings.partialRequestsEnabled) {
|
if (!settings.currentSettings.enableSpecialEpisodes) {
|
||||||
allSeasons = allSeasons.filter((season) => season.seasonNumber !== 0);
|
allSeasons = allSeasons.filter((season) => season.seasonNumber > 0);
|
||||||
}
|
}
|
||||||
return allSeasons.map((season) => season.seasonNumber);
|
return allSeasons.map((season) => season.seasonNumber);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ const SettingsMain = () => {
|
|||||||
locale: data?.locale ?? 'en',
|
locale: data?.locale ?? 'en',
|
||||||
discoverRegion: data?.discoverRegion,
|
discoverRegion: data?.discoverRegion,
|
||||||
originalLanguage: data?.originalLanguage,
|
originalLanguage: data?.originalLanguage,
|
||||||
streamingRegion: data?.streamingRegion,
|
streamingRegion: data?.streamingRegion || 'US',
|
||||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||||
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
||||||
trustProxy: data?.trustProxy,
|
trustProxy: data?.trustProxy,
|
||||||
@@ -433,7 +433,7 @@ const SettingsMain = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-30">
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
value={values.originalLanguage}
|
value={values.originalLanguage}
|
||||||
@@ -449,9 +449,9 @@ const SettingsMain = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-20">
|
||||||
<RegionSelector
|
<RegionSelector
|
||||||
value={values.streamingRegion || 'US'}
|
value={values.streamingRegion}
|
||||||
name="streamingRegion"
|
name="streamingRegion"
|
||||||
onChange={setFieldValue}
|
onChange={setFieldValue}
|
||||||
regionType="streaming"
|
regionType="streaming"
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
const showHasSpecials = data.seasons.some(
|
const showHasSpecials = data.seasons.some(
|
||||||
(season) =>
|
(season) =>
|
||||||
season.seasonNumber === 0 &&
|
season.seasonNumber === 0 &&
|
||||||
settings.currentSettings.partialRequestsEnabled
|
settings.currentSettings.enableSpecialEpisodes
|
||||||
);
|
);
|
||||||
|
|
||||||
const isComplete =
|
const isComplete =
|
||||||
|
|||||||
Reference in New Issue
Block a user