mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-30 21:49:11 -05:00
Compare commits
1 Commits
develop
...
renovate/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb421a5c65 |
8
.github/ISSUE_TEMPLATE/bug.yml
vendored
8
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -91,14 +91,6 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Additional Context
|
label: Additional Context
|
||||||
description: Please provide any additional information that may be relevant or helpful.
|
description: Please provide any additional information that may be relevant or helpful.
|
||||||
- type: checkboxes
|
|
||||||
id: search-existing
|
|
||||||
attributes:
|
|
||||||
label: Search Existing Issues
|
|
||||||
description: Have you searched existing issues to see if this bug has already been reported?
|
|
||||||
options:
|
|
||||||
- label: Yes, I have searched existing issues.
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
8
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
@@ -27,14 +27,6 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Additional Context
|
label: Additional Context
|
||||||
description: Provide any additional information or screenshots that may be relevant or helpful.
|
description: Provide any additional information or screenshots that may be relevant or helpful.
|
||||||
- type: checkboxes
|
|
||||||
id: search-existing
|
|
||||||
attributes:
|
|
||||||
label: Search Existing Issues
|
|
||||||
description: Have you searched existing issues to see if this feature has already been requested?
|
|
||||||
options:
|
|
||||||
- label: Yes, I have searched existing issues.
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||||
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
||||||
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/svg-badge.svg" alt="Translation status" /></a>
|
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||||
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
||||||
|
|
||||||
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
"swagger-ui-express": "4.6.2",
|
"swagger-ui-express": "4.6.2",
|
||||||
"swr": "2.3.7",
|
"swr": "2.3.7",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"typeorm": "0.3.12",
|
"typeorm": "0.3.28",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
"undici": "^7.16.0",
|
"undici": "^7.16.0",
|
||||||
"validator": "^13.15.23",
|
"validator": "^13.15.23",
|
||||||
|
|||||||
3468
pnpm-lock.yaml
generated
3468
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,6 @@ interface ProcessOptions {
|
|||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
mediaAddedAt?: Date;
|
mediaAddedAt?: Date;
|
||||||
ratingKey?: string;
|
ratingKey?: string;
|
||||||
jellyfinMediaId?: string;
|
|
||||||
imdbId?: string;
|
|
||||||
serviceId?: number;
|
serviceId?: number;
|
||||||
externalServiceId?: number;
|
externalServiceId?: number;
|
||||||
externalServiceSlug?: string;
|
externalServiceSlug?: string;
|
||||||
@@ -97,8 +95,6 @@ class BaseScanner<T> {
|
|||||||
is4k = false,
|
is4k = false,
|
||||||
mediaAddedAt,
|
mediaAddedAt,
|
||||||
ratingKey,
|
ratingKey,
|
||||||
jellyfinMediaId,
|
|
||||||
imdbId,
|
|
||||||
serviceId,
|
serviceId,
|
||||||
externalServiceId,
|
externalServiceId,
|
||||||
externalServiceSlug,
|
externalServiceSlug,
|
||||||
@@ -137,21 +133,6 @@ class BaseScanner<T> {
|
|||||||
changedExisting = true;
|
changedExisting = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
jellyfinMediaId &&
|
|
||||||
existing[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] !==
|
|
||||||
jellyfinMediaId
|
|
||||||
) {
|
|
||||||
existing[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
|
|
||||||
jellyfinMediaId;
|
|
||||||
changedExisting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imdbId && !existing.imdbId) {
|
|
||||||
existing.imdbId = imdbId;
|
|
||||||
changedExisting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
serviceId !== undefined &&
|
serviceId !== undefined &&
|
||||||
existing[is4k ? 'serviceId4k' : 'serviceId'] !== serviceId
|
existing[is4k ? 'serviceId4k' : 'serviceId'] !== serviceId
|
||||||
@@ -192,7 +173,6 @@ class BaseScanner<T> {
|
|||||||
} else {
|
} else {
|
||||||
const newMedia = new Media();
|
const newMedia = new Media();
|
||||||
newMedia.tmdbId = tmdbId;
|
newMedia.tmdbId = tmdbId;
|
||||||
newMedia.imdbId = imdbId;
|
|
||||||
|
|
||||||
newMedia.status =
|
newMedia.status =
|
||||||
!is4k && !processing
|
!is4k && !processing
|
||||||
@@ -223,13 +203,6 @@ class BaseScanner<T> {
|
|||||||
newMedia.ratingKey4k =
|
newMedia.ratingKey4k =
|
||||||
is4k && this.enable4kMovie ? ratingKey : undefined;
|
is4k && this.enable4kMovie ? ratingKey : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jellyfinMediaId) {
|
|
||||||
newMedia.jellyfinMediaId = !is4k ? jellyfinMediaId : undefined;
|
|
||||||
newMedia.jellyfinMediaId4k =
|
|
||||||
is4k && this.enable4kMovie ? jellyfinMediaId : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mediaRepository.save(newMedia);
|
await mediaRepository.save(newMedia);
|
||||||
this.log(`Saved new media: ${title}`);
|
this.log(`Saved new media: ${title}`);
|
||||||
}
|
}
|
||||||
@@ -248,12 +221,11 @@ class BaseScanner<T> {
|
|||||||
*/
|
*/
|
||||||
protected async processShow(
|
protected async processShow(
|
||||||
tmdbId: number,
|
tmdbId: number,
|
||||||
tvdbId: number | undefined,
|
tvdbId: number,
|
||||||
seasons: ProcessableSeason[],
|
seasons: ProcessableSeason[],
|
||||||
{
|
{
|
||||||
mediaAddedAt,
|
mediaAddedAt,
|
||||||
ratingKey,
|
ratingKey,
|
||||||
jellyfinMediaId,
|
|
||||||
serviceId,
|
serviceId,
|
||||||
externalServiceId,
|
externalServiceId,
|
||||||
externalServiceSlug,
|
externalServiceSlug,
|
||||||
@@ -285,7 +257,7 @@ class BaseScanner<T> {
|
|||||||
(es) => es.seasonNumber === season.seasonNumber
|
(es) => es.seasonNumber === season.seasonNumber
|
||||||
);
|
);
|
||||||
|
|
||||||
// We update the rating keys and jellyfinMediaId in the seasons loop because we need episode counts
|
// We update the rating keys in the seasons loop because we need episode counts
|
||||||
if (media && season.episodes > 0 && media.ratingKey !== ratingKey) {
|
if (media && season.episodes > 0 && media.ratingKey !== ratingKey) {
|
||||||
media.ratingKey = ratingKey;
|
media.ratingKey = ratingKey;
|
||||||
}
|
}
|
||||||
@@ -299,23 +271,6 @@ class BaseScanner<T> {
|
|||||||
media.ratingKey4k = ratingKey;
|
media.ratingKey4k = ratingKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
media &&
|
|
||||||
season.episodes > 0 &&
|
|
||||||
media.jellyfinMediaId !== jellyfinMediaId
|
|
||||||
) {
|
|
||||||
media.jellyfinMediaId = jellyfinMediaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
media &&
|
|
||||||
season.episodes4k > 0 &&
|
|
||||||
this.enable4kShow &&
|
|
||||||
media.jellyfinMediaId4k !== jellyfinMediaId
|
|
||||||
) {
|
|
||||||
media.jellyfinMediaId4k = jellyfinMediaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingSeason) {
|
if (existingSeason) {
|
||||||
// Here we update seasons if they already exist.
|
// Here we update seasons if they already exist.
|
||||||
// If the season is already marked as available, we
|
// If the season is already marked as available, we
|
||||||
@@ -536,22 +491,6 @@ class BaseScanner<T> {
|
|||||||
)
|
)
|
||||||
? ratingKey
|
? ratingKey
|
||||||
: undefined,
|
: undefined,
|
||||||
jellyfinMediaId: newSeasons.some(
|
|
||||||
(sn) =>
|
|
||||||
sn.status === MediaStatus.PARTIALLY_AVAILABLE ||
|
|
||||||
sn.status === MediaStatus.AVAILABLE
|
|
||||||
)
|
|
||||||
? jellyfinMediaId
|
|
||||||
: undefined,
|
|
||||||
jellyfinMediaId4k:
|
|
||||||
this.enable4kShow &&
|
|
||||||
newSeasons.some(
|
|
||||||
(sn) =>
|
|
||||||
sn.status4k === MediaStatus.PARTIALLY_AVAILABLE ||
|
|
||||||
sn.status4k === MediaStatus.AVAILABLE
|
|
||||||
)
|
|
||||||
? jellyfinMediaId
|
|
||||||
: undefined,
|
|
||||||
status: isAllStandardSeasons
|
status: isAllStandardSeasons
|
||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.AVAILABLE
|
||||||
: newSeasons.some(
|
: newSeasons.some(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -626,6 +626,76 @@ authRoutes.post('/local', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mainUser = await userRepository.findOneOrFail({
|
||||||
|
select: { id: true, plexToken: true, plexId: true },
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||||
|
|
||||||
|
if (!user.plexId) {
|
||||||
|
try {
|
||||||
|
const plexUsersResponse = await mainPlexTv.getUsers();
|
||||||
|
const account = plexUsersResponse.MediaContainer.User.find(
|
||||||
|
(account) =>
|
||||||
|
account.$.email &&
|
||||||
|
account.$.email.toLowerCase() === user.email.toLowerCase()
|
||||||
|
)?.$;
|
||||||
|
|
||||||
|
if (
|
||||||
|
account &&
|
||||||
|
(await mainPlexTv.checkUserAccess(parseInt(account.id)))
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
'Found matching Plex user; updating user with Plex data',
|
||||||
|
{
|
||||||
|
label: 'API',
|
||||||
|
ip: req.ip,
|
||||||
|
email: body.email,
|
||||||
|
userId: user.id,
|
||||||
|
plexId: account.id,
|
||||||
|
plexUsername: account.username,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
user.plexId = parseInt(account.id);
|
||||||
|
user.avatar = account.thumb;
|
||||||
|
user.email = account.email;
|
||||||
|
user.plexUsername = account.username;
|
||||||
|
user.userType = UserType.PLEX;
|
||||||
|
|
||||||
|
await userRepository.save(user);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong fetching Plex users', {
|
||||||
|
label: 'API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.plexId &&
|
||||||
|
user.plexId !== mainUser.plexId &&
|
||||||
|
!(await mainPlexTv.checkUserAccess(user.plexId))
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
'Failed sign-in attempt from Plex user without access to the media server',
|
||||||
|
{
|
||||||
|
label: 'API',
|
||||||
|
account: {
|
||||||
|
ip: req.ip,
|
||||||
|
email: body.email,
|
||||||
|
userId: user.id,
|
||||||
|
plexId: user.plexId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return next({
|
||||||
|
status: 403,
|
||||||
|
message: 'Access denied.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Set logged in session
|
// Set logged in session
|
||||||
if (user && req.session) {
|
if (user && req.session) {
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
@@ -705,7 +775,7 @@ authRoutes.post('/logout', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
return next({ status: 500, message: 'Failed to destroy session.' });
|
return next({ status: 500, message: 'Failed to destroy session.' });
|
||||||
}
|
}
|
||||||
logger.debug('Successfully logged out user', {
|
logger.info('Successfully logged out user', {
|
||||||
label: 'Auth',
|
label: 'Auth',
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -320,14 +320,12 @@ const SettingsMetadata = () => {
|
|||||||
|
|
||||||
addToast(intl.formatMessage(messages.metadataSettingsSaved), {
|
addToast(intl.formatMessage(messages.metadataSettingsSaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.failedToSaveMetadataSettings),
|
intl.formatMessage(messages.failedToSaveMetadataSettings),
|
||||||
{
|
{
|
||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
autoDismiss: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -424,7 +422,6 @@ const SettingsMetadata = () => {
|
|||||||
),
|
),
|
||||||
{
|
{
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user