From 77a33cb74d744bb747b791785799b632af8c7862 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Thu, 4 May 2023 14:08:22 -0400
Subject: [PATCH 01/34] fix(ui): corrected mobile menu spacing in collection
details (#3432)
---
src/components/CollectionDetails/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx
index 34b379e24..ff22ee107 100644
--- a/src/components/CollectionDetails/index.tsx
+++ b/src/components/CollectionDetails/index.tsx
@@ -348,7 +348,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
/>
))}
/>
-
+
);
};
From 7522aa31743b169c903ebdf9d4d698645d27514c Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Wed, 10 May 2023 20:36:12 -0400
Subject: [PATCH 02/34] fix: availability sync file detection (#3371)
* fix: added extra check for unmonitored movies in radarr
* feat: created new radarr/sonarr routes to grab existing series data
* refactor: updated job routes to check by external service id
* fix: season check will now also look at episode file count
---
server/api/servarr/sonarr.ts | 19 ++
server/lib/availabilitySync.ts | 530 +++++++++++++++++++--------------
2 files changed, 320 insertions(+), 229 deletions(-)
diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts
index 2e423ef39..6cda2a49c 100644
--- a/server/api/servarr/sonarr.ts
+++ b/server/api/servarr/sonarr.ts
@@ -76,6 +76,15 @@ export interface SonarrSeries {
ignoreEpisodesWithoutFiles?: boolean;
searchForMissingEpisodes?: boolean;
};
+ statistics: {
+ seasonCount: number;
+ episodeFileCount: number;
+ episodeCount: number;
+ totalEpisodeCount: number;
+ sizeOnDisk: number;
+ releaseGroups: string[];
+ percentOfEpisodes: number;
+ };
}
export interface AddSeriesOptions {
@@ -116,6 +125,16 @@ class SonarrAPI extends ServarrBase<{
}
}
+ public async getSeriesById(id: number): Promise {
+ try {
+ const response = await this.axios.get(`/series/${id}`);
+
+ return response.data;
+ } catch (e) {
+ throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
+ }
+ }
+
public async getSeriesByTitle(title: string): Promise {
try {
const response = await this.axios.get('/series/lookup', {
diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts
index 93ccfe391..a9f61fff6 100644
--- a/server/lib/availabilitySync.ts
+++ b/server/lib/availabilitySync.ts
@@ -1,7 +1,8 @@
import type { PlexMetadata } from '@server/api/plexapi';
import PlexAPI from '@server/api/plexapi';
+import type { RadarrMovie } from '@server/api/servarr/radarr';
import RadarrAPI from '@server/api/servarr/radarr';
-import type { SonarrSeason } from '@server/api/servarr/sonarr';
+import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
@@ -47,158 +48,150 @@ class AvailabilitySync {
try {
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
- try {
- if (!this.running) {
- throw new Error('Job aborted');
- }
+ if (!this.running) {
+ throw new Error('Job aborted');
+ }
- const mediaExists = await this.mediaExists(media);
+ const mediaExists = await this.mediaExists(media);
+
+ // We can not delete media so if both versions do not exist, we will change both columns to unknown or null
+ if (!mediaExists) {
+ if (
+ media.status !== MediaStatus.UNKNOWN ||
+ media.status4k !== MediaStatus.UNKNOWN
+ ) {
+ const request = await requestRepository.find({
+ relations: {
+ media: true,
+ },
+ where: { media: { id: media.id } },
+ });
- //We can not delete media so if both versions do not exist, we will change both columns to unknown or null
- if (!mediaExists) {
- if (
- media.status !== MediaStatus.UNKNOWN ||
- media.status4k !== MediaStatus.UNKNOWN
- ) {
- const request = await requestRepository.find({
- relations: {
- media: true,
- },
- where: { media: { id: media.id } },
- });
-
- logger.info(
- `${
- media.mediaType === 'tv' ? media.tvdbId : media.tmdbId
- } does not exist in any of your media instances. We will change its status to unknown.`,
- { label: 'AvailabilitySync' }
- );
+ logger.info(
+ `Media ID ${media.id} does not exist in any of your media instances. Status will be changed to unknown.`,
+ { label: 'AvailabilitySync' }
+ );
+
+ await mediaRepository.update(media.id, {
+ status: MediaStatus.UNKNOWN,
+ status4k: MediaStatus.UNKNOWN,
+ serviceId: null,
+ serviceId4k: null,
+ externalServiceId: null,
+ externalServiceId4k: null,
+ externalServiceSlug: null,
+ externalServiceSlug4k: null,
+ ratingKey: null,
+ ratingKey4k: null,
+ });
- await mediaRepository.update(media.id, {
- status: MediaStatus.UNKNOWN,
- status4k: MediaStatus.UNKNOWN,
- serviceId: null,
- serviceId4k: null,
- externalServiceId: null,
- externalServiceId4k: null,
- externalServiceSlug: null,
- externalServiceSlug4k: null,
- ratingKey: null,
- ratingKey4k: null,
- });
-
- await requestRepository.remove(request);
- }
+ await requestRepository.remove(request);
}
+ }
- if (media.mediaType === 'tv') {
- // ok, the show itself exists, but do all it's seasons?
- const seasons = await seasonRepository.find({
- where: [
- { status: MediaStatus.AVAILABLE, media: { id: media.id } },
- {
- status: MediaStatus.PARTIALLY_AVAILABLE,
- media: { id: media.id },
- },
- { status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
+ if (media.mediaType === 'tv') {
+ // ok, the show itself exists, but do all it's seasons?
+ const seasons = await seasonRepository.find({
+ where: [
+ { status: MediaStatus.AVAILABLE, media: { id: media.id } },
+ {
+ status: MediaStatus.PARTIALLY_AVAILABLE,
+ media: { id: media.id },
+ },
+ { status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
+ {
+ status4k: MediaStatus.PARTIALLY_AVAILABLE,
+ media: { id: media.id },
+ },
+ ],
+ });
+
+ let didDeleteSeasons = false;
+ for (const season of seasons) {
+ if (
+ !mediaExists &&
+ (season.status !== MediaStatus.UNKNOWN ||
+ season.status4k !== MediaStatus.UNKNOWN)
+ ) {
+ await seasonRepository.update(
+ { id: season.id },
{
- status4k: MediaStatus.PARTIALLY_AVAILABLE,
- media: { id: media.id },
- },
- ],
- });
+ status: MediaStatus.UNKNOWN,
+ status4k: MediaStatus.UNKNOWN,
+ }
+ );
+ } else {
+ const seasonExists = await this.seasonExists(media, season);
- let didDeleteSeasons = false;
- for (const season of seasons) {
- if (
- !mediaExists &&
- (season.status !== MediaStatus.UNKNOWN ||
- season.status4k !== MediaStatus.UNKNOWN)
- ) {
- await seasonRepository.update(
- { id: season.id },
- {
- status: MediaStatus.UNKNOWN,
- status4k: MediaStatus.UNKNOWN,
- }
+ if (!seasonExists) {
+ logger.info(
+ `Removing season ${season.seasonNumber}, media ID ${media.id} because it does not exist in any of your media instances.`,
+ { label: 'AvailabilitySync' }
);
- } else {
- const seasonExists = await this.seasonExists(media, season);
- if (!seasonExists) {
- logger.info(
- `Removing season ${season.seasonNumber}, media id: ${media.tvdbId} because it does not exist in any of your media instances.`,
- { label: 'AvailabilitySync' }
+ if (
+ season.status !== MediaStatus.UNKNOWN ||
+ season.status4k !== MediaStatus.UNKNOWN
+ ) {
+ await seasonRepository.update(
+ { id: season.id },
+ {
+ status: MediaStatus.UNKNOWN,
+ status4k: MediaStatus.UNKNOWN,
+ }
);
+ }
- if (
- season.status !== MediaStatus.UNKNOWN ||
- season.status4k !== MediaStatus.UNKNOWN
- ) {
- await seasonRepository.update(
- { id: season.id },
- {
- status: MediaStatus.UNKNOWN,
- status4k: MediaStatus.UNKNOWN,
- }
- );
- }
-
- const seasonToBeDeleted =
- await seasonRequestRepository.findOne({
- relations: {
- request: {
- media: true,
- },
+ const seasonToBeDeleted = await seasonRequestRepository.findOne(
+ {
+ relations: {
+ request: {
+ media: true,
},
- where: {
- request: {
- media: {
- id: media.id,
- },
+ },
+ where: {
+ request: {
+ media: {
+ id: media.id,
},
- seasonNumber: season.seasonNumber,
},
- });
-
- if (seasonToBeDeleted) {
- await seasonRequestRepository.remove(seasonToBeDeleted);
+ seasonNumber: season.seasonNumber,
+ },
}
+ );
- didDeleteSeasons = true;
+ if (seasonToBeDeleted) {
+ await seasonRequestRepository.remove(seasonToBeDeleted);
}
+
+ didDeleteSeasons = true;
}
+ }
- if (didDeleteSeasons) {
- if (
- media.status === MediaStatus.AVAILABLE ||
- media.status4k === MediaStatus.AVAILABLE
- ) {
- logger.info(
- `Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted some of its seasons.`,
- { label: 'AvailabilitySync' }
- );
+ if (didDeleteSeasons) {
+ if (
+ media.status === MediaStatus.AVAILABLE ||
+ media.status4k === MediaStatus.AVAILABLE
+ ) {
+ logger.info(
+ `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ { label: 'AvailabilitySync' }
+ );
- if (media.status === MediaStatus.AVAILABLE) {
- await mediaRepository.update(media.id, {
- status: MediaStatus.PARTIALLY_AVAILABLE,
- });
- }
+ if (media.status === MediaStatus.AVAILABLE) {
+ await mediaRepository.update(media.id, {
+ status: MediaStatus.PARTIALLY_AVAILABLE,
+ });
+ }
- if (media.status4k === MediaStatus.AVAILABLE) {
- await mediaRepository.update(media.id, {
- status4k: MediaStatus.PARTIALLY_AVAILABLE,
- });
- }
+ if (media.status4k === MediaStatus.AVAILABLE) {
+ await mediaRepository.update(media.id, {
+ status4k: MediaStatus.PARTIALLY_AVAILABLE,
+ });
}
}
}
}
- } catch (ex) {
- logger.error('Failure with media.', {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- });
}
}
} catch (ex) {
@@ -254,9 +247,9 @@ class AvailabilitySync {
});
logger.info(
- `${media.tmdbId} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
- isTVType ? 'sonarr' : 'radarr'
- } and plex instance. We will change its status to unknown.`,
+ `Media ID ${media.id} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
+ isTVType ? 'Sonarr' : 'Radarr'
+ } and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
@@ -306,46 +299,70 @@ class AvailabilitySync {
apiKey: server.apiKey,
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
- const meta = await api.getMovieByTmdbId(media.tmdbId);
+ try {
+ // Check if both exist or if a single non-4k or 4k exists
+ // If both do not exist we will return false
- //check if both exist or if a single non-4k or 4k exists
- //if both do not exist we will return false
- if (!server.is4k && !meta.id) {
- existsInRadarr = false;
- }
+ let meta: RadarrMovie | undefined;
- if (server.is4k && !meta.id) {
- existsInRadarr4k = false;
- }
- }
+ if (!server.is4k && media.externalServiceId) {
+ meta = await api.getMovie({ id: media.externalServiceId });
+ }
- if (existsInRadarr && existsInRadarr4k) {
- return true;
- }
+ if (server.is4k && media.externalServiceId4k) {
+ meta = await api.getMovie({ id: media.externalServiceId4k });
+ }
- if (!existsInRadarr && existsInPlex) {
- return true;
- }
+ if (!server.is4k && (!meta || !meta.hasFile)) {
+ existsInRadarr = false;
+ }
- if (!existsInRadarr4k && existsInPlex4k) {
- return true;
+ if (server.is4k && (!meta || !meta.hasFile)) {
+ existsInRadarr4k = false;
+ }
+ } catch (ex) {
+ logger.debug(
+ `Failure retrieving media ID ${media.id} from your ${
+ !server.is4k ? 'non-4K' : '4K'
+ } Radarr.`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
+ );
+ if (!server.is4k) {
+ existsInRadarr = false;
+ }
+
+ if (server.is4k) {
+ existsInRadarr4k = false;
+ }
+ }
}
- //if only a single non-4k or 4k exists, then change entity columns accordingly
- //related media request will then be deleted
- if (!existsInRadarr && existsInRadarr4k && !existsInPlex) {
+ // If only a single non-4k or 4k exists, then change entity columns accordingly
+ // Related media request will then be deleted
+ if (
+ !existsInRadarr &&
+ (existsInRadarr4k || existsInPlex4k) &&
+ !existsInPlex
+ ) {
if (media.status !== MediaStatus.UNKNOWN) {
this.mediaUpdater(media, false);
}
}
- if (existsInRadarr && !existsInRadarr4k && !existsInPlex4k) {
+ if (
+ (existsInRadarr || existsInPlex) &&
+ !existsInRadarr4k &&
+ !existsInPlex4k
+ ) {
if (media.status4k !== MediaStatus.UNKNOWN) {
this.mediaUpdater(media, true);
}
}
- if (existsInRadarr || existsInRadarr4k) {
+ if (existsInRadarr || existsInRadarr4k || existsInPlex || existsInPlex4k) {
return true;
}
@@ -357,10 +374,6 @@ class AvailabilitySync {
existsInPlex: boolean,
existsInPlex4k: boolean
): Promise {
- if (!media.tvdbId) {
- return false;
- }
-
let existsInSonarr = true;
let existsInSonarr4k = true;
@@ -369,49 +382,75 @@ class AvailabilitySync {
apiKey: server.apiKey,
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
+ try {
+ // Check if both exist or if a single non-4k or 4k exists
+ // If both do not exist we will return false
- const meta = await api.getSeriesByTvdbId(media.tvdbId);
+ let meta: SonarrSeries | undefined;
- this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = meta.seasons;
+ if (!server.is4k && media.externalServiceId) {
+ meta = await api.getSeriesById(media.externalServiceId);
+ this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
+ meta.seasons;
+ }
- //check if both exist or if a single non-4k or 4k exists
- //if both do not exist we will return false
- if (!server.is4k && !meta.id) {
- existsInSonarr = false;
- }
+ if (server.is4k && media.externalServiceId4k) {
+ meta = await api.getSeriesById(media.externalServiceId4k);
+ this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
+ meta.seasons;
+ }
- if (server.is4k && !meta.id) {
- existsInSonarr4k = false;
- }
- }
+ if (!server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
+ existsInSonarr = false;
+ }
- if (existsInSonarr && existsInSonarr4k) {
- return true;
- }
+ if (server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
+ existsInSonarr4k = false;
+ }
+ } catch (ex) {
+ logger.debug(
+ `Failure retrieving media ID ${media.id} from your ${
+ !server.is4k ? 'non-4K' : '4K'
+ } Sonarr.`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
+ );
- if (!existsInSonarr && existsInPlex) {
- return true;
- }
+ if (!server.is4k) {
+ existsInSonarr = false;
+ }
- if (!existsInSonarr4k && existsInPlex4k) {
- return true;
+ if (server.is4k) {
+ existsInSonarr4k = false;
+ }
+ }
}
- //if only a single non-4k or 4k exists, then change entity columns accordingly
- //related media request will then be deleted
- if (!existsInSonarr && existsInSonarr4k && !existsInPlex) {
+ // If only a single non-4k or 4k exists, then change entity columns accordingly
+ // Related media request will then be deleted
+ if (
+ !existsInSonarr &&
+ (existsInSonarr4k || existsInPlex4k) &&
+ !existsInPlex
+ ) {
if (media.status !== MediaStatus.UNKNOWN) {
this.mediaUpdater(media, false);
}
}
- if (existsInSonarr && !existsInSonarr4k && !existsInPlex4k) {
+ if (
+ (existsInSonarr || existsInPlex) &&
+ !existsInSonarr4k &&
+ !existsInPlex4k
+ ) {
if (media.status4k !== MediaStatus.UNKNOWN) {
this.mediaUpdater(media, true);
}
}
- if (existsInSonarr || existsInSonarr4k) {
+ if (existsInSonarr || existsInSonarr4k || existsInPlex || existsInPlex4k) {
return true;
}
@@ -424,10 +463,6 @@ class AvailabilitySync {
seasonExistsInPlex: boolean,
seasonExistsInPlex4k: boolean
): Promise {
- if (!media.tvdbId) {
- return false;
- }
-
let seasonExistsInSonarr = true;
let seasonExistsInSonarr4k = true;
@@ -441,35 +476,67 @@ class AvailabilitySync {
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
- const seasons =
- this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] ??
- (await api.getSeriesByTvdbId(media.tvdbId)).seasons;
- this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = seasons;
+ try {
+ // Here we can use the cache we built when we fetched the series with mediaExistsInSonarr
+ // If the cache does not have data, we will fetch with the api route
+
+ let seasons: SonarrSeason[] =
+ this.sonarrSeasonsCache[
+ `${server.id}-${
+ !server.is4k ? media.externalServiceId : media.externalServiceId4k
+ }`
+ ];
+
+ if (!server.is4k && media.externalServiceId) {
+ seasons =
+ this.sonarrSeasonsCache[
+ `${server.id}-${media.externalServiceId}`
+ ] ?? (await api.getSeriesById(media.externalServiceId)).seasons;
+ this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
+ seasons;
+ }
- const hasMonitoredSeason = seasons.find(
- ({ monitored, seasonNumber }) =>
- monitored && season.seasonNumber === seasonNumber
- );
+ if (server.is4k && media.externalServiceId4k) {
+ seasons =
+ this.sonarrSeasonsCache[
+ `${server.id}-${media.externalServiceId4k}`
+ ] ?? (await api.getSeriesById(media.externalServiceId4k)).seasons;
+ this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
+ seasons;
+ }
- if (!server.is4k && !hasMonitoredSeason) {
- seasonExistsInSonarr = false;
- }
+ const seasonIsUnavailable = seasons?.find(
+ ({ seasonNumber, statistics }) =>
+ season.seasonNumber === seasonNumber &&
+ statistics?.episodeFileCount === 0
+ );
- if (server.is4k && !hasMonitoredSeason) {
- seasonExistsInSonarr4k = false;
- }
- }
+ if (!server.is4k && seasonIsUnavailable) {
+ seasonExistsInSonarr = false;
+ }
- if (seasonExistsInSonarr && seasonExistsInSonarr4k) {
- return true;
- }
+ if (server.is4k && seasonIsUnavailable) {
+ seasonExistsInSonarr4k = false;
+ }
+ } catch (ex) {
+ logger.debug(
+ `Failure retrieving media ID ${media.id} from your ${
+ !server.is4k ? 'non-4K' : '4K'
+ } Sonarr.`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
+ );
- if (!seasonExistsInSonarr && seasonExistsInPlex) {
- return true;
- }
+ if (!server.is4k) {
+ seasonExistsInSonarr = false;
+ }
- if (!seasonExistsInSonarr4k && seasonExistsInPlex4k) {
- return true;
+ if (server.is4k) {
+ seasonExistsInSonarr4k = false;
+ }
+ }
}
const seasonToBeDeleted = await seasonRequestRepository.findOne({
@@ -489,16 +556,16 @@ class AvailabilitySync {
},
});
- //if season does not exist, we will change status to unknown and delete related season request
- //if parent media request is empty(all related seasons have been removed), parent is automatically deleted
+ // If season does not exist, we will change status to unknown and delete related season request
+ // If parent media request is empty(all related seasons have been removed), parent is automatically deleted
if (
!seasonExistsInSonarr &&
- seasonExistsInSonarr4k &&
+ (seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
!seasonExistsInPlex
) {
if (season.status !== MediaStatus.UNKNOWN) {
logger.info(
- `${media.tvdbId}, season: ${season.seasonNumber} does not exist in your non-4k sonarr and plex instance. We will change its status to unknown.`,
+ `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
await seasonRepository.update(season.id, {
@@ -511,7 +578,7 @@ class AvailabilitySync {
if (media.status === MediaStatus.AVAILABLE) {
logger.info(
- `Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
+ `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
{ label: 'AvailabilitySync' }
);
await mediaRepository.update(media.id, {
@@ -522,13 +589,13 @@ class AvailabilitySync {
}
if (
- seasonExistsInSonarr &&
+ (seasonExistsInSonarr || seasonExistsInPlex) &&
!seasonExistsInSonarr4k &&
!seasonExistsInPlex4k
) {
if (season.status4k !== MediaStatus.UNKNOWN) {
logger.info(
- `${media.tvdbId}, season: ${season.seasonNumber} does not exist in your 4k sonarr and plex instance. We will change its status to unknown.`,
+ `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
await seasonRepository.update(season.id, {
@@ -541,7 +608,7 @@ class AvailabilitySync {
if (media.status4k === MediaStatus.AVAILABLE) {
logger.info(
- `Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
+ `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
{ label: 'AvailabilitySync' }
);
await mediaRepository.update(media.id, {
@@ -551,7 +618,12 @@ class AvailabilitySync {
}
}
- if (seasonExistsInSonarr || seasonExistsInSonarr4k) {
+ if (
+ seasonExistsInSonarr ||
+ seasonExistsInSonarr4k ||
+ seasonExistsInPlex ||
+ seasonExistsInPlex4k
+ ) {
return true;
}
@@ -565,7 +637,7 @@ class AvailabilitySync {
let existsInPlex = false;
let existsInPlex4k = false;
- //check each plex instance to see if media exists
+ // Check each plex instance to see if media exists
try {
if (ratingKey) {
const meta = await this.plexClient?.getMetadata(ratingKey);
@@ -573,6 +645,7 @@ class AvailabilitySync {
existsInPlex = true;
}
}
+
if (ratingKey4k) {
const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
if (meta4k) {
@@ -580,18 +653,17 @@ class AvailabilitySync {
}
}
} catch (ex) {
- // TODO: oof, not the nicest way of handling this, but plex-api does not leave us with any other options...
if (!ex.message.includes('response code: 404')) {
throw ex;
}
}
- //base case for if both media versions exist in plex
+ // Base case if both media versions exist in plex
if (existsInPlex && existsInPlex4k) {
return true;
}
- //we then check radarr or sonarr has that specific media. If not, then we will move to delete
- //if a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
+ // We then check radarr or sonarr has that specific media. If not, then we will move to delete
+ // If a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
if (media.mediaType === 'movie') {
const existsInRadarr = await this.mediaExistsInRadarr(
media,
@@ -599,10 +671,10 @@ class AvailabilitySync {
existsInPlex4k
);
- //if true, media exists in at least one radarr or plex instance.
+ // If true, media exists in at least one radarr or plex instance.
if (existsInRadarr) {
logger.warn(
- `${media.tmdbId} exists in at least one radarr or plex instance. Media will be updated if set to available.`,
+ `${media.id} exists in at least one Radarr or Plex instance. Media will be updated if set to available.`,
{
label: 'AvailabilitySync',
}
@@ -619,10 +691,10 @@ class AvailabilitySync {
existsInPlex4k
);
- //if true, media exists in at least one sonarr or plex instance.
+ // If true, media exists in at least one sonarr or plex instance.
if (existsInSonarr) {
logger.warn(
- `${media.tvdbId} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
+ `${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
{
label: 'AvailabilitySync',
}
@@ -672,7 +744,7 @@ class AvailabilitySync {
}
}
- //base case for if both season versions exist in plex
+ // Base case if both season versions exist in plex
if (seasonExistsInPlex && seasonExistsInPlex4k) {
return true;
}
@@ -686,7 +758,7 @@ class AvailabilitySync {
if (existsInSonarr) {
logger.warn(
- `${media.tvdbId}, season: ${season.seasonNumber} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
+ `Season ${season.seasonNumber}, media ID ${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
{
label: 'AvailabilitySync',
}
From 70b1540ae23e83e01013856a9e06ad39e600922d Mon Sep 17 00:00:00 2001
From: Alex
Date: Thu, 11 May 2023 02:58:16 +0200
Subject: [PATCH 03/34] fix: handle search results with collections (#3393)
* feat: handle search collection
* Update server/utils/typeHelpers.ts
Co-authored-by: Danshil Kokil Mungur
* fix: modified title card to show collection instead of movies
---------
Co-authored-by: Danshil Kokil Mungur
Co-authored-by: Brandon
---
server/api/themoviedb/interfaces.ts | 19 ++++++++++-
server/models/Search.ts | 40 ++++++++++++++++++++++--
server/routes/discover.ts | 5 ++-
server/utils/typeHelpers.ts | 23 ++++++++++++--
src/components/Common/ListView/index.tsx | 15 ++++++++-
src/components/TitleCard/index.tsx | 28 ++++++++++++++---
src/i18n/globalMessages.ts | 1 +
7 files changed, 118 insertions(+), 13 deletions(-)
diff --git a/server/api/themoviedb/interfaces.ts b/server/api/themoviedb/interfaces.ts
index 955e1b12e..775a89765 100644
--- a/server/api/themoviedb/interfaces.ts
+++ b/server/api/themoviedb/interfaces.ts
@@ -28,6 +28,18 @@ export interface TmdbTvResult extends TmdbMediaResult {
first_air_date: string;
}
+export interface TmdbCollectionResult {
+ id: number;
+ media_type: 'collection';
+ title: string;
+ original_title: string;
+ adult: boolean;
+ poster_path?: string;
+ backdrop_path?: string;
+ overview: string;
+ original_language: string;
+}
+
export interface TmdbPersonResult {
id: number;
name: string;
@@ -45,7 +57,12 @@ interface TmdbPaginatedResponse {
}
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
- results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
+ results: (
+ | TmdbMovieResult
+ | TmdbTvResult
+ | TmdbPersonResult
+ | TmdbCollectionResult
+ )[];
}
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
diff --git a/server/models/Search.ts b/server/models/Search.ts
index 6ab696fe3..2193bbe15 100644
--- a/server/models/Search.ts
+++ b/server/models/Search.ts
@@ -1,4 +1,5 @@
import type {
+ TmdbCollectionResult,
TmdbMovieDetails,
TmdbMovieResult,
TmdbPersonDetails,
@@ -9,7 +10,7 @@ import type {
import { MediaType as MainMediaType } from '@server/constants/media';
import type Media from '@server/entity/Media';
-export type MediaType = 'tv' | 'movie' | 'person';
+export type MediaType = 'tv' | 'movie' | 'person' | 'collection';
interface SearchResult {
id: number;
@@ -43,6 +44,18 @@ export interface TvResult extends SearchResult {
firstAirDate: string;
}
+export interface CollectionResult {
+ id: number;
+ mediaType: 'collection';
+ title: string;
+ originalTitle: string;
+ adult: boolean;
+ posterPath?: string;
+ backdropPath?: string;
+ overview: string;
+ originalLanguage: string;
+}
+
export interface PersonResult {
id: number;
name: string;
@@ -53,7 +66,7 @@ export interface PersonResult {
knownFor: (MovieResult | TvResult)[];
}
-export type Results = MovieResult | TvResult | PersonResult;
+export type Results = MovieResult | TvResult | PersonResult | CollectionResult;
export const mapMovieResult = (
movieResult: TmdbMovieResult,
@@ -99,6 +112,20 @@ export const mapTvResult = (
mediaInfo: media,
});
+export const mapCollectionResult = (
+ collectionResult: TmdbCollectionResult
+): CollectionResult => ({
+ id: collectionResult.id,
+ mediaType: collectionResult.media_type || 'collection',
+ adult: collectionResult.adult,
+ originalLanguage: collectionResult.original_language,
+ originalTitle: collectionResult.original_title,
+ title: collectionResult.title,
+ overview: collectionResult.overview,
+ backdropPath: collectionResult.backdrop_path,
+ posterPath: collectionResult.poster_path,
+});
+
export const mapPersonResult = (
personResult: TmdbPersonResult
): PersonResult => ({
@@ -118,7 +145,12 @@ export const mapPersonResult = (
});
export const mapSearchResults = (
- results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[],
+ results: (
+ | TmdbMovieResult
+ | TmdbTvResult
+ | TmdbPersonResult
+ | TmdbCollectionResult
+ )[],
media?: Media[]
): Results[] =>
results.map((result) => {
@@ -139,6 +171,8 @@ export const mapSearchResults = (
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
)
);
+ case 'collection':
+ return mapCollectionResult(result);
default:
return mapPersonResult(result);
}
diff --git a/server/routes/discover.ts b/server/routes/discover.ts
index f032fa66b..47492fc06 100644
--- a/server/routes/discover.ts
+++ b/server/routes/discover.ts
@@ -14,12 +14,13 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { mapProductionCompany } from '@server/models/Movie';
import {
+ mapCollectionResult,
mapMovieResult,
mapPersonResult,
mapTvResult,
} from '@server/models/Search';
import { mapNetwork } from '@server/models/Tv';
-import { isMovie, isPerson } from '@server/utils/typeHelpers';
+import { isCollection, isMovie, isPerson } from '@server/utils/typeHelpers';
import { Router } from 'express';
import { sortBy } from 'lodash';
import { z } from 'zod';
@@ -647,6 +648,8 @@ discoverRoutes.get('/trending', async (req, res, next) => {
)
: isPerson(result)
? mapPersonResult(result)
+ : isCollection(result)
+ ? mapCollectionResult(result)
: mapTvResult(
result,
media.find(
diff --git a/server/utils/typeHelpers.ts b/server/utils/typeHelpers.ts
index 507ece8cd..548378ff7 100644
--- a/server/utils/typeHelpers.ts
+++ b/server/utils/typeHelpers.ts
@@ -1,4 +1,5 @@
import type {
+ TmdbCollectionResult,
TmdbMovieDetails,
TmdbMovieResult,
TmdbPersonDetails,
@@ -8,17 +9,35 @@ import type {
} from '@server/api/themoviedb/interfaces';
export const isMovie = (
- movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
+ movie:
+ | TmdbMovieResult
+ | TmdbTvResult
+ | TmdbPersonResult
+ | TmdbCollectionResult
): movie is TmdbMovieResult => {
return (movie as TmdbMovieResult).title !== undefined;
};
export const isPerson = (
- person: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
+ person:
+ | TmdbMovieResult
+ | TmdbTvResult
+ | TmdbPersonResult
+ | TmdbCollectionResult
): person is TmdbPersonResult => {
return (person as TmdbPersonResult).known_for !== undefined;
};
+export const isCollection = (
+ collection:
+ | TmdbMovieResult
+ | TmdbTvResult
+ | TmdbPersonResult
+ | TmdbCollectionResult
+): collection is TmdbCollectionResult => {
+ return (collection as TmdbCollectionResult).media_type === 'collection';
+};
+
export const isMovieDetails = (
movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
): movie is TmdbMovieDetails => {
diff --git a/src/components/Common/ListView/index.tsx b/src/components/Common/ListView/index.tsx
index 6f09f768b..b46086862 100644
--- a/src/components/Common/ListView/index.tsx
+++ b/src/components/Common/ListView/index.tsx
@@ -5,6 +5,7 @@ import useVerticalScroll from '@app/hooks/useVerticalScroll';
import globalMessages from '@app/i18n/globalMessages';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import type {
+ CollectionResult,
MovieResult,
PersonResult,
TvResult,
@@ -12,7 +13,7 @@ import type {
import { useIntl } from 'react-intl';
type ListViewProps = {
- items?: (TvResult | MovieResult | PersonResult)[];
+ items?: (TvResult | MovieResult | PersonResult | CollectionResult)[];
plexItems?: WatchlistItem[];
isEmpty?: boolean;
isLoading?: boolean;
@@ -90,6 +91,18 @@ const ListView = ({
/>
);
break;
+ case 'collection':
+ titleCard = (
+
+ );
+ break;
case 'person':
titleCard = (
{mediaType === 'movie'
? intl.formatMessage(globalMessages.movie)
+ : mediaType === 'collection'
+ ? intl.formatMessage(globalMessages.collection)
: intl.formatMessage(globalMessages.tvshow)}
@@ -177,7 +187,15 @@ const TitleCard = ({
leaveTo="opacity-0"
>
-
+
Date: Thu, 11 May 2023 09:59:15 +0900
Subject: [PATCH 04/34] docs: add Alexays as a contributor for code (#3452)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 113d0af47..180aea27a 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -809,6 +809,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "Alexays",
+ "name": "Alex",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13947260?v=4",
+ "profile": "https://arouillard.fr",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index 83fff5ea7..9cf0b0bb4 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -187,6 +187,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Owen Voke 💻 |
Sebastian K 💻 |
jariz 💻 |
+
Alex 💻 |
From ac77b037d5fb0c54f5edf4b29d04adb57aef388f Mon Sep 17 00:00:00 2001
From: Zeb Muller
Date: Thu, 11 May 2023 11:45:23 +1000
Subject: [PATCH 05/34] fix: error deleting users with over 1000 requests
(#3376)
Break-up request removal into groups of 1000 requests to be removed at a time.
---
server/routes/user/index.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts
index ea709caf5..94784df51 100644
--- a/server/routes/user/index.ts
+++ b/server/routes/user/index.ts
@@ -381,7 +381,14 @@ router.delete<{ id: string }>(
* we manually remove all requests from the user here so the parent media's
* properly reflect the change.
*/
- await requestRepository.remove(user.requests);
+ await requestRepository.remove(user.requests, {
+ /**
+ * Break-up into groups of 1000 requests to be removed at a time.
+ * Necessary for users with >1000 requests, else an SQLite 'Expression tree is too large' error occurs.
+ * https://typeorm.io/repository-api#additional-options
+ */
+ chunk: user.requests.length / 1000,
+ });
await userRepository.delete(user.id);
return res.status(200).json(user.filter());
From cd1cacad5589e188406466c498d98f2608d40f85 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 11 May 2023 10:45:54 +0900
Subject: [PATCH 06/34] docs: add Zebebles as a contributor for code (#3453)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 180aea27a..de5ea4913 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -818,6 +818,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "Zebebles",
+ "name": "Zeb Muller",
+ "avatar_url": "https://avatars.githubusercontent.com/u/11425451?v=4",
+ "profile": "https://github.com/Zebebles",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index 9cf0b0bb4..c50ce854c 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -188,6 +188,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Sebastian K 💻 |
jariz 💻 |
Alex 💻 |
+ Zeb Muller 💻 |
From c1e10338c1a5d1e5d4928428af7edeacfc428916 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Wed, 10 May 2023 22:59:12 -0400
Subject: [PATCH 07/34] refactor: pull to refresh (#3391)
* refactor: decoupled PTR by removing import and creating new touch logic
* fix: overscroll behavior on mobile is now prevented on the y axis
* feat: added shadow effects to icon
* fix: modified cypress test
* fix: added better scroll lock functionality
* fix: hide icon if scroll value is negative
* fix: changed to allow usage on all touch devices
---
cypress/e2e/pull-to-refresh.cy.ts | 2 +-
package.json | 2 -
src/components/Layout/PullToRefresh/index.tsx | 118 ++++++++++++++++++
src/components/Layout/index.tsx | 2 +-
src/components/PullToRefresh/index.tsx | 45 -------
src/styles/globals.css | 2 +-
yarn.lock | 10 --
7 files changed, 121 insertions(+), 60 deletions(-)
create mode 100644 src/components/Layout/PullToRefresh/index.tsx
delete mode 100644 src/components/PullToRefresh/index.tsx
diff --git a/cypress/e2e/pull-to-refresh.cy.ts b/cypress/e2e/pull-to-refresh.cy.ts
index d56c55897..732ee4137 100644
--- a/cypress/e2e/pull-to-refresh.cy.ts
+++ b/cypress/e2e/pull-to-refresh.cy.ts
@@ -13,7 +13,7 @@ describe('Pull To Refresh', () => {
url: '/api/v1/*',
}).as('apiCall');
- cy.get('.searchbar').swipe('bottom', [190, 400]);
+ cy.get('.searchbar').swipe('bottom', [190, 500]);
cy.wait('@apiCall').then((interception) => {
assert.isNotNull(
diff --git a/package.json b/package.json
index 5dff068ee..8b82e45d4 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,6 @@
"openpgp": "5.7.0",
"plex-api": "5.3.2",
"pug": "3.0.2",
- "pulltorefreshjs": "0.1.22",
"react": "18.2.0",
"react-ace": "10.1.0",
"react-animate-height": "2.1.2",
@@ -121,7 +120,6 @@
"@types/node": "17.0.36",
"@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7",
- "@types/pulltorefreshjs": "0.1.5",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react-transition-group": "4.4.5",
diff --git a/src/components/Layout/PullToRefresh/index.tsx b/src/components/Layout/PullToRefresh/index.tsx
new file mode 100644
index 000000000..cdedcf43c
--- /dev/null
+++ b/src/components/Layout/PullToRefresh/index.tsx
@@ -0,0 +1,118 @@
+import { ArrowPathIcon } from '@heroicons/react/24/outline';
+import { useRouter } from 'next/router';
+import { useEffect, useRef, useState } from 'react';
+
+const PullToRefresh = () => {
+ const router = useRouter();
+
+ const [pullStartPoint, setPullStartPoint] = useState(0);
+ const [pullChange, setPullChange] = useState(0);
+ const refreshDiv = useRef(null);
+
+ // Various pull down thresholds that determine icon location
+ const pullDownInitThreshold = pullChange > 20;
+ const pullDownStopThreshold = 120;
+ const pullDownReloadThreshold = pullChange > 340;
+ const pullDownIconLocation = pullChange / 3;
+
+ useEffect(() => {
+ // Reload function that is called when reload threshold has been hit
+ // Add loading class to determine when to add spin animation
+ const forceReload = () => {
+ refreshDiv.current?.classList.add('loading');
+ setTimeout(() => {
+ router.reload();
+ }, 1000);
+ };
+
+ const html = document.querySelector('html');
+
+ // Determines if we are at the top of the page
+ // Locks or unlocks page when pulling down to refresh
+ const pullStart = (e: TouchEvent) => {
+ setPullStartPoint(e.targetTouches[0].screenY);
+
+ if (window.scrollY === 0 && window.scrollX === 0) {
+ refreshDiv.current?.classList.add('block');
+ refreshDiv.current?.classList.remove('hidden');
+ document.body.style.touchAction = 'none';
+ document.body.style.overscrollBehavior = 'none';
+ if (html) {
+ html.style.overscrollBehaviorY = 'none';
+ }
+ } else {
+ refreshDiv.current?.classList.remove('block');
+ refreshDiv.current?.classList.add('hidden');
+ }
+ };
+
+ // Tracks how far we have pulled down the refresh icon
+ const pullDown = async (e: TouchEvent) => {
+ const screenY = e.targetTouches[0].screenY;
+
+ const pullLength =
+ pullStartPoint < screenY ? Math.abs(screenY - pullStartPoint) : 0;
+
+ setPullChange(pullLength);
+ };
+
+ // Will reload the page if we are past the threshold
+ // Otherwise, we reset the pull
+ const pullFinish = () => {
+ setPullStartPoint(0);
+
+ if (pullDownReloadThreshold) {
+ forceReload();
+ } else {
+ setPullChange(0);
+ }
+
+ document.body.style.touchAction = 'auto';
+ document.body.style.overscrollBehaviorY = 'auto';
+ if (html) {
+ html.style.overscrollBehaviorY = 'auto';
+ }
+ };
+
+ window.addEventListener('touchstart', pullStart, { passive: false });
+ window.addEventListener('touchmove', pullDown, { passive: false });
+ window.addEventListener('touchend', pullFinish, { passive: false });
+
+ return () => {
+ window.removeEventListener('touchstart', pullStart);
+ window.removeEventListener('touchmove', pullDown);
+ window.removeEventListener('touchend', pullFinish);
+ };
+ }, [pullDownInitThreshold, pullDownReloadThreshold, pullStartPoint, router]);
+
+ return (
+
+ );
+};
+
+export default PullToRefresh;
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index b30b9712a..878f27b18 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -1,8 +1,8 @@
import MobileMenu from '@app/components/Layout/MobileMenu';
+import PullToRefresh from '@app/components/Layout/PullToRefresh';
import SearchInput from '@app/components/Layout/SearchInput';
import Sidebar from '@app/components/Layout/Sidebar';
import UserDropdown from '@app/components/Layout/UserDropdown';
-import PullToRefresh from '@app/components/PullToRefresh';
import type { AvailableLocale } from '@app/context/LanguageContext';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
diff --git a/src/components/PullToRefresh/index.tsx b/src/components/PullToRefresh/index.tsx
deleted file mode 100644
index 68939c486..000000000
--- a/src/components/PullToRefresh/index.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ArrowPathIcon } from '@heroicons/react/24/outline';
-import { useRouter } from 'next/router';
-import PR from 'pulltorefreshjs';
-import { useEffect } from 'react';
-import ReactDOMServer from 'react-dom/server';
-
-const PullToRefresh = () => {
- const router = useRouter();
-
- useEffect(() => {
- PR.init({
- mainElement: '#pull-to-refresh',
- onRefresh() {
- router.reload();
- },
- iconArrow: ReactDOMServer.renderToString(
-
- ),
- iconRefreshing: ReactDOMServer.renderToString(
-
- ),
- instructionsPullToRefresh: ReactDOMServer.renderToString(),
- instructionsReleaseToRefresh: ReactDOMServer.renderToString(),
- instructionsRefreshing: ReactDOMServer.renderToString(),
- distReload: 60,
- distIgnore: 15,
- shouldPullToRefresh: () =>
- !window.scrollY && document.body.style.overflow !== 'hidden',
- });
- return () => {
- PR.destroyAll();
- };
- }, [router]);
-
- return ;
-};
-
-export default PullToRefresh;
diff --git a/src/styles/globals.css b/src/styles/globals.css
index fac7272d7..8110e87e0 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -17,7 +17,7 @@
body {
@apply bg-gray-900;
- overscroll-behavior-y: contain;
+ -webkit-overflow-scrolling: touch;
}
code {
diff --git a/yarn.lock b/yarn.lock
index c95f591d7..886aee53e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3954,11 +3954,6 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
-"@types/pulltorefreshjs@0.1.5":
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/@types/pulltorefreshjs/-/pulltorefreshjs-0.1.5.tgz#f15c9dbc91b8fdd8135093d81ece9e9d4d2324d7"
- integrity sha512-/VRTgBettvBg1KI8mGnA9oeWs359tTXQ7qsxLuXnksL88jvK6ZNMStG5T9x9vUO9O7jLsgREB0cElz/BWFfdew==
-
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@@ -11434,11 +11429,6 @@ pug@3.0.2, pug@^3.0.2:
pug-runtime "^3.0.1"
pug-strip-comments "^2.0.0"
-pulltorefreshjs@0.1.22:
- version "0.1.22"
- resolved "https://registry.yarnpkg.com/pulltorefreshjs/-/pulltorefreshjs-0.1.22.tgz#ddb5e3feee0b2a49fd46e1b18e84fffef2c47ac0"
- integrity sha512-haxNVEHnS4NCQA7NeG7TSV69z4uqy/N7nfPRuc4dPWe8H6ygUrMjdNeohE+6v0lVVX/ukSjbLYwPUGUYtFKfvQ==
-
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
From 4bd87647d0551c20e13589a62690a6f3e5ad8ff7 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Thu, 11 May 2023 00:16:50 -0400
Subject: [PATCH 08/34] fix: corrected initial fallback data load on details
page (#3395)
---
src/pages/movie/[movieId]/index.tsx | 36 ++++++++++++++---------------
src/pages/tv/[tvId]/index.tsx | 34 +++++++++++++--------------
2 files changed, 34 insertions(+), 36 deletions(-)
diff --git a/src/pages/movie/[movieId]/index.tsx b/src/pages/movie/[movieId]/index.tsx
index 2bb971c18..053ee3ff3 100644
--- a/src/pages/movie/[movieId]/index.tsx
+++ b/src/pages/movie/[movieId]/index.tsx
@@ -1,7 +1,7 @@
import MovieDetails from '@app/components/MovieDetails';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import axios from 'axios';
-import type { NextPage } from 'next';
+import type { GetServerSideProps, NextPage } from 'next';
interface MoviePageProps {
movie?: MovieDetailsType;
@@ -11,25 +11,25 @@ const MoviePage: NextPage = ({ movie }) => {
return ;
};
-MoviePage.getInitialProps = async (ctx) => {
- if (ctx.req) {
- const response = await axios.get(
- `http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
- ctx.query.movieId
- }`,
- {
- headers: ctx.req?.headers?.cookie
- ? { cookie: ctx.req.headers.cookie }
- : undefined,
- }
- );
+export const getServerSideProps: GetServerSideProps = async (
+ ctx
+) => {
+ const response = await axios.get(
+ `http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
+ ctx.query.movieId
+ }`,
+ {
+ headers: ctx.req?.headers?.cookie
+ ? { cookie: ctx.req.headers.cookie }
+ : undefined,
+ }
+ );
- return {
+ return {
+ props: {
movie: response.data,
- };
- }
-
- return {};
+ },
+ };
};
export default MoviePage;
diff --git a/src/pages/tv/[tvId]/index.tsx b/src/pages/tv/[tvId]/index.tsx
index 69fe216fd..a8a3cbd7d 100644
--- a/src/pages/tv/[tvId]/index.tsx
+++ b/src/pages/tv/[tvId]/index.tsx
@@ -1,7 +1,7 @@
import TvDetails from '@app/components/TvDetails';
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import axios from 'axios';
-import type { NextPage } from 'next';
+import type { GetServerSideProps, NextPage } from 'next';
interface TvPageProps {
tv?: TvDetailsType;
@@ -11,25 +11,23 @@ const TvPage: NextPage = ({ tv }) => {
return ;
};
-TvPage.getInitialProps = async (ctx) => {
- if (ctx.req) {
- const response = await axios.get(
- `http://localhost:${process.env.PORT || 5055}/api/v1/tv/${
- ctx.query.tvId
- }`,
- {
- headers: ctx.req?.headers?.cookie
- ? { cookie: ctx.req.headers.cookie }
- : undefined,
- }
- );
+export const getServerSideProps: GetServerSideProps = async (
+ ctx
+) => {
+ const response = await axios.get(
+ `http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
+ {
+ headers: ctx.req?.headers?.cookie
+ ? { cookie: ctx.req.headers.cookie }
+ : undefined,
+ }
+ );
- return {
+ return {
+ props: {
tv: response.data,
- };
- }
-
- return {};
+ },
+ };
};
export default TvPage;
From c27f96096ac8cc6c387f9d1dde5b263576ac2132 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Thu, 11 May 2023 00:27:45 -0400
Subject: [PATCH 09/34] fix: lock body scroll when using webkit (#3399)
---
src/hooks/useLockBodyScroll.ts | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/hooks/useLockBodyScroll.ts b/src/hooks/useLockBodyScroll.ts
index 08d7e3b6b..e962447cf 100644
--- a/src/hooks/useLockBodyScroll.ts
+++ b/src/hooks/useLockBodyScroll.ts
@@ -15,13 +15,20 @@ export const useLockBodyScroll = (
disabled?: boolean
): void => {
useEffect(() => {
- const originalStyle = window.getComputedStyle(document.body).overflow;
+ const originalOverflowStyle = window.getComputedStyle(
+ document.body
+ ).overflow;
+ const originalTouchActionStyle = window.getComputedStyle(
+ document.body
+ ).touchAction;
if (isLocked && !disabled) {
document.body.style.overflow = 'hidden';
+ document.body.style.touchAction = 'none';
}
return () => {
if (!disabled) {
- document.body.style.overflow = originalStyle;
+ document.body.style.overflow = originalOverflowStyle;
+ document.body.style.touchAction = originalTouchActionStyle;
}
};
}, [isLocked, disabled]);
From e051b1dfea9c9320cc9dd420c475ae74cff0d901 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Thu, 11 May 2023 00:43:11 -0400
Subject: [PATCH 10/34] fix: correctly load series fallback modal with sonarr
v4 (#3451)
---
server/routes/service.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/server/routes/service.ts b/server/routes/service.ts
index b77d58c9d..083e1eb57 100644
--- a/server/routes/service.ts
+++ b/server/routes/service.ts
@@ -183,9 +183,7 @@ serviceRoutes.get<{ tmdbId: string }>(
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
- url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
- sonarrSettings.hostname
- }:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
+ url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
});
try {
From aa849776809dfe891e67ff4db6861ef44df1a774 Mon Sep 17 00:00:00 2001
From: Shane Friedman
Date: Fri, 12 May 2023 20:23:14 -0400
Subject: [PATCH 11/34] feat(discover): support filtering by tmdb user vote
count on discover page (#3407)
---
overseerr-api.yml | 20 +++++++++
server/api/themoviedb/index.ts | 12 ++++++
server/routes/discover.ts | 6 +++
.../Discover/FilterSlideover/index.tsx | 41 +++++++++++++++++++
src/components/Discover/constants.ts | 16 ++++++++
src/i18n/locale/en.json | 2 +
6 files changed, 97 insertions(+)
diff --git a/overseerr-api.yml b/overseerr-api.yml
index 542ac59f4..c8b528859 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -4186,6 +4186,16 @@ paths:
schema:
type: number
example: 10
+ - in: query
+ name: voteCountGte
+ schema:
+ type: number
+ example: 7
+ - in: query
+ name: voteCountLte
+ schema:
+ type: number
+ example: 10
- in: query
name: watchRegion
schema:
@@ -4465,6 +4475,16 @@ paths:
schema:
type: number
example: 10
+ - in: query
+ name: voteCountGte
+ schema:
+ type: number
+ example: 7
+ - in: query
+ name: voteCountLte
+ schema:
+ type: number
+ example: 10
- in: query
name: watchRegion
schema:
diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts
index 4c931ff97..ef36fcd6d 100644
--- a/server/api/themoviedb/index.ts
+++ b/server/api/themoviedb/index.ts
@@ -65,6 +65,8 @@ interface DiscoverMovieOptions {
withRuntimeLte?: string;
voteAverageGte?: string;
voteAverageLte?: string;
+ voteCountGte?: string;
+ voteCountLte?: string;
originalLanguage?: string;
genre?: string;
studio?: string;
@@ -83,6 +85,8 @@ interface DiscoverTvOptions {
withRuntimeLte?: string;
voteAverageGte?: string;
voteAverageLte?: string;
+ voteCountGte?: string;
+ voteCountLte?: string;
includeEmptyReleaseDate?: boolean;
originalLanguage?: string;
genre?: string;
@@ -460,6 +464,8 @@ class TheMovieDb extends ExternalAPI {
withRuntimeLte,
voteAverageGte,
voteAverageLte,
+ voteCountGte,
+ voteCountLte,
watchProviders,
watchRegion,
}: DiscoverMovieOptions = {}): Promise => {
@@ -504,6 +510,8 @@ class TheMovieDb extends ExternalAPI {
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
'vote_average.lte': voteAverageLte,
+ 'vote_count.gte': voteCountGte,
+ 'vote_count.lte': voteCountLte,
watch_region: watchRegion,
with_watch_providers: watchProviders,
},
@@ -530,6 +538,8 @@ class TheMovieDb extends ExternalAPI {
withRuntimeLte,
voteAverageGte,
voteAverageLte,
+ voteCountGte,
+ voteCountLte,
watchProviders,
watchRegion,
}: DiscoverTvOptions = {}): Promise => {
@@ -574,6 +584,8 @@ class TheMovieDb extends ExternalAPI {
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
'vote_average.lte': voteAverageLte,
+ 'vote_count.gte': voteCountGte,
+ 'vote_count.lte': voteCountLte,
with_watch_providers: watchProviders,
watch_region: watchRegion,
},
diff --git a/server/routes/discover.ts b/server/routes/discover.ts
index 47492fc06..487d1a329 100644
--- a/server/routes/discover.ts
+++ b/server/routes/discover.ts
@@ -65,6 +65,8 @@ const QueryFilterOptions = z.object({
withRuntimeLte: z.coerce.string().optional(),
voteAverageGte: z.coerce.string().optional(),
voteAverageLte: z.coerce.string().optional(),
+ voteCountGte: z.coerce.string().optional(),
+ voteCountLte: z.coerce.string().optional(),
network: z.coerce.string().optional(),
watchProviders: z.coerce.string().optional(),
watchRegion: z.coerce.string().optional(),
@@ -96,6 +98,8 @@ discoverRoutes.get('/movies', async (req, res, next) => {
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,
voteAverageLte: query.voteAverageLte,
+ voteCountGte: query.voteCountGte,
+ voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders,
watchRegion: query.watchRegion,
});
@@ -371,6 +375,8 @@ discoverRoutes.get('/tv', async (req, res, next) => {
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,
voteAverageLte: query.voteAverageLte,
+ voteCountGte: query.voteCountGte,
+ voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders,
watchRegion: query.watchRegion,
});
diff --git a/src/components/Discover/FilterSlideover/index.tsx b/src/components/Discover/FilterSlideover/index.tsx
index 10ee0fea2..83d5a2e49 100644
--- a/src/components/Discover/FilterSlideover/index.tsx
+++ b/src/components/Discover/FilterSlideover/index.tsx
@@ -35,8 +35,10 @@ const messages = defineMessages({
ratingText: 'Ratings between {minValue} and {maxValue}',
clearfilters: 'Clear Active Filters',
tmdbuserscore: 'TMDB User Score',
+ tmdbuservotecount: 'TMDB User Vote Count',
runtime: 'Runtime',
streamingservices: 'Streaming Services',
+ voteCount: 'Number of votes between {minValue} and {maxValue}',
});
type FilterSlideoverProps = {
@@ -246,6 +248,45 @@ const FilterSlideover = ({
})}
/>
+
+ {intl.formatMessage(messages.tmdbuservotecount)}
+
+
+ {
+ updateQueryParams(
+ 'voteCountGte',
+ min !== 0 && Number(currentFilters.voteCountLte) !== 1000
+ ? min.toString()
+ : undefined
+ );
+ }}
+ onUpdateMax={(max) => {
+ updateQueryParams(
+ 'voteCountLte',
+ max !== 1000 && Number(currentFilters.voteCountGte) !== 0
+ ? max.toString()
+ : undefined
+ );
+ }}
+ subText={intl.formatMessage(messages.voteCount, {
+ minValue: currentFilters.voteCountGte ?? 0,
+ maxValue: currentFilters.voteCountLte ?? 1000,
+ })}
+ />
+
{intl.formatMessage(messages.streamingservices)}
diff --git a/src/components/Discover/constants.ts b/src/components/Discover/constants.ts
index 802ba7c6e..0571f1fc7 100644
--- a/src/components/Discover/constants.ts
+++ b/src/components/Discover/constants.ts
@@ -104,6 +104,8 @@ export const QueryFilterOptions = z.object({
withRuntimeLte: z.string().optional(),
voteAverageGte: z.string().optional(),
voteAverageLte: z.string().optional(),
+ voteCountLte: z.string().optional(),
+ voteCountGte: z.string().optional(),
watchRegion: z.string().optional(),
watchProviders: z.string().optional(),
});
@@ -169,6 +171,14 @@ export const prepareFilterValues = (
filterValues.voteAverageLte = values.voteAverageLte;
}
+ if (values.voteCountGte) {
+ filterValues.voteCountGte = values.voteCountGte;
+ }
+
+ if (values.voteCountLte) {
+ filterValues.voteCountLte = values.voteCountLte;
+ }
+
if (values.watchProviders) {
filterValues.watchProviders = values.watchProviders;
}
@@ -190,6 +200,12 @@ export const countActiveFilters = (filterValues: FilterOptions): number => {
delete clonedFilters.voteAverageLte;
}
+ if (clonedFilters.voteCountGte || filterValues.voteCountLte) {
+ totalCount += 1;
+ delete clonedFilters.voteCountGte;
+ delete clonedFilters.voteCountLte;
+ }
+
if (clonedFilters.withRuntimeGte || filterValues.withRuntimeLte) {
totalCount += 1;
delete clonedFilters.withRuntimeGte;
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index 39d44bbc3..0098eb5c6 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -76,7 +76,9 @@
"components.Discover.FilterSlideover.streamingservices": "Streaming Services",
"components.Discover.FilterSlideover.studio": "Studio",
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
+ "components.Discover.FilterSlideover.tmdbuservotecount": "TMDB User Vote Count",
"components.Discover.FilterSlideover.to": "To",
+ "components.Discover.FilterSlideover.voteCount": "Number of votes between {minValue} and {maxValue}",
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
"components.Discover.NetworkSlider.networks": "Networks",
From b8e3c07c4760462d42dafb05ebaea8c19a8ec842 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sat, 13 May 2023 09:35:51 +0900
Subject: [PATCH 12/34] docs: add SMores as a contributor for code (#3455)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index de5ea4913..7cf0f74e8 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -827,6 +827,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "SMores",
+ "name": "Shane Friedman",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5354254?v=4",
+ "profile": "http://smoores.dev",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index c50ce854c..915c75c8f 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -189,6 +189,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
jariz 💻 |
Alex 💻 |
Zeb Muller 💻 |
+ Shane Friedman 💻 |
From 24f268b6cb67d9a8d8675cd6e09dd83a7f499add Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Izaac=20Br=C3=A5nn?=
Date: Mon, 29 May 2023 04:29:36 +0200
Subject: [PATCH 13/34] feat: auto tagging requested media with username
(#3338)
* feat: auto tagging requested media with username
Relating to discussion: https://github.com/sct/overseerr/discussions/3313
Adding an option to the Radarr and Sonarr service to enable automatic tagging
with the username requesting the media.
Current format, to reduce tag clutter if a user changes displayname:
`[user.id] - [user.displayName]`
* fix: modified new secondary tip language
---------
Co-authored-by: Brandon Cohen
---
server/entity/MediaRequest.ts | 64 +++++++++++++++++++
server/lib/settings.ts | 1 +
src/components/Settings/RadarrModal/index.tsx | 20 ++++++
src/components/Settings/SonarrModal/index.tsx | 20 ++++++
src/i18n/locale/en.json | 5 ++
5 files changed, 110 insertions(+)
diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index 61122afc3..5a8d3988c 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -764,6 +764,38 @@ export class MediaRequest {
return;
}
+ if (radarrSettings.tagRequests) {
+ let userTag = (await radarr.getTags()).find((v) =>
+ v.label.startsWith(this.requestedBy.id + ' - ')
+ );
+ if (!userTag) {
+ logger.info(`Requester has no active tag. Creating new`, {
+ label: 'Media Request',
+ requestId: this.id,
+ mediaId: this.media.id,
+ userId: this.requestedBy.id,
+ newTag:
+ this.requestedBy.id + ' - ' + this.requestedBy.displayName,
+ });
+ userTag = await radarr.createTag({
+ label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
+ });
+ }
+ if (userTag.id) {
+ if (!tags?.find((v) => v === userTag?.id)) {
+ tags?.push(userTag.id);
+ }
+ } else {
+ logger.warn(`Requester has no tag and failed to add one`, {
+ label: 'Media Request',
+ requestId: this.id,
+ mediaId: this.media.id,
+ userId: this.requestedBy.id,
+ radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
+ });
+ }
+ }
+
if (
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
) {
@@ -1022,6 +1054,38 @@ export class MediaRequest {
});
}
+ if (sonarrSettings.tagRequests) {
+ let userTag = (await sonarr.getTags()).find((v) =>
+ v.label.startsWith(this.requestedBy.id + ' - ')
+ );
+ if (!userTag) {
+ logger.info(`Requester has no active tag. Creating new`, {
+ label: 'Media Request',
+ requestId: this.id,
+ mediaId: this.media.id,
+ userId: this.requestedBy.id,
+ newTag:
+ this.requestedBy.id + ' - ' + this.requestedBy.displayName,
+ });
+ userTag = await sonarr.createTag({
+ label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
+ });
+ }
+ if (userTag.id) {
+ if (!tags?.find((v) => v === userTag?.id)) {
+ tags?.push(userTag.id);
+ }
+ } else {
+ logger.warn(`Requester has no tag and failed to add one`, {
+ label: 'Media Request',
+ requestId: this.id,
+ mediaId: this.media.id,
+ userId: this.requestedBy.id,
+ sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
+ });
+ }
+ }
+
const sonarrSeriesOptions: AddSeriesOptions = {
profileId: qualityProfile,
languageProfileId: languageProfile,
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index 8e66ebc5d..c3981fe90 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -61,6 +61,7 @@ export interface DVRSettings {
externalUrl?: string;
syncEnabled: boolean;
preventSearch: boolean;
+ tagRequests: boolean;
}
export interface RadarrSettings extends DVRSettings {
diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx
index e74b0465f..4ebc7a8ba 100644
--- a/src/components/Settings/RadarrModal/index.tsx
+++ b/src/components/Settings/RadarrModal/index.tsx
@@ -57,6 +57,9 @@ const messages = defineMessages({
testFirstTags: 'Test connection to load tags',
tags: 'Tags',
enableSearch: 'Enable Automatic Search',
+ tagRequests: 'Tag Requests',
+ tagRequestsInfo:
+ "Automatically add an additional tag with the requester's user ID & display name",
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBaseUrlLeadingSlash: 'URL base must have a leading slash',
@@ -238,6 +241,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
externalUrl: radarr?.externalUrl,
syncEnabled: radarr?.syncEnabled ?? false,
enableSearch: !radarr?.preventSearch,
+ tagRequests: radarr?.tagRequests ?? false,
}}
validationSchema={RadarrSettingsSchema}
onSubmit={async (values) => {
@@ -263,6 +267,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
externalUrl: values.externalUrl,
syncEnabled: values.syncEnabled,
preventSearch: !values.enableSearch,
+ tagRequests: values.tagRequests,
};
if (!radarr) {
await axios.post('/api/v1/settings/radarr', submission);
@@ -713,6 +718,21 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
/>
+
+
+
+
+
+
);
diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx
index d9ff0c172..6c61d5dbf 100644
--- a/src/components/Settings/SonarrModal/index.tsx
+++ b/src/components/Settings/SonarrModal/index.tsx
@@ -62,6 +62,9 @@ const messages = defineMessages({
syncEnabled: 'Enable Scan',
externalUrl: 'External URL',
enableSearch: 'Enable Automatic Search',
+ tagRequests: 'Tag Requests',
+ tagRequestsInfo:
+ "Automatically add an additional tag with the requester's user ID & display name",
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBaseUrlLeadingSlash: 'Base URL must have a leading slash',
@@ -252,6 +255,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
externalUrl: sonarr?.externalUrl,
syncEnabled: sonarr?.syncEnabled ?? false,
enableSearch: !sonarr?.preventSearch,
+ tagRequests: sonarr?.tagRequests ?? false,
}}
validationSchema={SonarrSettingsSchema}
onSubmit={async (values) => {
@@ -292,6 +296,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
externalUrl: values.externalUrl,
syncEnabled: values.syncEnabled,
preventSearch: !values.enableSearch,
+ tagRequests: values.tagRequests,
};
if (!sonarr) {
await axios.post('/api/v1/settings/sonarr', submission);
@@ -960,6 +965,21 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
/>
+
+
+
+
+
+
);
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index 0098eb5c6..24a537a07 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -684,6 +684,8 @@
"components.Settings.RadarrModal.servername": "Server Name",
"components.Settings.RadarrModal.ssl": "Use SSL",
"components.Settings.RadarrModal.syncEnabled": "Enable Scan",
+ "components.Settings.RadarrModal.tagRequests": "Tag Requests",
+ "components.Settings.RadarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name",
"components.Settings.RadarrModal.tags": "Tags",
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
"components.Settings.RadarrModal.testFirstRootFolders": "Test connection to load root folders",
@@ -861,6 +863,8 @@
"components.Settings.SonarrModal.servername": "Server Name",
"components.Settings.SonarrModal.ssl": "Use SSL",
"components.Settings.SonarrModal.syncEnabled": "Enable Scan",
+ "components.Settings.SonarrModal.tagRequests": "Tag Requests",
+ "components.Settings.SonarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name",
"components.Settings.SonarrModal.tags": "Tags",
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles",
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
@@ -1178,6 +1182,7 @@
"i18n.cancel": "Cancel",
"i18n.canceling": "Canceling…",
"i18n.close": "Close",
+ "i18n.collection": "Collection",
"i18n.decline": "Decline",
"i18n.declined": "Declined",
"i18n.delete": "Delete",
From 0a007ca805dff334fc7972590c6f7bc85dce95a7 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 29 May 2023 11:32:08 +0900
Subject: [PATCH 14/34] docs: add IzaacJ as a contributor for code (#3473)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 12 +++++++++++-
README.md | 5 ++++-
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 7cf0f74e8..62df8b807 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -836,6 +836,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "IzaacJ",
+ "name": "Izaac Brånn",
+ "avatar_url": "https://avatars.githubusercontent.com/u/711323?v=4",
+ "profile": "https://izaacj.me",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
@@ -845,5 +854,6 @@
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": false,
- "commitConvention": "angular"
+ "commitConvention": "angular",
+ "commitType": "docs"
}
diff --git a/README.md b/README.md
index 915c75c8f..af2e7859d 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -191,6 +191,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Zeb Muller 💻 |
Shane Friedman 💻 |
+
+ Izaac Brånn 💻 |
+
From f33eb862fd85ff135828ab25c8d12d84e7e50762 Mon Sep 17 00:00:00 2001
From: Ryan Cohen
Date: Mon, 29 May 2023 11:36:26 +0900
Subject: [PATCH 15/34] chore: update codeowners (#3474) [skip ci]
---
.github/CODEOWNERS | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 6effae045..68a2d018e 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,10 +1,10 @@
# Global code ownership
-* @sct @TheCatLady @danshilm
+* @sct @TheCatLady @danshilm @OwsleyJr
# Documentation
-/.all-contributorsrc @TheCatLady @samwiseg0 @danshilm
-/*.md @TheCatLady @samwiseg0 @danshilm
-/docs/ @TheCatLady @samwiseg0 @danshilm
+/.all-contributorsrc @TheCatLady @samwiseg0 @danshilm @OwsleyJr
+/*.md @TheCatLady @samwiseg0 @danshilm @OwsleyJr
+/docs/ @TheCatLady @samwiseg0 @danshilm @OwsleyJr
# Snap-related files
/.github/workflows/snap.yaml @samwiseg0
@@ -12,4 +12,4 @@
# i18n locale files
/src/i18n/locale/ @sct @TheCatLady
-/src/i18n/locale/en.json @sct @TheCatLady @danshilm
+/src/i18n/locale/en.json @sct @TheCatLady @danshilm @OwsleyJr
From d7fa35e066cf371797aaa46ca464aa531ba8fb35 Mon Sep 17 00:00:00 2001
From: Salman Tariq
Date: Thu, 1 Jun 2023 02:53:50 +0500
Subject: [PATCH 16/34] fix(genreselector): fix searching in Genre filter
(#3468)
---
src/components/Selector/index.tsx | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx
index 8a21d3fbe..78ae33ea1 100644
--- a/src/components/Selector/index.tsx
+++ b/src/components/Selector/index.tsx
@@ -169,15 +169,19 @@ export const GenreSelector = ({
loadDefaultGenre();
}, [defaultValue, type]);
- const loadGenreOptions = async () => {
+ const loadGenreOptions = async (inputValue: string) => {
const results = await axios.get(
`/api/v1/discover/genreslider/${type}`
);
- return results.data.map((result) => ({
- label: result.name,
- value: result.id,
- }));
+ return results.data
+ .map((result) => ({
+ label: result.name,
+ value: result.id,
+ }))
+ .filter(({ label }) =>
+ label.toLowerCase().includes(inputValue.toLowerCase())
+ );
};
return (
From df332cec84f31e7a5152d9ad80f3f7839c0f3329 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 1 Jun 2023 00:24:05 +0000
Subject: [PATCH 17/34] docs: add SalmanTariq as a contributor for code (#3478)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 62df8b807..634d9050e 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -845,6 +845,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "SalmanTariq",
+ "name": "Salman Tariq",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13284494?v=4",
+ "profile": "https://github.com/SalmanTariq",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index af2e7859d..5aa90d257 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -193,6 +193,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Izaac Brånn 💻 |
+ Salman Tariq 💻 |
From 48f76662d5c08156f1da3f47e216c5f02668f64b Mon Sep 17 00:00:00 2001
From: Andrew Kennedy
Date: Fri, 9 Jun 2023 15:46:04 -0700
Subject: [PATCH 18/34] fix: make a (shallow) copy of radarr/sonarr tags into a
request before adding user tags (#3485)
* Make a (shallow) copy of radarr/sonarr tags into a request before adding user tags
* Undo random formatting changes
* more undoing formatting changes
* Fix undefined case.
* Prettier format
---
server/entity/MediaRequest.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index 5a8d3988c..e980860c6 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -704,7 +704,7 @@ export class MediaRequest {
let rootFolder = radarrSettings.activeDirectory;
let qualityProfile = radarrSettings.activeProfileId;
- let tags = radarrSettings.tags;
+ let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
if (
this.rootFolder &&
@@ -1002,7 +1002,11 @@ export class MediaRequest {
let tags =
seriesType === 'anime'
? sonarrSettings.animeTags
- : sonarrSettings.tags;
+ ? [...sonarrSettings.animeTags]
+ : []
+ : sonarrSettings.tags
+ ? [...sonarrSettings.tags]
+ : [];
if (
this.rootFolder &&
From 21231186d17df00073a8977bab9f7605736fd155 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 9 Jun 2023 23:20:02 +0000
Subject: [PATCH 19/34] docs: add andrew-kennedy as a contributor for code
(#3489) [skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 634d9050e..5ece548ea 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -854,6 +854,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "andrew-kennedy",
+ "name": "Andrew Kennedy",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2387159?v=4",
+ "profile": "https://github.com/andrew-kennedy",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index 5aa90d257..522988c86 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -194,6 +194,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Izaac Brånn 💻 |
Salman Tariq 💻 |
+ Andrew Kennedy 💻 |
From 04fbd00d4ac29045592588ef8b664d1916991e37 Mon Sep 17 00:00:00 2001
From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
Date: Sun, 11 Jun 2023 06:14:14 +0500
Subject: [PATCH 20/34] fix: fixes RT ratings for tv shows (#3492)
fix #3491
---
server/api/rottentomatoes.ts | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/server/api/rottentomatoes.ts b/server/api/rottentomatoes.ts
index 7695e3987..99a74eb1b 100644
--- a/server/api/rottentomatoes.ts
+++ b/server/api/rottentomatoes.ts
@@ -17,7 +17,7 @@ interface RTAlgoliaHit {
title: string;
titles: string[];
description: string;
- releaseYear: string;
+ releaseYear: number;
rating: string;
genres: string[];
updateDate: string;
@@ -111,22 +111,19 @@ class RottenTomatoes extends ExternalAPI {
// First, attempt to match exact name and year
let movie = contentResults.hits.find(
- (movie) => movie.releaseYear === year.toString() && movie.title === name
+ (movie) => movie.releaseYear === year && movie.title === name
);
// If we don't find a movie, try to match partial name and year
if (!movie) {
movie = contentResults.hits.find(
- (movie) =>
- movie.releaseYear === year.toString() && movie.title.includes(name)
+ (movie) => movie.releaseYear === year && movie.title.includes(name)
);
}
// If we still dont find a movie, try to match just on year
if (!movie) {
- movie = contentResults.hits.find(
- (movie) => movie.releaseYear === year.toString()
- );
+ movie = contentResults.hits.find((movie) => movie.releaseYear === year);
}
// One last try, try exact name match only
@@ -181,7 +178,7 @@ class RottenTomatoes extends ExternalAPI {
if (year) {
tvshow = contentResults.hits.find(
- (series) => series.releaseYear === year.toString()
+ (series) => series.releaseYear === year
);
}
From 24d94ef6fd79d1fc1cef5464bbff5e6d2ba0789e Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sun, 11 Jun 2023 10:15:55 +0900
Subject: [PATCH 21/34] docs: add Fallenbagel as a contributor for code (#3493)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 5ece548ea..371872759 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -863,6 +863,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "Fallenbagel",
+ "name": "Fallenbagel",
+ "avatar_url": "https://avatars.githubusercontent.com/u/98979876?v=4",
+ "profile": "https://github.com/Fallenbagel",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index 522988c86..6fe73c27d 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -195,6 +195,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Izaac Brånn 💻 |
Salman Tariq 💻 |
Andrew Kennedy 💻 |
+ Fallenbagel 💻 |
From c1a47bd9de332cb4925974690f5a33448b5cc2e6 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Mon, 12 Jun 2023 04:35:01 -0400
Subject: [PATCH 22/34] fix(ui): corrected issues icon color (#3498)
---
src/components/Layout/Sidebar/index.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx
index d9f8ffd51..81ebb86c7 100644
--- a/src/components/Layout/Sidebar/index.tsx
+++ b/src/components/Layout/Sidebar/index.tsx
@@ -71,9 +71,7 @@ const SidebarLinks: SidebarLinkProps[] = [
{
href: '/issues',
messagesKey: 'issues',
- svgIcon: (
-
- ),
+ svgIcon: ,
activeRegExp: /^\/issues/,
requiredPermission: [
Permission.MANAGE_ISSUES,
From 2c3f5330764492e1323afd2d1f25e28ad78a2f2f Mon Sep 17 00:00:00 2001
From: Ryan Cohen
Date: Tue, 13 Jun 2023 23:15:54 +0900
Subject: [PATCH 23/34] fix: adjust the plex watchlist sync schedule to have
fuzziness (#3502)
also fixes the schedule making it uneditable
---
server/job/schedule.ts | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/server/job/schedule.ts b/server/job/schedule.ts
index 998abf1f4..932d6107f 100644
--- a/server/job/schedule.ts
+++ b/server/job/schedule.ts
@@ -8,6 +8,7 @@ import type { JobId } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import watchlistSync from '@server/lib/watchlistsync';
import logger from '@server/logger';
+import random from 'lodash/random';
import schedule from 'node-schedule';
interface ScheduledJob {
@@ -60,21 +61,31 @@ export const startJobs = (): void => {
cancelFn: () => plexFullScanner.cancel(),
});
- // Run watchlist sync every 5 minutes
- scheduledJobs.push({
+ // Watchlist Sync
+ const watchlistSyncJob: ScheduledJob = {
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
- interval: 'minutes',
+ interval: 'fixed',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
- job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
+ job: schedule.scheduleJob(new Date(Date.now() + 1000 * 60 * 20), () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
label: 'Jobs',
});
watchlistSync.syncWatchlist();
}),
+ };
+
+ // To help alleviate load on Plex's servers, we will add some fuzziness to the next schedule
+ // after each run
+ watchlistSyncJob.job.on('run', () => {
+ watchlistSyncJob.job.schedule(
+ new Date(Math.floor(Date.now() + 1000 * 60 * random(14, 24, true)))
+ );
});
+ scheduledJobs.push(watchlistSyncJob);
+
// Run full radarr scan every 24 hours
scheduledJobs.push({
id: 'radarr-scan',
From d0836ce0efd55fccf2546087a0c4f94f7cb2e82a Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Wed, 14 Jun 2023 01:43:08 -0400
Subject: [PATCH 24/34] fix: improved handling of edge case that could cause
availability sync to fail (#3497)
---
server/lib/availabilitySync.ts | 303 ++++++++++++++++++---------------
1 file changed, 165 insertions(+), 138 deletions(-)
diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts
index a9f61fff6..231dd9a20 100644
--- a/server/lib/availabilitySync.ts
+++ b/server/lib/availabilitySync.ts
@@ -30,23 +30,24 @@ class AvailabilitySync {
this.sonarrSeasonsCache = {};
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
- await this.initPlexClient();
- if (!this.plexClient) {
- return;
- }
+ try {
+ await this.initPlexClient();
- logger.info(`Starting availability sync...`, {
- label: 'AvailabilitySync',
- });
- const mediaRepository = getRepository(Media);
- const requestRepository = getRepository(MediaRequest);
- const seasonRepository = getRepository(Season);
- const seasonRequestRepository = getRepository(SeasonRequest);
+ if (!this.plexClient) {
+ return;
+ }
- const pageSize = 50;
+ logger.info(`Starting availability sync...`, {
+ label: 'AvailabilitySync',
+ });
+ const mediaRepository = getRepository(Media);
+ const requestRepository = getRepository(MediaRequest);
+ const seasonRepository = getRepository(Season);
+ const seasonRequestRepository = getRepository(SeasonRequest);
+
+ const pageSize = 50;
- try {
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
if (!this.running) {
throw new Error('Job aborted');
@@ -239,51 +240,60 @@ class AvailabilitySync {
const isTVType = media.mediaType === 'tv';
- const request = await requestRepository.findOne({
- relations: {
- media: true,
- },
- where: { media: { id: media.id }, is4k: is4k ? true : false },
- });
-
- logger.info(
- `Media ID ${media.id} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
- isTVType ? 'Sonarr' : 'Radarr'
- } and Plex instance. Status will be changed to unknown.`,
- { label: 'AvailabilitySync' }
- );
-
- await mediaRepository.update(
- media.id,
- is4k
- ? {
- status4k: MediaStatus.UNKNOWN,
- serviceId4k: null,
- externalServiceId4k: null,
- externalServiceSlug4k: null,
- ratingKey4k: null,
- }
- : {
- status: MediaStatus.UNKNOWN,
- serviceId: null,
- externalServiceId: null,
- externalServiceSlug: null,
- ratingKey: null,
- }
- );
+ try {
+ const request = await requestRepository.findOne({
+ relations: {
+ media: true,
+ },
+ where: { media: { id: media.id }, is4k: is4k ? true : false },
+ });
- if (isTVType) {
- const seasonRepository = getRepository(Season);
+ logger.info(
+ `Media ID ${media.id} does not exist in your ${
+ is4k ? '4k' : 'non-4k'
+ } ${
+ isTVType ? 'Sonarr' : 'Radarr'
+ } and Plex instance. Status will be changed to unknown.`,
+ { label: 'AvailabilitySync' }
+ );
- await seasonRepository?.update(
- { media: { id: media.id } },
+ await mediaRepository.update(
+ media.id,
is4k
- ? { status4k: MediaStatus.UNKNOWN }
- : { status: MediaStatus.UNKNOWN }
+ ? {
+ status4k: MediaStatus.UNKNOWN,
+ serviceId4k: null,
+ externalServiceId4k: null,
+ externalServiceSlug4k: null,
+ ratingKey4k: null,
+ }
+ : {
+ status: MediaStatus.UNKNOWN,
+ serviceId: null,
+ externalServiceId: null,
+ externalServiceSlug: null,
+ ratingKey: null,
+ }
);
- }
- await requestRepository.delete({ id: request?.id });
+ if (isTVType) {
+ const seasonRepository = getRepository(Season);
+
+ await seasonRepository?.update(
+ { media: { id: media.id } },
+ is4k
+ ? { status4k: MediaStatus.UNKNOWN }
+ : { status: MediaStatus.UNKNOWN }
+ );
+ }
+
+ await requestRepository.delete({ id: request?.id });
+ } catch (ex) {
+ logger.debug(`Failure updating media ID ${media.id}`, {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ });
+ }
}
private async mediaExistsInRadarr(
@@ -539,83 +549,90 @@ class AvailabilitySync {
}
}
- const seasonToBeDeleted = await seasonRequestRepository.findOne({
- relations: {
- request: {
- media: true,
+ try {
+ const seasonToBeDeleted = await seasonRequestRepository.findOne({
+ relations: {
+ request: {
+ media: true,
+ },
},
- },
- where: {
- request: {
- is4k: seasonExistsInSonarr ? true : false,
- media: {
- id: media.id,
+ where: {
+ request: {
+ is4k: seasonExistsInSonarr ? true : false,
+ media: {
+ id: media.id,
+ },
},
+ seasonNumber: season.seasonNumber,
},
- seasonNumber: season.seasonNumber,
- },
- });
-
- // If season does not exist, we will change status to unknown and delete related season request
- // If parent media request is empty(all related seasons have been removed), parent is automatically deleted
- if (
- !seasonExistsInSonarr &&
- (seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
- !seasonExistsInPlex
- ) {
- if (season.status !== MediaStatus.UNKNOWN) {
- logger.info(
- `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
- { label: 'AvailabilitySync' }
- );
- await seasonRepository.update(season.id, {
- status: MediaStatus.UNKNOWN,
- });
-
- if (seasonToBeDeleted) {
- await seasonRequestRepository.remove(seasonToBeDeleted);
- }
+ });
- if (media.status === MediaStatus.AVAILABLE) {
+ // If season does not exist, we will change status to unknown and delete related season request
+ // If parent media request is empty(all related seasons have been removed), parent is automatically deleted
+ if (
+ !seasonExistsInSonarr &&
+ (seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
+ !seasonExistsInPlex
+ ) {
+ if (season.status !== MediaStatus.UNKNOWN) {
logger.info(
- `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
- await mediaRepository.update(media.id, {
- status: MediaStatus.PARTIALLY_AVAILABLE,
+ await seasonRepository.update(season.id, {
+ status: MediaStatus.UNKNOWN,
});
- }
- }
- }
- if (
- (seasonExistsInSonarr || seasonExistsInPlex) &&
- !seasonExistsInSonarr4k &&
- !seasonExistsInPlex4k
- ) {
- if (season.status4k !== MediaStatus.UNKNOWN) {
- logger.info(
- `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
- { label: 'AvailabilitySync' }
- );
- await seasonRepository.update(season.id, {
- status4k: MediaStatus.UNKNOWN,
- });
+ if (seasonToBeDeleted) {
+ await seasonRequestRepository.remove(seasonToBeDeleted);
+ }
- if (seasonToBeDeleted) {
- await seasonRequestRepository.remove(seasonToBeDeleted);
+ if (media.status === MediaStatus.AVAILABLE) {
+ logger.info(
+ `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ { label: 'AvailabilitySync' }
+ );
+ await mediaRepository.update(media.id, {
+ status: MediaStatus.PARTIALLY_AVAILABLE,
+ });
+ }
}
+ }
- if (media.status4k === MediaStatus.AVAILABLE) {
+ if (
+ (seasonExistsInSonarr || seasonExistsInPlex) &&
+ !seasonExistsInSonarr4k &&
+ !seasonExistsInPlex4k
+ ) {
+ if (season.status4k !== MediaStatus.UNKNOWN) {
logger.info(
- `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
- await mediaRepository.update(media.id, {
- status4k: MediaStatus.PARTIALLY_AVAILABLE,
+ await seasonRepository.update(season.id, {
+ status4k: MediaStatus.UNKNOWN,
});
+
+ if (seasonToBeDeleted) {
+ await seasonRequestRepository.remove(seasonToBeDeleted);
+ }
+
+ if (media.status4k === MediaStatus.AVAILABLE) {
+ logger.info(
+ `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ { label: 'AvailabilitySync' }
+ );
+ await mediaRepository.update(media.id, {
+ status4k: MediaStatus.PARTIALLY_AVAILABLE,
+ });
+ }
}
}
+ } catch (ex) {
+ logger.debug(`Failure updating media ID ${media.id}`, {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ });
}
if (
@@ -654,7 +671,10 @@ class AvailabilitySync {
}
} catch (ex) {
if (!ex.message.includes('response code: 404')) {
- throw ex;
+ logger.debug(`Failed to retrieve plex metadata`, {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ });
}
}
// Base case if both media versions exist in plex
@@ -714,36 +734,43 @@ class AvailabilitySync {
let seasonExistsInPlex = false;
let seasonExistsInPlex4k = false;
- if (ratingKey) {
- const children =
- this.plexSeasonsCache[ratingKey] ??
- (await this.plexClient?.getChildrenMetadata(ratingKey)) ??
- [];
- this.plexSeasonsCache[ratingKey] = children;
- const seasonMeta = children?.find(
- (child) => child.index === season.seasonNumber
- );
+ try {
+ if (ratingKey) {
+ const children =
+ this.plexSeasonsCache[ratingKey] ??
+ (await this.plexClient?.getChildrenMetadata(ratingKey)) ??
+ [];
+ this.plexSeasonsCache[ratingKey] = children;
+ const seasonMeta = children?.find(
+ (child) => child.index === season.seasonNumber
+ );
- if (seasonMeta) {
- seasonExistsInPlex = true;
+ if (seasonMeta) {
+ seasonExistsInPlex = true;
+ }
}
- }
-
- if (ratingKey4k) {
- const children4k =
- this.plexSeasonsCache[ratingKey4k] ??
- (await this.plexClient?.getChildrenMetadata(ratingKey4k)) ??
- [];
- this.plexSeasonsCache[ratingKey4k] = children4k;
- const seasonMeta4k = children4k?.find(
- (child) => child.index === season.seasonNumber
- );
+ if (ratingKey4k) {
+ const children4k =
+ this.plexSeasonsCache[ratingKey4k] ??
+ (await this.plexClient?.getChildrenMetadata(ratingKey4k)) ??
+ [];
+ this.plexSeasonsCache[ratingKey4k] = children4k;
+ const seasonMeta4k = children4k?.find(
+ (child) => child.index === season.seasonNumber
+ );
- if (seasonMeta4k) {
- seasonExistsInPlex4k = true;
+ if (seasonMeta4k) {
+ seasonExistsInPlex4k = true;
+ }
+ }
+ } catch (ex) {
+ if (!ex.message.includes('response code: 404')) {
+ logger.debug(`Failed to retrieve plex's children metadata`, {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ });
}
}
-
// Base case if both season versions exist in plex
if (seasonExistsInPlex && seasonExistsInPlex4k) {
return true;
From a761b7dd35a5bd61bb4eb0275b75d1e0977e6a2d Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Wed, 21 Jun 2023 13:18:50 -0400
Subject: [PATCH 25/34] fix: resolved issue with create slider causing
incorrect form submission (#3514)
---
src/components/Selector/index.tsx | 1 +
src/components/Slider/index.tsx | 2 ++
2 files changed, 3 insertions(+)
diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx
index 78ae33ea1..7b2165872 100644
--- a/src/components/Selector/index.tsx
+++ b/src/components/Selector/index.tsx
@@ -437,6 +437,7 @@ export const WatchProviderSelector = ({
{otherProviders.length > 0 && (
@@ -165,6 +166,7 @@ const Slider = ({
}`}
onClick={() => slide(Direction.RIGHT)}
disabled={scrollPos.isEnd}
+ type="button"
>
From 01de972a8fe2ea3c18d5b2f426d01b5b14d142d4 Mon Sep 17 00:00:00 2001
From: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:34:10 -0700
Subject: [PATCH 26/34] fix(statusbadge): handle missing season/episode number
(#3526)
---
src/components/StatusBadge/index.tsx | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx
index 3dbe6e74b..b60b7af04 100644
--- a/src/components/StatusBadge/index.tsx
+++ b/src/components/StatusBadge/index.tsx
@@ -166,11 +166,11 @@ const StatusBadge = ({
{inProgress && (
<>
- {mediaType === 'tv' && (
+ {mediaType === 'tv' && downloadItem[0].episode && (
{intl.formatMessage(messages.seasonepisodenumber, {
- seasonNumber: downloadItem[0].episode?.seasonNumber,
- episodeNumber: downloadItem[0].episode?.episodeNumber,
+ seasonNumber: downloadItem[0].episode.seasonNumber,
+ episodeNumber: downloadItem[0].episode.episodeNumber,
})}
)}
@@ -219,11 +219,11 @@ const StatusBadge = ({
{inProgress && (
<>
- {mediaType === 'tv' && (
+ {mediaType === 'tv' && downloadItem[0].episode && (
{intl.formatMessage(messages.seasonepisodenumber, {
- seasonNumber: downloadItem[0].episode?.seasonNumber,
- episodeNumber: downloadItem[0].episode?.episodeNumber,
+ seasonNumber: downloadItem[0].episode.seasonNumber,
+ episodeNumber: downloadItem[0].episode.episodeNumber,
})}
)}
@@ -272,11 +272,11 @@ const StatusBadge = ({
{inProgress && (
<>
- {mediaType === 'tv' && (
+ {mediaType === 'tv' && downloadItem[0].episode && (
{intl.formatMessage(messages.seasonepisodenumber, {
- seasonNumber: downloadItem[0].episode?.seasonNumber,
- episodeNumber: downloadItem[0].episode?.episodeNumber,
+ seasonNumber: downloadItem[0].episode.seasonNumber,
+ episodeNumber: downloadItem[0].episode.episodeNumber,
})}
)}
From 2816c66300bf870d493c0665b0e984d60f707dfd Mon Sep 17 00:00:00 2001
From: "Anton K. (ai Doge)"
Date: Tue, 18 Jul 2023 01:03:52 -0400
Subject: [PATCH 27/34] fix: resolved user access check issue (#3551)
* fix: importing friends
update checkUserAccess to use getUsers
* refactor(server/api/plextv.ts): clean up
removed unused getFriends function, and its interface.
renamed friends variable.
---
server/api/plextv.ts | 32 ++------------------------------
1 file changed, 2 insertions(+), 30 deletions(-)
diff --git a/server/api/plextv.ts b/server/api/plextv.ts
index 76ee66188..704926895 100644
--- a/server/api/plextv.ts
+++ b/server/api/plextv.ts
@@ -82,21 +82,6 @@ interface ServerResponse {
};
}
-interface FriendResponse {
- MediaContainer: {
- User: {
- $: {
- id: string;
- title: string;
- username: string;
- email: string;
- thumb: string;
- };
- Server?: ServerResponse[];
- }[];
- };
-}
-
interface UsersResponse {
MediaContainer: {
User: {
@@ -234,19 +219,6 @@ class PlexTvAPI extends ExternalAPI {
}
}
- public async getFriends(): Promise {
- const response = await this.axios.get('/pms/friends/all', {
- transformResponse: [],
- responseType: 'text',
- });
-
- const parsedXml = (await xml2js.parseStringPromise(
- response.data
- )) as FriendResponse;
-
- return parsedXml;
- }
-
public async checkUserAccess(userId: number): Promise {
const settings = getSettings();
@@ -255,9 +227,9 @@ class PlexTvAPI extends ExternalAPI {
throw new Error('Plex is not configured!');
}
- const friends = await this.getFriends();
+ const usersResponse = await this.getUsers();
- const users = friends.MediaContainer.User;
+ const users = usersResponse.MediaContainer.User;
const user = users.find((u) => parseInt(u.$.id) === userId);
From 68c7b3650ec82437bdb128f72f734e227ad763cb Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Tue, 18 Jul 2023 05:17:07 +0000
Subject: [PATCH 28/34] docs: add scorp200 as a contributor for code (#3555)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 371872759..f29044994 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -872,6 +872,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "scorp200",
+ "name": "Anton K. (ai Doge)",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9427639?v=4",
+ "profile": "http://aidoge.xyz",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index 6fe73c27d..0f6e32413 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -196,6 +196,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Salman Tariq 💻 |
Andrew Kennedy 💻 |
Fallenbagel 💻 |
+ Anton K. (ai Doge) 💻 |
From 83b008c8391459bd02dc74bcdb0d8caf27207bdf Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Mon, 24 Jul 2023 02:33:12 -0400
Subject: [PATCH 29/34] fix: handle issue causing incorrect media to change to
unknown (#3516)
* fix: handle issue causing incorrect media to change back to unknown
* fix: prevent start if plex client is unavailable
* fix: initialize radarr and sonarr clients before the scan starts
* fix: compensate for multiple *arr servers
* fix: added a more reliable season lookup
* refactor: modified tuples to increase code readability
---
server/lib/availabilitySync.ts | 1096 +++++++++++++++-----------------
1 file changed, 502 insertions(+), 594 deletions(-)
diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts
index 231dd9a20..0a16302cc 100644
--- a/server/lib/availabilitySync.ts
+++ b/server/lib/availabilitySync.ts
@@ -1,14 +1,13 @@
import type { PlexMetadata } from '@server/api/plexapi';
import PlexAPI from '@server/api/plexapi';
-import type { RadarrMovie } from '@server/api/servarr/radarr';
-import RadarrAPI from '@server/api/servarr/radarr';
+import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr';
-import { MediaStatus } from '@server/constants/media';
+import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import MediaRequest from '@server/entity/MediaRequest';
-import Season from '@server/entity/Season';
+import type Season from '@server/entity/Season';
import SeasonRequest from '@server/entity/SeasonRequest';
import { User } from '@server/entity/User';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
@@ -18,8 +17,8 @@ import logger from '@server/logger';
class AvailabilitySync {
public running = false;
private plexClient: PlexAPI;
- private plexSeasonsCache: Record = {};
- private sonarrSeasonsCache: Record = {};
+ private plexSeasonsCache: Record;
+ private sonarrSeasonsCache: Record;
private radarrServers: RadarrSettings[];
private sonarrServers: SonarrSettings[];
@@ -32,166 +31,176 @@ class AvailabilitySync {
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
try {
- await this.initPlexClient();
-
- if (!this.plexClient) {
- return;
- }
-
logger.info(`Starting availability sync...`, {
label: 'AvailabilitySync',
});
- const mediaRepository = getRepository(Media);
- const requestRepository = getRepository(MediaRequest);
- const seasonRepository = getRepository(Season);
- const seasonRequestRepository = getRepository(SeasonRequest);
-
const pageSize = 50;
+ const userRepository = getRepository(User);
+ const admin = await userRepository.findOne({
+ select: { id: true, plexToken: true },
+ where: { id: 1 },
+ });
+
+ if (admin) {
+ this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
+ } else {
+ logger.error('An admin is not configured.');
+ }
+
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
if (!this.running) {
throw new Error('Job aborted');
}
- const mediaExists = await this.mediaExists(media);
+ // Check plex, radarr, and sonarr for that specific media and
+ // if unavailable, then we change the status accordingly.
+ // If a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
+ if (media.mediaType === 'movie') {
+ let movieExists = false;
+ let movieExists4k = false;
+
+ const { existsInPlex } = await this.mediaExistsInPlex(media, false);
+ const { existsInPlex: existsInPlex4k } = await this.mediaExistsInPlex(
+ media,
+ true
+ );
+
+ const existsInRadarr = await this.mediaExistsInRadarr(media, false);
+ const existsInRadarr4k = await this.mediaExistsInRadarr(media, true);
- // We can not delete media so if both versions do not exist, we will change both columns to unknown or null
- if (!mediaExists) {
- if (
- media.status !== MediaStatus.UNKNOWN ||
- media.status4k !== MediaStatus.UNKNOWN
- ) {
- const request = await requestRepository.find({
- relations: {
- media: true,
- },
- where: { media: { id: media.id } },
- });
+ if (existsInPlex || existsInRadarr) {
+ movieExists = true;
+ logger.info(
+ `The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
+ {
+ label: 'AvailabilitySync',
+ }
+ );
+ }
+ if (existsInPlex4k || existsInRadarr4k) {
+ movieExists4k = true;
logger.info(
- `Media ID ${media.id} does not exist in any of your media instances. Status will be changed to unknown.`,
- { label: 'AvailabilitySync' }
+ `The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
+ {
+ label: 'AvailabilitySync',
+ }
);
+ }
- await mediaRepository.update(media.id, {
- status: MediaStatus.UNKNOWN,
- status4k: MediaStatus.UNKNOWN,
- serviceId: null,
- serviceId4k: null,
- externalServiceId: null,
- externalServiceId4k: null,
- externalServiceSlug: null,
- externalServiceSlug4k: null,
- ratingKey: null,
- ratingKey4k: null,
- });
-
- await requestRepository.remove(request);
+ if (!movieExists && media.status === MediaStatus.AVAILABLE) {
+ await this.mediaUpdater(media, false);
+ }
+
+ if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) {
+ await this.mediaUpdater(media, true);
}
}
+ // If both versions still exist in plex, we still need
+ // to check through sonarr to verify season availability
if (media.mediaType === 'tv') {
- // ok, the show itself exists, but do all it's seasons?
- const seasons = await seasonRepository.find({
- where: [
- { status: MediaStatus.AVAILABLE, media: { id: media.id } },
- {
- status: MediaStatus.PARTIALLY_AVAILABLE,
- media: { id: media.id },
- },
- { status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
+ let showExists = false;
+ let showExists4k = false;
+
+ const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } =
+ await this.mediaExistsInPlex(media, false);
+ const {
+ existsInPlex: existsInPlex4k,
+ seasonsMap: plexSeasonsMap4k = new Map(),
+ } = await this.mediaExistsInPlex(media, true);
+
+ const { existsInSonarr, seasonsMap: sonarrSeasonsMap } =
+ await this.mediaExistsInSonarr(media, false);
+ const {
+ existsInSonarr: existsInSonarr4k,
+ seasonsMap: sonarrSeasonsMap4k,
+ } = await this.mediaExistsInSonarr(media, true);
+
+ if (existsInPlex || existsInSonarr) {
+ showExists = true;
+ logger.info(
+ `The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
- status4k: MediaStatus.PARTIALLY_AVAILABLE,
- media: { id: media.id },
- },
- ],
- });
-
- let didDeleteSeasons = false;
- for (const season of seasons) {
- if (
- !mediaExists &&
- (season.status !== MediaStatus.UNKNOWN ||
- season.status4k !== MediaStatus.UNKNOWN)
- ) {
- await seasonRepository.update(
- { id: season.id },
- {
- status: MediaStatus.UNKNOWN,
- status4k: MediaStatus.UNKNOWN,
- }
- );
- } else {
- const seasonExists = await this.seasonExists(media, season);
-
- if (!seasonExists) {
- logger.info(
- `Removing season ${season.seasonNumber}, media ID ${media.id} because it does not exist in any of your media instances.`,
- { label: 'AvailabilitySync' }
- );
-
- if (
- season.status !== MediaStatus.UNKNOWN ||
- season.status4k !== MediaStatus.UNKNOWN
- ) {
- await seasonRepository.update(
- { id: season.id },
- {
- status: MediaStatus.UNKNOWN,
- status4k: MediaStatus.UNKNOWN,
- }
- );
- }
-
- const seasonToBeDeleted = await seasonRequestRepository.findOne(
- {
- relations: {
- request: {
- media: true,
- },
- },
- where: {
- request: {
- media: {
- id: media.id,
- },
- },
- seasonNumber: season.seasonNumber,
- },
- }
- );
-
- if (seasonToBeDeleted) {
- await seasonRequestRepository.remove(seasonToBeDeleted);
- }
-
- didDeleteSeasons = true;
+ label: 'AvailabilitySync',
}
- }
+ );
+ }
- if (didDeleteSeasons) {
- if (
- media.status === MediaStatus.AVAILABLE ||
- media.status4k === MediaStatus.AVAILABLE
- ) {
- logger.info(
- `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
- { label: 'AvailabilitySync' }
- );
-
- if (media.status === MediaStatus.AVAILABLE) {
- await mediaRepository.update(media.id, {
- status: MediaStatus.PARTIALLY_AVAILABLE,
- });
- }
-
- if (media.status4k === MediaStatus.AVAILABLE) {
- await mediaRepository.update(media.id, {
- status4k: MediaStatus.PARTIALLY_AVAILABLE,
- });
- }
+ if (existsInPlex4k || existsInSonarr4k) {
+ showExists4k = true;
+ logger.info(
+ `The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
+ {
+ label: 'AvailabilitySync',
}
- }
+ );
+ }
+
+ // Here we will create a final map that will cross compare
+ // with plex and sonarr. Filtered seasons will go through
+ // each season and assume the season does not exist. If Plex or
+ // Sonarr finds that season, we will change the final seasons value
+ // to true.
+ const filteredSeasonsMap: Map = new Map();
+
+ media.seasons
+ .filter(
+ (season) =>
+ season.status === MediaStatus.AVAILABLE ||
+ season.status === MediaStatus.PARTIALLY_AVAILABLE
+ )
+ .forEach((season) =>
+ filteredSeasonsMap.set(season.seasonNumber, false)
+ );
+
+ const finalSeasons = new Map([
+ ...filteredSeasonsMap,
+ ...plexSeasonsMap,
+ ...sonarrSeasonsMap,
+ ]);
+
+ const filteredSeasonsMap4k: Map = new Map();
+
+ media.seasons
+ .filter(
+ (season) =>
+ season.status4k === MediaStatus.AVAILABLE ||
+ season.status4k === MediaStatus.PARTIALLY_AVAILABLE
+ )
+ .forEach((season) =>
+ filteredSeasonsMap4k.set(season.seasonNumber, false)
+ );
+
+ const finalSeasons4k = new Map([
+ ...filteredSeasonsMap4k,
+ ...plexSeasonsMap4k,
+ ...sonarrSeasonsMap4k,
+ ]);
+
+ if ([...finalSeasons.values()].includes(false)) {
+ await this.seasonUpdater(media, finalSeasons, false);
+ }
+
+ if ([...finalSeasons4k.values()].includes(false)) {
+ await this.seasonUpdater(media, finalSeasons4k, true);
+ }
+
+ if (
+ !showExists &&
+ (media.status === MediaStatus.AVAILABLE ||
+ media.status === MediaStatus.PARTIALLY_AVAILABLE)
+ ) {
+ await this.mediaUpdater(media, false);
+ }
+
+ if (
+ !showExists4k &&
+ (media.status4k === MediaStatus.AVAILABLE ||
+ media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
+ ) {
+ await this.mediaUpdater(media, true);
}
}
}
@@ -234,582 +243,481 @@ class AvailabilitySync {
} while (mediaPage.length > 0);
}
+ private findMediaStatus(
+ requests: MediaRequest[],
+ is4k: boolean
+ ): MediaStatus {
+ const filteredRequests = requests.filter(
+ (request) => request.is4k === is4k
+ );
+
+ let mediaStatus: MediaStatus;
+
+ if (
+ filteredRequests.some(
+ (request) => request.status === MediaRequestStatus.APPROVED
+ )
+ ) {
+ mediaStatus = MediaStatus.PROCESSING;
+ } else if (
+ filteredRequests.some(
+ (request) => request.status === MediaRequestStatus.PENDING
+ )
+ ) {
+ mediaStatus = MediaStatus.PENDING;
+ } else {
+ mediaStatus = MediaStatus.UNKNOWN;
+ }
+
+ return mediaStatus;
+ }
+
private async mediaUpdater(media: Media, is4k: boolean): Promise {
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
- const isTVType = media.mediaType === 'tv';
-
try {
- const request = await requestRepository.findOne({
- relations: {
- media: true,
- },
- where: { media: { id: media.id }, is4k: is4k ? true : false },
- });
+ // Find all related requests only if
+ // the related media has an available status
+ const requests = await requestRepository
+ .createQueryBuilder('request')
+ .leftJoinAndSelect('request.media', 'media')
+ .where('(media.id = :id)', {
+ id: media.id,
+ })
+ .andWhere(
+ `(request.is4k = :is4k AND media.${
+ is4k ? 'status4k' : 'status'
+ } IN (:...mediaStatus))`,
+ {
+ mediaStatus: [
+ MediaStatus.AVAILABLE,
+ MediaStatus.PARTIALLY_AVAILABLE,
+ ],
+ is4k: is4k,
+ }
+ )
+ .getMany();
+
+ // Check if a season is processing or pending to
+ // make sure we set the media to the correct status
+ let mediaStatus = MediaStatus.UNKNOWN;
+
+ if (media.mediaType === 'tv') {
+ mediaStatus = this.findMediaStatus(requests, is4k);
+ }
+
+ media[is4k ? 'status4k' : 'status'] = mediaStatus;
+ media[is4k ? 'serviceId4k' : 'serviceId'] =
+ mediaStatus === MediaStatus.PROCESSING
+ ? media[is4k ? 'serviceId4k' : 'serviceId']
+ : null;
+ media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
+ mediaStatus === MediaStatus.PROCESSING
+ ? media[is4k ? 'externalServiceId4k' : 'externalServiceId']
+ : null;
+ media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
+ mediaStatus === MediaStatus.PROCESSING
+ ? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
+ : null;
+ media[is4k ? 'ratingKey4k' : 'ratingKey'] =
+ mediaStatus === MediaStatus.PROCESSING
+ ? media[is4k ? 'ratingKey4k' : 'ratingKey']
+ : null;
logger.info(
- `Media ID ${media.id} does not exist in your ${
- is4k ? '4k' : 'non-4k'
- } ${
- isTVType ? 'Sonarr' : 'Radarr'
+ `The ${is4k ? '4K' : 'non-4K'} ${
+ media.mediaType === 'movie' ? 'movie' : 'show'
+ } [TMDB ID ${media.tmdbId}] was not found in any ${
+ media.mediaType === 'movie' ? 'Radarr' : 'Sonarr'
} and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
- await mediaRepository.update(
- media.id,
- is4k
- ? {
- status4k: MediaStatus.UNKNOWN,
- serviceId4k: null,
- externalServiceId4k: null,
- externalServiceSlug4k: null,
- ratingKey4k: null,
- }
- : {
- status: MediaStatus.UNKNOWN,
- serviceId: null,
- externalServiceId: null,
- externalServiceSlug: null,
- ratingKey: null,
- }
+ await mediaRepository.save({ media, ...media });
+
+ // Only delete media request if type is movie.
+ // Type tv request deletion is handled
+ // in the season request entity
+ if (requests.length > 0 && media.mediaType === 'movie') {
+ await requestRepository.remove(requests);
+ }
+ } catch (ex) {
+ logger.debug(
+ `Failure updating the ${is4k ? '4K' : 'non-4K'} ${
+ media.mediaType === 'tv' ? 'show' : 'movie'
+ } [TMDB ID ${media.tmdbId}].`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
);
+ }
+ }
- if (isTVType) {
- const seasonRepository = getRepository(Season);
+ private async seasonUpdater(
+ media: Media,
+ seasons: Map,
+ is4k: boolean
+ ): Promise {
+ const mediaRepository = getRepository(Media);
+ const seasonRequestRepository = getRepository(SeasonRequest);
- await seasonRepository?.update(
- { media: { id: media.id } },
- is4k
- ? { status4k: MediaStatus.UNKNOWN }
- : { status: MediaStatus.UNKNOWN }
+ const seasonsPendingRemoval = new Map(
+ // Disabled linter as only the value is needed from the filter
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ [...seasons].filter(([_, exists]) => !exists)
+ );
+ const seasonKeys = [...seasonsPendingRemoval.keys()];
+
+ try {
+ // Need to check and see if there are any related season
+ // requests. If they are, we will need to delete them.
+ const seasonRequests = await seasonRequestRepository
+ .createQueryBuilder('seasonRequest')
+ .leftJoinAndSelect('seasonRequest.request', 'request')
+ .leftJoinAndSelect('request.media', 'media')
+ .where('(media.id = :id)', { id: media.id })
+ .andWhere(
+ '(request.is4k = :is4k AND seasonRequest.seasonNumber IN (:...seasonNumbers))',
+ {
+ seasonNumbers: seasonKeys,
+ is4k: is4k,
+ }
+ )
+ .getMany();
+
+ for (const mediaSeason of media.seasons) {
+ if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) {
+ mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
+ }
+ }
+
+ if (media.status === MediaStatus.AVAILABLE) {
+ media.status = MediaStatus.PARTIALLY_AVAILABLE;
+ logger.info(
+ `Marking the non-4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ { label: 'AvailabilitySync' }
);
}
- await requestRepository.delete({ id: request?.id });
+ if (media.status4k === MediaStatus.AVAILABLE) {
+ media.status4k = MediaStatus.PARTIALLY_AVAILABLE;
+ logger.info(
+ `Marking the 4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
+ { label: 'AvailabilitySync' }
+ );
+ }
+
+ await mediaRepository.save({ media, ...media });
+
+ if (seasonRequests.length > 0) {
+ await seasonRequestRepository.remove(seasonRequests);
+ }
+
+ logger.info(
+ `The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${
+ media.tmdbId
+ }] was not found in any ${
+ media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
+ } and Plex instance. Status will be changed to unknown.`,
+ { label: 'AvailabilitySync' }
+ );
} catch (ex) {
- logger.debug(`Failure updating media ID ${media.id}`, {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- });
+ logger.debug(
+ `Failure updating the ${
+ is4k ? '4K' : 'non-4K'
+ } season(s) [${seasonKeys}], TMDB ID ${media.tmdbId}.`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
+ );
}
}
private async mediaExistsInRadarr(
media: Media,
- existsInPlex: boolean,
- existsInPlex4k: boolean
+ is4k: boolean
): Promise {
- let existsInRadarr = true;
- let existsInRadarr4k = true;
+ let existsInRadarr = false;
+ // Check for availability in all of the available radarr servers
+ // If any find the media, we will assume the media exists
for (const server of this.radarrServers) {
- const api = new RadarrAPI({
+ const radarrAPI = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
- try {
- // Check if both exist or if a single non-4k or 4k exists
- // If both do not exist we will return false
- let meta: RadarrMovie | undefined;
-
- if (!server.is4k && media.externalServiceId) {
- meta = await api.getMovie({ id: media.externalServiceId });
- }
+ try {
+ let radarr: RadarrMovie | undefined;
- if (server.is4k && media.externalServiceId4k) {
- meta = await api.getMovie({ id: media.externalServiceId4k });
+ if (!server.is4k && media.externalServiceId && !is4k) {
+ radarr = await radarrAPI.getMovie({
+ id: media.externalServiceId,
+ });
}
- if (!server.is4k && (!meta || !meta.hasFile)) {
- existsInRadarr = false;
+ if (server.is4k && media.externalServiceId4k && is4k) {
+ radarr = await radarrAPI.getMovie({
+ id: media.externalServiceId4k,
+ });
}
- if (server.is4k && (!meta || !meta.hasFile)) {
- existsInRadarr4k = false;
+ if (radarr && radarr.hasFile) {
+ existsInRadarr = true;
}
} catch (ex) {
- logger.debug(
- `Failure retrieving media ID ${media.id} from your ${
- !server.is4k ? 'non-4K' : '4K'
- } Radarr.`,
- {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- }
- );
- if (!server.is4k) {
- existsInRadarr = false;
- }
-
- if (server.is4k) {
- existsInRadarr4k = false;
+ if (!ex.message.includes('404')) {
+ existsInRadarr = true;
+ logger.debug(
+ `Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
+ media.tmdbId
+ }] from Radarr.`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
+ );
}
}
}
- // If only a single non-4k or 4k exists, then change entity columns accordingly
- // Related media request will then be deleted
- if (
- !existsInRadarr &&
- (existsInRadarr4k || existsInPlex4k) &&
- !existsInPlex
- ) {
- if (media.status !== MediaStatus.UNKNOWN) {
- this.mediaUpdater(media, false);
- }
- }
-
- if (
- (existsInRadarr || existsInPlex) &&
- !existsInRadarr4k &&
- !existsInPlex4k
- ) {
- if (media.status4k !== MediaStatus.UNKNOWN) {
- this.mediaUpdater(media, true);
- }
- }
-
- if (existsInRadarr || existsInRadarr4k || existsInPlex || existsInPlex4k) {
- return true;
- }
-
- return false;
+ return existsInRadarr;
}
private async mediaExistsInSonarr(
media: Media,
- existsInPlex: boolean,
- existsInPlex4k: boolean
- ): Promise {
- let existsInSonarr = true;
- let existsInSonarr4k = true;
+ is4k: boolean
+ ): Promise<{ existsInSonarr: boolean; seasonsMap: Map }> {
+ let existsInSonarr = false;
+ let preventSeasonSearch = false;
+ // Check for availability in all of the available sonarr servers
+ // If any find the media, we will assume the media exists
for (const server of this.sonarrServers) {
- const api = new SonarrAPI({
+ const sonarrAPI = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
- try {
- // Check if both exist or if a single non-4k or 4k exists
- // If both do not exist we will return false
- let meta: SonarrSeries | undefined;
+ try {
+ let sonarr: SonarrSeries | undefined;
- if (!server.is4k && media.externalServiceId) {
- meta = await api.getSeriesById(media.externalServiceId);
+ if (!server.is4k && media.externalServiceId && !is4k) {
+ sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
- meta.seasons;
+ sonarr.seasons;
}
- if (server.is4k && media.externalServiceId4k) {
- meta = await api.getSeriesById(media.externalServiceId4k);
+ if (server.is4k && media.externalServiceId4k && is4k) {
+ sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
- meta.seasons;
+ sonarr.seasons;
}
- if (!server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
- existsInSonarr = false;
- }
-
- if (server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
- existsInSonarr4k = false;
+ if (sonarr && sonarr.statistics.episodeFileCount > 0) {
+ existsInSonarr = true;
}
} catch (ex) {
- logger.debug(
- `Failure retrieving media ID ${media.id} from your ${
- !server.is4k ? 'non-4K' : '4K'
- } Sonarr.`,
- {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- }
- );
-
- if (!server.is4k) {
- existsInSonarr = false;
- }
-
- if (server.is4k) {
- existsInSonarr4k = false;
+ if (!ex.message.includes('404')) {
+ existsInSonarr = true;
+ preventSeasonSearch = true;
+ logger.debug(
+ `Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
+ media.tmdbId
+ }] from Sonarr.`,
+ {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ }
+ );
}
}
}
- // If only a single non-4k or 4k exists, then change entity columns accordingly
- // Related media request will then be deleted
- if (
- !existsInSonarr &&
- (existsInSonarr4k || existsInPlex4k) &&
- !existsInPlex
- ) {
- if (media.status !== MediaStatus.UNKNOWN) {
- this.mediaUpdater(media, false);
- }
- }
+ // Here we check each season for availability
+ // If the API returns an error other than a 404,
+ // we will have to prevent the season check from happening
+ const seasonsMap: Map = new Map();
+
+ if (!preventSeasonSearch) {
+ const filteredSeasons = media.seasons.filter(
+ (season) =>
+ season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
+ season[is4k ? 'status4k' : 'status'] ===
+ MediaStatus.PARTIALLY_AVAILABLE
+ );
- if (
- (existsInSonarr || existsInPlex) &&
- !existsInSonarr4k &&
- !existsInPlex4k
- ) {
- if (media.status4k !== MediaStatus.UNKNOWN) {
- this.mediaUpdater(media, true);
- }
- }
+ for (const season of filteredSeasons) {
+ const seasonExists = await this.seasonExistsInSonarr(
+ media,
+ season,
+ is4k
+ );
- if (existsInSonarr || existsInSonarr4k || existsInPlex || existsInPlex4k) {
- return true;
+ if (seasonExists) {
+ seasonsMap.set(season.seasonNumber, true);
+ }
+ }
}
- return false;
+ return { existsInSonarr, seasonsMap };
}
private async seasonExistsInSonarr(
media: Media,
season: Season,
- seasonExistsInPlex: boolean,
- seasonExistsInPlex4k: boolean
+ is4k: boolean
): Promise {
- let seasonExistsInSonarr = true;
- let seasonExistsInSonarr4k = true;
-
- const mediaRepository = getRepository(Media);
- const seasonRepository = getRepository(Season);
- const seasonRequestRepository = getRepository(SeasonRequest);
+ let seasonExists = false;
+ // Check each sonarr instance to see if the media still exists
+ // If found, we will assume the media exists and prevent removal
+ // We can use the cache we built when we fetched the series with mediaExistsInSonarr
for (const server of this.sonarrServers) {
- const api = new SonarrAPI({
- apiKey: server.apiKey,
- url: SonarrAPI.buildUrl(server, '/api/v3'),
- });
-
- try {
- // Here we can use the cache we built when we fetched the series with mediaExistsInSonarr
- // If the cache does not have data, we will fetch with the api route
-
- let seasons: SonarrSeason[] =
- this.sonarrSeasonsCache[
- `${server.id}-${
- !server.is4k ? media.externalServiceId : media.externalServiceId4k
- }`
- ];
-
- if (!server.is4k && media.externalServiceId) {
- seasons =
- this.sonarrSeasonsCache[
- `${server.id}-${media.externalServiceId}`
- ] ?? (await api.getSeriesById(media.externalServiceId)).seasons;
- this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
- seasons;
- }
+ let sonarrSeasons: SonarrSeason[] | undefined;
- if (server.is4k && media.externalServiceId4k) {
- seasons =
- this.sonarrSeasonsCache[
- `${server.id}-${media.externalServiceId4k}`
- ] ?? (await api.getSeriesById(media.externalServiceId4k)).seasons;
- this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
- seasons;
- }
-
- const seasonIsUnavailable = seasons?.find(
- ({ seasonNumber, statistics }) =>
- season.seasonNumber === seasonNumber &&
- statistics?.episodeFileCount === 0
- );
-
- if (!server.is4k && seasonIsUnavailable) {
- seasonExistsInSonarr = false;
- }
-
- if (server.is4k && seasonIsUnavailable) {
- seasonExistsInSonarr4k = false;
- }
- } catch (ex) {
- logger.debug(
- `Failure retrieving media ID ${media.id} from your ${
- !server.is4k ? 'non-4K' : '4K'
- } Sonarr.`,
- {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- }
- );
-
- if (!server.is4k) {
- seasonExistsInSonarr = false;
- }
-
- if (server.is4k) {
- seasonExistsInSonarr4k = false;
- }
+ if (media.externalServiceId && !is4k) {
+ sonarrSeasons =
+ this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`];
}
- }
-
- try {
- const seasonToBeDeleted = await seasonRequestRepository.findOne({
- relations: {
- request: {
- media: true,
- },
- },
- where: {
- request: {
- is4k: seasonExistsInSonarr ? true : false,
- media: {
- id: media.id,
- },
- },
- seasonNumber: season.seasonNumber,
- },
- });
- // If season does not exist, we will change status to unknown and delete related season request
- // If parent media request is empty(all related seasons have been removed), parent is automatically deleted
- if (
- !seasonExistsInSonarr &&
- (seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
- !seasonExistsInPlex
- ) {
- if (season.status !== MediaStatus.UNKNOWN) {
- logger.info(
- `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
- { label: 'AvailabilitySync' }
- );
- await seasonRepository.update(season.id, {
- status: MediaStatus.UNKNOWN,
- });
-
- if (seasonToBeDeleted) {
- await seasonRequestRepository.remove(seasonToBeDeleted);
- }
-
- if (media.status === MediaStatus.AVAILABLE) {
- logger.info(
- `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
- { label: 'AvailabilitySync' }
- );
- await mediaRepository.update(media.id, {
- status: MediaStatus.PARTIALLY_AVAILABLE,
- });
- }
- }
+ if (media.externalServiceId4k && is4k) {
+ sonarrSeasons =
+ this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`];
}
- if (
- (seasonExistsInSonarr || seasonExistsInPlex) &&
- !seasonExistsInSonarr4k &&
- !seasonExistsInPlex4k
- ) {
- if (season.status4k !== MediaStatus.UNKNOWN) {
- logger.info(
- `Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
- { label: 'AvailabilitySync' }
- );
- await seasonRepository.update(season.id, {
- status4k: MediaStatus.UNKNOWN,
- });
-
- if (seasonToBeDeleted) {
- await seasonRequestRepository.remove(seasonToBeDeleted);
- }
+ const seasonIsAvailable = sonarrSeasons?.find(
+ ({ seasonNumber, statistics }) =>
+ season.seasonNumber === seasonNumber &&
+ statistics?.episodeFileCount &&
+ statistics?.episodeFileCount > 0
+ );
- if (media.status4k === MediaStatus.AVAILABLE) {
- logger.info(
- `Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
- { label: 'AvailabilitySync' }
- );
- await mediaRepository.update(media.id, {
- status4k: MediaStatus.PARTIALLY_AVAILABLE,
- });
- }
- }
+ if (seasonIsAvailable && sonarrSeasons) {
+ seasonExists = true;
}
- } catch (ex) {
- logger.debug(`Failure updating media ID ${media.id}`, {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- });
- }
-
- if (
- seasonExistsInSonarr ||
- seasonExistsInSonarr4k ||
- seasonExistsInPlex ||
- seasonExistsInPlex4k
- ) {
- return true;
}
- return false;
+ return seasonExists;
}
- private async mediaExists(media: Media): Promise {
+ private async mediaExistsInPlex(
+ media: Media,
+ is4k: boolean
+ ): Promise<{ existsInPlex: boolean; seasonsMap?: Map }> {
const ratingKey = media.ratingKey;
const ratingKey4k = media.ratingKey4k;
-
let existsInPlex = false;
- let existsInPlex4k = false;
+ let preventSeasonSearch = false;
- // Check each plex instance to see if media exists
+ // Check each plex instance to see if the media still exists
+ // If found, we will assume the media exists and prevent removal
+ // We can use the cache we built when we fetched the series with mediaExistsInPlex
try {
- if (ratingKey) {
- const meta = await this.plexClient?.getMetadata(ratingKey);
- if (meta) {
- existsInPlex = true;
+ let plexMedia: PlexMetadata | undefined;
+
+ if (ratingKey && !is4k) {
+ plexMedia = await this.plexClient?.getMetadata(ratingKey);
+
+ if (media.mediaType === 'tv') {
+ this.plexSeasonsCache[ratingKey] =
+ await this.plexClient?.getChildrenMetadata(ratingKey);
}
}
- if (ratingKey4k) {
- const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
- if (meta4k) {
- existsInPlex4k = true;
+ if (ratingKey4k && is4k) {
+ plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
+
+ if (media.mediaType === 'tv') {
+ this.plexSeasonsCache[ratingKey4k] =
+ await this.plexClient?.getChildrenMetadata(ratingKey4k);
}
}
- } catch (ex) {
- if (!ex.message.includes('response code: 404')) {
- logger.debug(`Failed to retrieve plex metadata`, {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- });
- }
- }
- // Base case if both media versions exist in plex
- if (existsInPlex && existsInPlex4k) {
- return true;
- }
- // We then check radarr or sonarr has that specific media. If not, then we will move to delete
- // If a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
- if (media.mediaType === 'movie') {
- const existsInRadarr = await this.mediaExistsInRadarr(
- media,
- existsInPlex,
- existsInPlex4k
- );
-
- // If true, media exists in at least one radarr or plex instance.
- if (existsInRadarr) {
- logger.warn(
- `${media.id} exists in at least one Radarr or Plex instance. Media will be updated if set to available.`,
+ if (plexMedia) {
+ existsInPlex = true;
+ }
+ } catch (ex) {
+ if (!ex.message.includes('404')) {
+ existsInPlex = true;
+ preventSeasonSearch = true;
+ logger.debug(
+ `Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
+ media.mediaType === 'tv' ? 'show' : 'movie'
+ } [TMDB ID ${media.tmdbId}] from Plex.`,
{
+ errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
-
- return true;
}
}
+ // Here we check each season in plex for availability
+ // If the API returns an error other than a 404,
+ // we will have to prevent the season check from happening
if (media.mediaType === 'tv') {
- const existsInSonarr = await this.mediaExistsInSonarr(
- media,
- existsInPlex,
- existsInPlex4k
- );
-
- // If true, media exists in at least one sonarr or plex instance.
- if (existsInSonarr) {
- logger.warn(
- `${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
- {
- label: 'AvailabilitySync',
- }
+ const seasonsMap: Map = new Map();
+
+ if (!preventSeasonSearch) {
+ const filteredSeasons = media.seasons.filter(
+ (season) =>
+ season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
+ season[is4k ? 'status4k' : 'status'] ===
+ MediaStatus.PARTIALLY_AVAILABLE
);
- return true;
+ for (const season of filteredSeasons) {
+ const seasonExists = await this.seasonExistsInPlex(
+ media,
+ season,
+ is4k
+ );
+
+ if (seasonExists) {
+ seasonsMap.set(season.seasonNumber, true);
+ }
+ }
}
+
+ return { existsInPlex, seasonsMap };
}
- return false;
+ return { existsInPlex };
}
- private async seasonExists(media: Media, season: Season) {
+ private async seasonExistsInPlex(
+ media: Media,
+ season: Season,
+ is4k: boolean
+ ): Promise {
const ratingKey = media.ratingKey;
const ratingKey4k = media.ratingKey4k;
-
let seasonExistsInPlex = false;
- let seasonExistsInPlex4k = false;
- try {
- if (ratingKey) {
- const children =
- this.plexSeasonsCache[ratingKey] ??
- (await this.plexClient?.getChildrenMetadata(ratingKey)) ??
- [];
- this.plexSeasonsCache[ratingKey] = children;
- const seasonMeta = children?.find(
- (child) => child.index === season.seasonNumber
- );
-
- if (seasonMeta) {
- seasonExistsInPlex = true;
- }
- }
- if (ratingKey4k) {
- const children4k =
- this.plexSeasonsCache[ratingKey4k] ??
- (await this.plexClient?.getChildrenMetadata(ratingKey4k)) ??
- [];
- this.plexSeasonsCache[ratingKey4k] = children4k;
- const seasonMeta4k = children4k?.find(
- (child) => child.index === season.seasonNumber
- );
+ // Check each plex instance to see if the season exists
+ let plexSeasons: PlexMetadata[] | undefined;
- if (seasonMeta4k) {
- seasonExistsInPlex4k = true;
- }
- }
- } catch (ex) {
- if (!ex.message.includes('response code: 404')) {
- logger.debug(`Failed to retrieve plex's children metadata`, {
- errorMessage: ex.message,
- label: 'AvailabilitySync',
- });
- }
- }
- // Base case if both season versions exist in plex
- if (seasonExistsInPlex && seasonExistsInPlex4k) {
- return true;
+ if (ratingKey && !is4k) {
+ plexSeasons = this.plexSeasonsCache[ratingKey];
}
- const existsInSonarr = await this.seasonExistsInSonarr(
- media,
- season,
- seasonExistsInPlex,
- seasonExistsInPlex4k
- );
-
- if (existsInSonarr) {
- logger.warn(
- `Season ${season.seasonNumber}, media ID ${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
- {
- label: 'AvailabilitySync',
- }
- );
-
- return true;
+ if (ratingKey4k && is4k) {
+ plexSeasons = this.plexSeasonsCache[ratingKey4k];
}
- return false;
- }
-
- private async initPlexClient() {
- const userRepository = getRepository(User);
- const admin = await userRepository.findOne({
- select: { id: true, plexToken: true },
- where: { id: 1 },
- });
+ const seasonIsAvailable = plexSeasons?.find(
+ (plexSeason) => plexSeason.index === season.seasonNumber
+ );
- if (!admin) {
- logger.warning('No admin configured. Availability sync skipped.');
- return;
+ if (seasonIsAvailable) {
+ seasonExistsInPlex = true;
}
- this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
+ return seasonExistsInPlex;
}
}
From b4191f9c65b7ff08764e61d18e7a75bc8d4b3325 Mon Sep 17 00:00:00 2001
From: Marco Faggian
Date: Fri, 28 Jul 2023 13:51:19 +0200
Subject: [PATCH 30/34] feat(rating): added IMDB Radarr proxy (#3496)
* feat(rating): added imdb radarr proxy
Signed-off-by: marcofaggian
* refactor(rating/imdb): rm export unused interfaces
Signed-off-by: marcofaggian
* docs(rating/imdb): rt to imdb
Signed-off-by: marcofaggian
* refactor(rating/imdb): specified error message
Signed-off-by: marcofaggian
* refactor(rating/imdb): rm line break
Signed-off-by: marcofaggian
* refactor(rating): conform to types patter
Signed-off-by: marcofaggian
* chore(rating/imdb): added line to translation file
Signed-off-by: marcofaggian
* feat(rating/imdb): ratings to ratingscombined
Signed-off-by: marcofaggian
* fix(rating/imdb): reinstating ratings route
Signed-off-by: marcofaggian
* docs(ratings): openapi ratings
Signed-off-by: marcofaggian
* chore(ratings): undo openapi ratings apex
Signed-off-by: marcofaggian
---------
Signed-off-by: marcofaggian
---
overseerr-api.yml | 57 +++++++
server/api/rating/imdbRadarrProxy.ts | 195 ++++++++++++++++++++++
server/api/{ => rating}/rottentomatoes.ts | 8 +-
server/api/ratings.ts | 7 +
server/lib/cache.ts | 5 +
server/routes/movie.ts | 56 ++++++-
server/routes/tv.ts | 2 +-
src/components/MovieDetails/index.tsx | 90 ++++++----
src/components/TvDetails/index.tsx | 2 +-
src/i18n/locale/en.json | 1 +
10 files changed, 384 insertions(+), 39 deletions(-)
create mode 100644 server/api/rating/imdbRadarrProxy.ts
rename server/api/{ => rating}/rottentomatoes.ts (93%)
create mode 100644 server/api/ratings.ts
diff --git a/overseerr-api.yml b/overseerr-api.yml
index c8b528859..f3a1cc74b 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -5338,6 +5338,63 @@ paths:
audienceRating:
type: string
enum: ['Spilled', 'Upright']
+ /movie/{movieId}/ratingscombined:
+ get:
+ summary: Get RT and IMDB movie ratings combined
+ description: Returns ratings from RottenTomatoes and IMDB based on the provided movieId in a JSON object.
+ tags:
+ - movies
+ parameters:
+ - in: path
+ name: movieId
+ required: true
+ schema:
+ type: number
+ example: 337401
+ responses:
+ '200':
+ description: Ratings returned
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ rt:
+ type: object
+ properties:
+ title:
+ type: string
+ example: Mulan
+ year:
+ type: number
+ example: 2020
+ url:
+ type: string
+ example: 'http://www.rottentomatoes.com/m/mulan_2020/'
+ criticsScore:
+ type: number
+ example: 85
+ criticsRating:
+ type: string
+ enum: ['Rotten', 'Fresh', 'Certified Fresh']
+ audienceScore:
+ type: number
+ example: 65
+ audienceRating:
+ type: string
+ enum: ['Spilled', 'Upright']
+ imdb:
+ type: object
+ properties:
+ title:
+ type: string
+ example: I am Legend
+ url:
+ type: string
+ example: 'https://www.imdb.com/title/tt0480249'
+ criticsScore:
+ type: number
+ example: 6.5
/tv/{tvId}:
get:
summary: Get TV details
diff --git a/server/api/rating/imdbRadarrProxy.ts b/server/api/rating/imdbRadarrProxy.ts
new file mode 100644
index 000000000..0d8ec79fb
--- /dev/null
+++ b/server/api/rating/imdbRadarrProxy.ts
@@ -0,0 +1,195 @@
+import ExternalAPI from '@server/api/externalapi';
+import cacheManager from '@server/lib/cache';
+
+type IMDBRadarrProxyResponse = IMDBMovie[];
+
+interface IMDBMovie {
+ ImdbId: string;
+ Overview: string;
+ Title: string;
+ OriginalTitle: string;
+ TitleSlug: string;
+ Ratings: Rating[];
+ MovieRatings: MovieRatings;
+ Runtime: number;
+ Images: Image[];
+ Genres: string[];
+ Popularity: number;
+ Premier: string;
+ InCinema: string;
+ PhysicalRelease: any;
+ DigitalRelease: string;
+ Year: number;
+ AlternativeTitles: AlternativeTitle[];
+ Translations: Translation[];
+ Recommendations: Recommendation[];
+ Credits: Credits;
+ Studio: string;
+ YoutubeTrailerId: string;
+ Certifications: Certification[];
+ Status: any;
+ Collection: Collection;
+ OriginalLanguage: string;
+ Homepage: string;
+ TmdbId: number;
+}
+
+interface Rating {
+ Count: number;
+ Value: number;
+ Origin: string;
+ Type: string;
+}
+
+interface MovieRatings {
+ Tmdb: Tmdb;
+ Imdb: Imdb;
+ Metacritic: Metacritic;
+ RottenTomatoes: RottenTomatoes;
+}
+
+interface Tmdb {
+ Count: number;
+ Value: number;
+ Type: string;
+}
+
+interface Imdb {
+ Count: number;
+ Value: number;
+ Type: string;
+}
+
+interface Metacritic {
+ Count: number;
+ Value: number;
+ Type: string;
+}
+
+interface RottenTomatoes {
+ Count: number;
+ Value: number;
+ Type: string;
+}
+
+interface Image {
+ CoverType: string;
+ Url: string;
+}
+
+interface AlternativeTitle {
+ Title: string;
+ Type: string;
+ Language: string;
+}
+
+interface Translation {
+ Title: string;
+ Overview: string;
+ Language: string;
+}
+
+interface Recommendation {
+ TmdbId: number;
+ Title: string;
+}
+
+interface Credits {
+ Cast: Cast[];
+ Crew: Crew[];
+}
+
+interface Cast {
+ Name: string;
+ Order: number;
+ Character: string;
+ TmdbId: number;
+ CreditId: string;
+ Images: Image2[];
+}
+
+interface Image2 {
+ CoverType: string;
+ Url: string;
+}
+
+interface Crew {
+ Name: string;
+ Job: string;
+ Department: string;
+ TmdbId: number;
+ CreditId: string;
+ Images: Image3[];
+}
+
+interface Image3 {
+ CoverType: string;
+ Url: string;
+}
+
+interface Certification {
+ Country: string;
+ Certification: string;
+}
+
+interface Collection {
+ Name: string;
+ Images: any;
+ Overview: any;
+ Translations: any;
+ Parts: any;
+ TmdbId: number;
+}
+
+export interface IMDBRating {
+ title: string;
+ url: string;
+ criticsScore: number;
+}
+
+/**
+ * This is a best-effort API. The IMDB API is technically
+ * private and getting access costs money/requires approval.
+ *
+ * Radarr hosts a public proxy that's in use by all Radarr instances.
+ */
+class IMDBRadarrProxy extends ExternalAPI {
+ constructor() {
+ super('https://api.radarr.video/v1', {
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ nodeCache: cacheManager.getCache('imdb').data,
+ });
+ }
+
+ /**
+ * Ask the Radarr IMDB Proxy for the movie
+ *
+ * @param IMDBid Id of IMDB movie
+ */
+ public async getMovieRatings(IMDBid: string): Promise {
+ try {
+ const data = await this.get(
+ `/movie/imdb/${IMDBid}`
+ );
+
+ if (!data?.length || data[0].ImdbId !== IMDBid) {
+ return null;
+ }
+
+ return {
+ title: data[0].Title,
+ url: `https://www.imdb.com/title/${data[0].ImdbId}`,
+ criticsScore: data[0].MovieRatings.Imdb.Value,
+ };
+ } catch (e) {
+ throw new Error(
+ `[IMDB RADARR PROXY API] Failed to retrieve movie ratings: ${e.message}`
+ );
+ }
+ }
+}
+
+export default IMDBRadarrProxy;
diff --git a/server/api/rottentomatoes.ts b/server/api/rating/rottentomatoes.ts
similarity index 93%
rename from server/api/rottentomatoes.ts
rename to server/api/rating/rottentomatoes.ts
index 99a74eb1b..1cf9d6d8e 100644
--- a/server/api/rottentomatoes.ts
+++ b/server/api/rating/rottentomatoes.ts
@@ -1,6 +1,6 @@
+import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings';
-import ExternalAPI from './externalapi';
interface RTAlgoliaSearchResponse {
results: {
@@ -144,6 +144,9 @@ class RottenTomatoes extends ExternalAPI {
? 'Fresh'
: 'Rotten',
criticsScore: movie.rottenTomatoes.criticsScore,
+ audienceRating:
+ movie.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
+ audienceScore: movie.rottenTomatoes.audienceScore,
year: Number(movie.releaseYear),
};
} catch (e) {
@@ -192,6 +195,9 @@ class RottenTomatoes extends ExternalAPI {
criticsRating:
tvshow.rottenTomatoes.criticsScore >= 60 ? 'Fresh' : 'Rotten',
criticsScore: tvshow.rottenTomatoes.criticsScore,
+ audienceRating:
+ tvshow.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
+ audienceScore: tvshow.rottenTomatoes.audienceScore,
year: Number(tvshow.releaseYear),
};
} catch (e) {
diff --git a/server/api/ratings.ts b/server/api/ratings.ts
new file mode 100644
index 000000000..1fe1354cf
--- /dev/null
+++ b/server/api/ratings.ts
@@ -0,0 +1,7 @@
+import { type IMDBRating } from '@server/api/rating/imdbRadarrProxy';
+import { type RTRating } from '@server/api/rating/rottentomatoes';
+
+export interface RatingResponse {
+ rt?: RTRating;
+ imdb?: IMDBRating;
+}
diff --git a/server/lib/cache.ts b/server/lib/cache.ts
index e81466629..011205e7f 100644
--- a/server/lib/cache.ts
+++ b/server/lib/cache.ts
@@ -5,6 +5,7 @@ export type AvailableCacheIds =
| 'radarr'
| 'sonarr'
| 'rt'
+ | 'imdb'
| 'github'
| 'plexguid'
| 'plextv';
@@ -51,6 +52,10 @@ class CacheManager {
stdTtl: 43200,
checkPeriod: 60 * 30,
}),
+ imdb: new Cache('imdb', 'IMDB Radarr Proxy', {
+ stdTtl: 43200,
+ checkPeriod: 60 * 30,
+ }),
github: new Cache('github', 'GitHub API', {
stdTtl: 21600,
checkPeriod: 60 * 30,
diff --git a/server/routes/movie.ts b/server/routes/movie.ts
index f11cead8c..e39e2e86e 100644
--- a/server/routes/movie.ts
+++ b/server/routes/movie.ts
@@ -1,4 +1,6 @@
-import RottenTomatoes from '@server/api/rottentomatoes';
+import IMDBRadarrProxy from '@server/api/rating/imdbRadarrProxy';
+import RottenTomatoes from '@server/api/rating/rottentomatoes';
+import { type RatingResponse } from '@server/api/ratings';
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import Media from '@server/entity/Media';
@@ -116,6 +118,9 @@ movieRoutes.get('/:id/similar', async (req, res, next) => {
}
});
+/**
+ * Endpoint backed by RottenTomatoes
+ */
movieRoutes.get('/:id/ratings', async (req, res, next) => {
const tmdb = new TheMovieDb();
const rtapi = new RottenTomatoes();
@@ -151,4 +156,53 @@ movieRoutes.get('/:id/ratings', async (req, res, next) => {
}
});
+/**
+ * Endpoint combining RottenTomatoes and IMDB
+ */
+movieRoutes.get('/:id/ratingscombined', async (req, res, next) => {
+ const tmdb = new TheMovieDb();
+ const rtapi = new RottenTomatoes();
+ const imdbApi = new IMDBRadarrProxy();
+
+ try {
+ const movie = await tmdb.getMovie({
+ movieId: Number(req.params.id),
+ });
+
+ const rtratings = await rtapi.getMovieRatings(
+ movie.title,
+ Number(movie.release_date.slice(0, 4))
+ );
+
+ let imdbRatings;
+ if (movie.imdb_id) {
+ imdbRatings = await imdbApi.getMovieRatings(movie.imdb_id);
+ }
+
+ if (!rtratings && !imdbRatings) {
+ return next({
+ status: 404,
+ message: 'No ratings found.',
+ });
+ }
+
+ const ratings: RatingResponse = {
+ ...(rtratings ? { rt: rtratings } : {}),
+ ...(imdbRatings ? { imdb: imdbRatings } : {}),
+ };
+
+ return res.status(200).json(ratings);
+ } catch (e) {
+ logger.debug('Something went wrong retrieving movie ratings', {
+ label: 'API',
+ errorMessage: e.message,
+ movieId: req.params.id,
+ });
+ return next({
+ status: 500,
+ message: 'Unable to retrieve movie ratings.',
+ });
+ }
+});
+
export default movieRoutes;
diff --git a/server/routes/tv.ts b/server/routes/tv.ts
index d45e40620..95c8dc11c 100644
--- a/server/routes/tv.ts
+++ b/server/routes/tv.ts
@@ -1,4 +1,4 @@
-import RottenTomatoes from '@server/api/rottentomatoes';
+import RottenTomatoes from '@server/api/rating/rottentomatoes';
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import Media from '@server/entity/Media';
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx
index 1b142d4de..eaaa902e8 100644
--- a/src/components/MovieDetails/index.tsx
+++ b/src/components/MovieDetails/index.tsx
@@ -2,6 +2,7 @@ import RTAudFresh from '@app/assets/rt_aud_fresh.svg';
import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
import RTFresh from '@app/assets/rt_fresh.svg';
import RTRotten from '@app/assets/rt_rotten.svg';
+import ImdbLogo from '@app/assets/services/imdb.svg';
import TmdbLogo from '@app/assets/tmdb_logo.svg';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
@@ -40,7 +41,7 @@ import {
ChevronDoubleDownIcon,
ChevronDoubleUpIcon,
} from '@heroicons/react/24/solid';
-import type { RTRating } from '@server/api/rottentomatoes';
+import { type RatingResponse } from '@server/api/ratings';
import { IssueStatus } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
@@ -86,6 +87,7 @@ const messages = defineMessages({
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
rtaudiencescore: 'Rotten Tomatoes Audience Score',
tmdbuserscore: 'TMDB User Score',
+ imdbuserscore: 'IMDB User Score',
});
interface MovieDetailsProps {
@@ -120,8 +122,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
),
});
- const { data: ratingData } = useSWR(
- `/api/v1/movie/${router.query.movieId}/ratings`
+ const { data: ratingData } = useSWR(
+ `/api/v1/movie/${router.query.movieId}/ratingscombined`
);
const sortedCrew = useMemo(
@@ -511,44 +513,62 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
)}
{(!!data.voteCount ||
- (ratingData?.criticsRating && !!ratingData?.criticsScore) ||
- (ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
+ (ratingData?.rt?.criticsRating &&
+ !!ratingData?.rt?.criticsScore) ||
+ (ratingData?.rt?.audienceRating &&
+ !!ratingData?.rt?.audienceScore) ||
+ ratingData?.imdb?.criticsScore) && (
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx
index c450ef4ad..2f44a7d8c 100644
--- a/src/components/TvDetails/index.tsx
+++ b/src/components/TvDetails/index.tsx
@@ -40,7 +40,7 @@ import {
PlayIcon,
} from '@heroicons/react/24/outline';
import { ChevronDownIcon } from '@heroicons/react/24/solid';
-import type { RTRating } from '@server/api/rottentomatoes';
+import type { RTRating } from '@server/api/rating/rottentomatoes';
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
import { IssueStatus } from '@server/constants/issue';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index 24a537a07..83d3d8421 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -256,6 +256,7 @@
"components.MovieDetails.budget": "Budget",
"components.MovieDetails.cast": "Cast",
"components.MovieDetails.digitalrelease": "Digital Release",
+ "components.MovieDetails.imdbuserscore": "IMDB User Score",
"components.MovieDetails.managemovie": "Manage Movie",
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
"components.MovieDetails.markavailable": "Mark as Available",
From 46e21c4e3e890ebc37ffcbb862148d07c79372d0 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 28 Jul 2023 20:52:03 +0900
Subject: [PATCH 31/34] docs: add marcofaggian as a contributor for code
(#3563) [skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index f29044994..53ca949de 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -881,6 +881,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "marcofaggian",
+ "name": "Marco Faggian",
+ "avatar_url": "https://avatars.githubusercontent.com/u/19221001?v=4",
+ "profile": "https://marcofaggian.com",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "
-orange.svg\"/>",
diff --git a/README.md b/README.md
index 0f6e32413..4c7db12e3 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -197,6 +197,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Andrew Kennedy 💻 |
Fallenbagel 💻 |
Anton K. (ai Doge) 💻 |
+
Marco Faggian 💻 |
From cb63bf217b9e8810a5210b4bf475b2a96583cc84 Mon Sep 17 00:00:00 2001
From: Eric Nemchik
Date: Fri, 28 Jul 2023 09:41:52 -0500
Subject: [PATCH 32/34] fix: Include all defaults in payload (#3538)
* fix: Include all defaults in payload
* style(src/components/settings/notifications/notificationswebhook/index.tsx): prettier format
format changes from previous commit using prettier. line length requirement now met.
* fix(server/lib/settings.ts): update default settings for first install
---
server/lib/settings.ts | 2 +-
.../Notifications/NotificationsWebhook/index.tsx | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index c3981fe90..36dbb1097 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -378,7 +378,7 @@ class Settings {
options: {
webhookUrl: '',
jsonPayload:
- 'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
+ 'IntcbiAgXCJub3RpZmljYXRpb25fdHlwZVwiOiBcInt7bm90aWZpY2F0aW9uX3R5cGV9fVwiLFxuICBcImV2ZW50XCI6IFwie3tldmVudH19XCIsXG4gIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gIFwibWVzc2FnZVwiOiBcInt7bWVzc2FnZX19XCIsXG4gIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgXCJ7e21lZGlhfX1cIjoge1xuICAgIFwibWVkaWFfdHlwZVwiOiBcInt7bWVkaWFfdHlwZX19XCIsXG4gICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgXCJzdGF0dXM0a1wiOiBcInt7bWVkaWFfc3RhdHVzNGt9fVwiXG4gIH0sXG4gIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgXCJyZXF1ZXN0ZWRCeV9lbWFpbFwiOiBcInt7cmVxdWVzdGVkQnlfZW1haWx9fVwiLFxuICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVxdWVzdGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tyZXF1ZXN0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2lzc3VlfX1cIjoge1xuICAgIFwiaXNzdWVfaWRcIjogXCJ7e2lzc3VlX2lkfX1cIixcbiAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgIFwiaXNzdWVfc3RhdHVzXCI6IFwie3tpc3N1ZV9zdGF0dXN9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9lbWFpbFwiOiBcInt7cmVwb3J0ZWRCeV9lbWFpbH19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcG9ydGVkQnlfYXZhdGFyXCI6IFwie3tyZXBvcnRlZEJ5X2F2YXRhcn19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc19kaXNjb3JkSWR9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2NvbW1lbnR9fVwiOiB7XG4gICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgXCJjb21tZW50ZWRCeV9lbWFpbFwiOiBcInt7Y29tbWVudGVkQnlfZW1haWx9fVwiLFxuICAgIFwiY29tbWVudGVkQnlfdXNlcm5hbWVcIjogXCJ7e2NvbW1lbnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7Y29tbWVudGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tjb21tZW50ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
},
},
webpush: {
diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx
index 14f1e672e..fcddfdbdf 100644
--- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx
+++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx
@@ -39,6 +39,9 @@ const defaultPayload = {
requestedBy_email: '{{requestedBy_email}}',
requestedBy_username: '{{requestedBy_username}}',
requestedBy_avatar: '{{requestedBy_avatar}}',
+ requestedBy_settings_discordId: '{{requestedBy_settings_discordId}}',
+ requestedBy_settings_telegramChatId:
+ '{{requestedBy_settings_telegramChatId}}',
},
'{{issue}}': {
issue_id: '{{issue_id}}',
@@ -47,12 +50,18 @@ const defaultPayload = {
reportedBy_email: '{{reportedBy_email}}',
reportedBy_username: '{{reportedBy_username}}',
reportedBy_avatar: '{{reportedBy_avatar}}',
+ reportedBy_settings_discordId: '{{reportedBy_settings_discordId}}',
+ reportedBy_settings_telegramChatId:
+ '{{reportedBy_settings_telegramChatId}}',
},
'{{comment}}': {
comment_message: '{{comment_message}}',
commentedBy_email: '{{commentedBy_email}}',
commentedBy_username: '{{commentedBy_username}}',
commentedBy_avatar: '{{commentedBy_avatar}}',
+ commentedBy_settings_discordId: '{{commentedBy_settings_discordId}}',
+ commentedBy_settings_telegramChatId:
+ '{{commentedBy_settings_telegramChatId}}',
},
'{{extra}}': [],
};
From a686d31e4d7b64812fdb5d2049d7b4d38990f4f5 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 28 Jul 2023 23:42:29 +0900
Subject: [PATCH 33/34] docs: add nemchik as a contributor for code (#3565)
[skip ci]
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 53ca949de..b5166196f 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -890,6 +890,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "nemchik",
+ "name": "Eric Nemchik",
+ "avatar_url": "https://avatars.githubusercontent.com/u/725456?v=4",
+ "profile": "http://nemchik.com/",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "-orange.svg\"/>",
diff --git a/README.md b/README.md
index 4c7db12e3..2369a0cda 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -198,6 +198,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Fallenbagel 💻 |
Anton K. (ai Doge) 💻 |
Marco Faggian 💻 |
+ Eric Nemchik 💻 |
From f7b4dfcac472d08c54779a14fc1ad3c90927df26 Mon Sep 17 00:00:00 2001
From: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Date: Fri, 4 Aug 2023 16:26:03 -0700
Subject: [PATCH 34/34] fix(tautulli): only test connection if hostname is
defined (#3573)
---
server/routes/settings/index.ts | 34 +++++++++++++++++----------------
1 file changed, 18 insertions(+), 16 deletions(-)
diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts
index 8023ba960..98fe0f776 100644
--- a/server/routes/settings/index.ts
+++ b/server/routes/settings/index.ts
@@ -254,25 +254,27 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
Object.assign(settings.tautulli, req.body);
- try {
- const tautulliClient = new TautulliAPI(settings.tautulli);
+ if (settings.tautulli.hostname) {
+ try {
+ const tautulliClient = new TautulliAPI(settings.tautulli);
- const result = await tautulliClient.getInfo();
+ const result = await tautulliClient.getInfo();
- if (!semver.gte(semver.coerce(result?.tautulli_version) ?? '', '2.9.0')) {
- throw new Error('Tautulli version not supported');
- }
+ if (!semver.gte(semver.coerce(result?.tautulli_version) ?? '', '2.9.0')) {
+ throw new Error('Tautulli version not supported');
+ }
- settings.save();
- } catch (e) {
- logger.error('Something went wrong testing Tautulli connection', {
- label: 'API',
- errorMessage: e.message,
- });
- return next({
- status: 500,
- message: 'Unable to connect to Tautulli.',
- });
+ settings.save();
+ } catch (e) {
+ logger.error('Something went wrong testing Tautulli connection', {
+ label: 'API',
+ errorMessage: e.message,
+ });
+ return next({
+ status: 500,
+ message: 'Unable to connect to Tautulli.',
+ });
+ }
}
return res.status(200).json(settings.tautulli);