From c93467b3acf2c256324297e7e8f21e9944005dd4 Mon Sep 17 00:00:00 2001
From: Ryan Cohen
Date: Tue, 31 Jan 2023 01:31:39 +0900
Subject: [PATCH 01/18] fix(snapcraft): use the correct config folder for image
cache (#3302)
---
server/lib/imageproxy.ts | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/server/lib/imageproxy.ts b/server/lib/imageproxy.ts
index 4ba6b97a..38203b7b 100644
--- a/server/lib/imageproxy.ts
+++ b/server/lib/imageproxy.ts
@@ -18,14 +18,14 @@ type ImageResponse = {
imageBuffer: Buffer;
};
+const baseCacheDirectory = process.env.CONFIG_DIRECTORY
+ ? `${process.env.CONFIG_DIRECTORY}/cache/images`
+ : path.join(__dirname, '../../config/cache/images');
+
class ImageProxy {
public static async clearCache(key: string) {
let deletedImages = 0;
- const cacheDirectory = path.join(
- __dirname,
- '../../config/cache/images/',
- key
- );
+ const cacheDirectory = path.join(baseCacheDirectory, key);
const files = await promises.readdir(cacheDirectory);
@@ -57,11 +57,7 @@ class ImageProxy {
public static async getImageStats(
key: string
): Promise<{ size: number; imageCount: number }> {
- const cacheDirectory = path.join(
- __dirname,
- '../../config/cache/images/',
- key
- );
+ const cacheDirectory = path.join(baseCacheDirectory, key);
const imageTotalSize = await ImageProxy.getDirectorySize(cacheDirectory);
const imageCount = await ImageProxy.getImageCount(cacheDirectory);
@@ -263,7 +259,7 @@ class ImageProxy {
}
private getCacheDirectory() {
- return path.join(__dirname, '../../config/cache/images/', this.key);
+ return path.join(baseCacheDirectory, this.key);
}
}
From b6eac0f364680675aa6ad3aaf5d858c27873dcdf Mon Sep 17 00:00:00 2001
From: Ryan Cohen
Date: Sat, 11 Feb 2023 17:18:30 +0900
Subject: [PATCH 02/18] test: change custom keyword for slider creation (#3333)
---
cypress/e2e/settings/discover-customization.cy.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cypress/e2e/settings/discover-customization.cy.ts b/cypress/e2e/settings/discover-customization.cy.ts
index a0756ae2..469994a3 100644
--- a/cypress/e2e/settings/discover-customization.cy.ts
+++ b/cypress/e2e/settings/discover-customization.cy.ts
@@ -96,7 +96,7 @@ describe('Discover Customization', () => {
.should('be.disabled');
cy.get('#data').clear();
- cy.get('#data').type('time travel{enter}', { delay: 100 });
+ cy.get('#data').type('christmas{enter}', { delay: 100 });
// Confirming we have some results
cy.contains('.slider-header', sliderTitle)
From d7b83d22cee3d20db564cc0564d42802b02327e3 Mon Sep 17 00:00:00 2001
From: Danshil Kokil Mungur
Date: Sat, 11 Feb 2023 13:36:31 +0400
Subject: [PATCH 03/18] fix(build): increase threshold for amount of data to be
fetched when SSR'ing (#3320)
Co-authored-by: Ryan Cohen
---
next.config.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/next.config.js b/next.config.js
index b0e872e8..9cd65f97 100644
--- a/next.config.js
+++ b/next.config.js
@@ -19,5 +19,6 @@ module.exports = {
},
experimental: {
scrollRestoration: true,
+ largePageDataBytes: 256000,
},
};
From 33e7691b94d7d369a0a1410e434850bc51e5572e Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Sat, 11 Feb 2023 05:30:53 -0500
Subject: [PATCH 04/18] feat: full title of download item on hover with tooltip
(#3296)
Co-authored-by: Ryan Cohen
---
src/components/ManageSlideOver/index.tsx | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx
index b6fd51cf..8609c828 100644
--- a/src/components/ManageSlideOver/index.tsx
+++ b/src/components/ManageSlideOver/index.tsx
@@ -1,6 +1,7 @@
import Button from '@app/components/Common/Button';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import SlideOver from '@app/components/Common/SlideOver';
+import Tooltip from '@app/components/Common/Tooltip';
import DownloadBlock from '@app/components/DownloadBlock';
import IssueBlock from '@app/components/IssueBlock';
import RequestBlock from '@app/components/RequestBlock';
@@ -144,20 +145,24 @@ const ManageSlideOver = ({
{data.mediaInfo?.downloadStatus?.map((status, index) => (
- -
-
-
+ -
+
+
+
))}
{data.mediaInfo?.downloadStatus4k?.map((status, index) => (
- -
-
-
+ -
+
+
+
))}
From 966639df430d32f6bfebdb16314dc4590d21caf8 Mon Sep 17 00:00:00 2001
From: Pedro Nascimento
Date: Sun, 12 Feb 2023 08:05:29 -0300
Subject: [PATCH 05/18] perf(imageproxy): do not set cookies to image proxy so
CDNs can cache images (#3332)
CDNs such as Cloudflare bypass their cache if cookies are set in the response.
clearCookies
middleware removes the header before imageproxy serves the image.
---
server/index.ts | 4 +++-
server/middleware/clearcookies.ts | 6 ++++++
2 files changed, 9 insertions(+), 1 deletion(-)
create mode 100644 server/middleware/clearcookies.ts
diff --git a/server/index.ts b/server/index.ts
index 93703402..b426f0f3 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -17,6 +17,7 @@ import WebhookAgent from '@server/lib/notifications/agents/webhook';
import WebPushAgent from '@server/lib/notifications/agents/webpush';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
+import clearCookies from '@server/middleware/clearcookies';
import routes from '@server/routes';
import imageproxy from '@server/routes/imageproxy';
import { getAppVersion } from '@server/utils/appVersion';
@@ -182,7 +183,8 @@ app
});
server.use('/api/v1', routes);
- server.use('/imageproxy', imageproxy);
+ // Do not set cookies so CDNs can cache them
+ server.use('/imageproxy', clearCookies, imageproxy);
server.get('*', (req, res) => handle(req, res));
server.use(
diff --git a/server/middleware/clearcookies.ts b/server/middleware/clearcookies.ts
new file mode 100644
index 00000000..73713e52
--- /dev/null
+++ b/server/middleware/clearcookies.ts
@@ -0,0 +1,6 @@
+const clearCookies: Middleware = (_req, res, next) => {
+ res.removeHeader('Set-Cookie');
+ next();
+};
+
+export default clearCookies;
From 2d97be0d6c2065cbb4520694adcb8537d7eef127 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sun, 12 Feb 2023 20:05:59 +0900
Subject: [PATCH 06/18] docs: add lunks as a contributor for code (#3334) [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 | 5 ++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index c57826d6..1766057a 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -773,6 +773,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "lunks",
+ "name": "Pedro Nascimento",
+ "avatar_url": "https://avatars.githubusercontent.com/u/91118?v=4",
+ "profile": "http://twitter.com/lunks/",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "
-orange.svg\"/>",
diff --git a/README.md b/README.md
index ae82f548..af828033 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -182,6 +182,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 ceptonit 📖 |
 aedelbro 💻 |
+
+  Pedro Nascimento 💻 |
+
From 6bd3f015d65507efca60279007bd2b86ee860643 Mon Sep 17 00:00:00 2001
From: Owen Voke
Date: Tue, 14 Feb 2023 05:52:26 +0000
Subject: [PATCH 07/18] fix: remove unnecessary parenthesis from api key
generation (#3336)
---
server/lib/settings.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index cf475554..86bcf144 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -546,7 +546,7 @@ class Settings {
}
private generateApiKey(): string {
- return Buffer.from(`${Date.now()}${randomUUID()})`).toString('base64');
+ return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
}
private generateVapidKeys(force = false): void {
From e684456bbab3a7092a143c3af86608b6ddf3cd2c Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Tue, 14 Feb 2023 14:53:35 +0900
Subject: [PATCH 08/18] docs: add owenvoke as a contributor for code (#3340)
[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 1766057a..3202486c 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -782,6 +782,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "owenvoke",
+ "name": "Owen Voke",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1899334?v=4",
+ "profile": "https://voke.dev",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "
-orange.svg\"/>",
diff --git a/README.md b/README.md
index af828033..a17dad13 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -184,6 +184,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Pedro Nascimento 💻 |
+  Owen Voke 💻 |
From dd1378cef53dfd9dadbc08996e7f07c018e413b9 Mon Sep 17 00:00:00 2001
From: Owen Voke
Date: Tue, 14 Feb 2023 20:40:51 +0000
Subject: [PATCH 09/18] chore(api): update descriptions for API endpoints
(#3341)
---
overseerr-api.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/overseerr-api.yml b/overseerr-api.yml
index 443a9c94..542ac59f 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -3615,7 +3615,7 @@ paths:
$ref: '#/components/schemas/User'
/user/{userId}/requests:
get:
- summary: Get user by ID
+ summary: Get requests for a specific user
description: |
Retrieves a user's requests in a JSON object.
tags:
@@ -3711,7 +3711,7 @@ paths:
example: false
/user/{userId}/watchlist:
get:
- summary: Get user by ID
+ summary: Get the Plex watchlist for a specific user
description: |
Retrieves a user's Plex Watchlist in a JSON object.
tags:
From 1e2c6f46ab66c836f321b5d8e34f1e8124c0b542 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Wed, 15 Feb 2023 10:16:13 -0500
Subject: [PATCH 10/18] fix: added a refresh interval if download status is in
progress (#3275)
* fix: added a refresh interval if download status is in progress
* refactor: switched to a function instead of useEffect
* feat: added editable download sync schedule
---
server/job/schedule.ts | 20 ++++-----
src/components/CollectionDetails/index.tsx | 42 ++++++++++++-------
src/components/MovieDetails/index.tsx | 8 ++++
src/components/RequestCard/index.tsx | 9 ++++
.../RequestList/RequestItem/index.tsx | 8 ++++
.../Settings/SettingsJobsCache/index.tsx | 41 +++++++++++++++---
src/components/TvDetails/index.tsx | 8 ++++
src/i18n/locale/en.json | 1 +
src/utils/refreshIntervalHelper.ts | 18 ++++++++
9 files changed, 125 insertions(+), 30 deletions(-)
create mode 100644 src/utils/refreshIntervalHelper.ts
diff --git a/server/job/schedule.ts b/server/job/schedule.ts
index 725e67b5..7925dd21 100644
--- a/server/job/schedule.ts
+++ b/server/job/schedule.ts
@@ -14,7 +14,7 @@ interface ScheduledJob {
job: schedule.Job;
name: string;
type: 'process' | 'command';
- interval: 'short' | 'long' | 'fixed';
+ interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
cronSchedule: string;
running?: () => boolean;
cancelFn?: () => void;
@@ -30,7 +30,7 @@ export const startJobs = (): void => {
id: 'plex-recently-added-scan',
name: 'Plex Recently Added Scan',
type: 'process',
- interval: 'short',
+ interval: 'minutes',
cronSchedule: jobs['plex-recently-added-scan'].schedule,
job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Recently Added Scan', {
@@ -47,7 +47,7 @@ export const startJobs = (): void => {
id: 'plex-full-scan',
name: 'Plex Full Library Scan',
type: 'process',
- interval: 'long',
+ interval: 'hours',
cronSchedule: jobs['plex-full-scan'].schedule,
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Full Library Scan', {
@@ -64,7 +64,7 @@ export const startJobs = (): void => {
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
- interval: 'short',
+ interval: 'minutes',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
@@ -79,7 +79,7 @@ export const startJobs = (): void => {
id: 'radarr-scan',
name: 'Radarr Scan',
type: 'process',
- interval: 'long',
+ interval: 'hours',
cronSchedule: jobs['radarr-scan'].schedule,
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
@@ -94,7 +94,7 @@ export const startJobs = (): void => {
id: 'sonarr-scan',
name: 'Sonarr Scan',
type: 'process',
- interval: 'long',
+ interval: 'hours',
cronSchedule: jobs['sonarr-scan'].schedule,
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
@@ -109,7 +109,7 @@ export const startJobs = (): void => {
id: 'download-sync',
name: 'Download Sync',
type: 'command',
- interval: 'fixed',
+ interval: 'seconds',
cronSchedule: jobs['download-sync'].schedule,
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
logger.debug('Starting scheduled job: Download Sync', {
@@ -124,7 +124,7 @@ export const startJobs = (): void => {
id: 'download-sync-reset',
name: 'Download Sync Reset',
type: 'command',
- interval: 'long',
+ interval: 'hours',
cronSchedule: jobs['download-sync-reset'].schedule,
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
logger.info('Starting scheduled job: Download Sync Reset', {
@@ -134,12 +134,12 @@ export const startJobs = (): void => {
}),
});
- // Run image cache cleanup every 5 minutes
+ // Run image cache cleanup every 24 hours
scheduledJobs.push({
id: 'image-cache-cleanup',
name: 'Image Cache Cleanup',
type: 'process',
- interval: 'long',
+ interval: 'hours',
cronSchedule: jobs['image-cache-cleanup'].schedule,
job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => {
logger.info('Starting scheduled job: Image Cache Cleanup', {
diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx
index 0136113a..34b379e2 100644
--- a/src/components/CollectionDetails/index.tsx
+++ b/src/components/CollectionDetails/index.tsx
@@ -10,6 +10,7 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
+import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media';
import type { Collection } from '@server/models/Collection';
@@ -39,20 +40,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
const [requestModal, setRequestModal] = useState(false);
const [is4k, setIs4k] = useState(false);
- const {
- data,
- error,
- mutate: revalidate,
- } = useSWR(`/api/v1/collection/${router.query.collectionId}`, {
- fallbackData: collection,
- revalidateOnMount: true,
- });
-
- const { data: genres } =
- useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
-
- const [downloadStatus, downloadStatus4k] = useMemo(() => {
- return [
+ const returnCollectionDownloadItems = (data: Collection | undefined) => {
+ const [downloadStatus, downloadStatus4k] = [
data?.parts.flatMap((item) =>
item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : []
),
@@ -60,7 +49,30 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : []
),
];
- }, [data?.parts]);
+
+ return { downloadStatus, downloadStatus4k };
+ };
+
+ const {
+ data,
+ error,
+ mutate: revalidate,
+ } = useSWR(`/api/v1/collection/${router.query.collectionId}`, {
+ fallbackData: collection,
+ revalidateOnMount: true,
+ refreshInterval: refreshIntervalHelper(
+ returnCollectionDownloadItems(collection),
+ 15000
+ ),
+ });
+
+ const { data: genres } =
+ useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
+
+ const [downloadStatus, downloadStatus4k] = useMemo(() => {
+ const downloadItems = returnCollectionDownloadItems(data);
+ return [downloadItems.downloadStatus, downloadItems.downloadStatus4k];
+ }, [data]);
const [titles, titles4k] = useMemo(() => {
return [
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx
index 6f8b2e01..1b142d4d 100644
--- a/src/components/MovieDetails/index.tsx
+++ b/src/components/MovieDetails/index.tsx
@@ -26,6 +26,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers';
+import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import {
ArrowRightCircleIcon,
CloudIcon,
@@ -110,6 +111,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
mutate: revalidate,
} = useSWR(`/api/v1/movie/${router.query.movieId}`, {
fallbackData: movie,
+ refreshInterval: refreshIntervalHelper(
+ {
+ downloadStatus: movie?.mediaInfo?.downloadStatus,
+ downloadStatus4k: movie?.mediaInfo?.downloadStatus4k,
+ },
+ 15000
+ ),
});
const { data: ratingData } = useSWR(
diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx
index a7a76bec..44abd555 100644
--- a/src/components/RequestCard/index.tsx
+++ b/src/components/RequestCard/index.tsx
@@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
+import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { withProperties } from '@app/utils/typeHelpers';
import {
ArrowPathIcon,
@@ -220,6 +221,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
+
const { data: title, error } = useSWR(
inView ? `${url}` : null
);
@@ -229,6 +231,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
mutate: revalidate,
} = useSWR(`/api/v1/request/${request.id}`, {
fallbackData: request,
+ refreshInterval: refreshIntervalHelper(
+ {
+ downloadStatus: request.media.downloadStatus,
+ downloadStatus4k: request.media.downloadStatus4k,
+ },
+ 15000
+ ),
});
const { plexUrl, plexUrl4k } = useDeepLinks({
diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx
index dbce03e5..a42483ab 100644
--- a/src/components/RequestList/RequestItem/index.tsx
+++ b/src/components/RequestList/RequestItem/index.tsx
@@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
+import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import {
ArrowPathIcon,
CheckIcon,
@@ -293,6 +294,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
`/api/v1/request/${request.id}`,
{
fallbackData: request,
+ refreshInterval: refreshIntervalHelper(
+ {
+ downloadStatus: request.media.downloadStatus,
+ downloadStatus4k: request.media.downloadStatus4k,
+ },
+ 15000
+ ),
}
);
diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx
index cd985619..b8ebe708 100644
--- a/src/components/Settings/SettingsJobsCache/index.tsx
+++ b/src/components/Settings/SettingsJobsCache/index.tsx
@@ -67,6 +67,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
editJobScheduleSelectorMinutes:
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
+ editJobScheduleSelectorSeconds:
+ 'Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}',
imagecache: 'Image Cache',
imagecacheDescription:
'When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in {appDataPath}/cache/images.',
@@ -78,7 +80,7 @@ interface Job {
id: JobId;
name: string;
type: 'process' | 'command';
- interval: 'short' | 'long' | 'fixed';
+ interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
cronSchedule: string;
nextExecutionTime: string;
running: boolean;
@@ -89,10 +91,11 @@ type JobModalState = {
job?: Job;
scheduleHours: number;
scheduleMinutes: number;
+ scheduleSeconds: number;
};
type JobModalAction =
- | { type: 'set'; hours?: number; minutes?: number }
+ | { type: 'set'; hours?: number; minutes?: number; seconds?: number }
| {
type: 'close';
}
@@ -115,6 +118,7 @@ const jobModalReducer = (
job: action.job,
scheduleHours: 1,
scheduleMinutes: 5,
+ scheduleSeconds: 30,
};
case 'set':
@@ -122,6 +126,7 @@ const jobModalReducer = (
...state,
scheduleHours: action.hours ?? state.scheduleHours,
scheduleMinutes: action.minutes ?? state.scheduleMinutes,
+ scheduleSeconds: action.seconds ?? state.scheduleSeconds,
};
}
};
@@ -149,6 +154,7 @@ const SettingsJobs = () => {
isOpen: false,
scheduleHours: 1,
scheduleMinutes: 5,
+ scheduleSeconds: 30,
});
const [isSaving, setIsSaving] = useState(false);
@@ -200,9 +206,11 @@ const SettingsJobs = () => {
const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
try {
- if (jobModalState.job?.interval === 'short') {
+ if (jobModalState.job?.interval === 'seconds') {
+ jobScheduleCron.splice(0, 2, `*/${jobModalState.scheduleSeconds}`, '*');
+ } else if (jobModalState.job?.interval === 'minutes') {
jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`;
- } else if (jobModalState.job?.interval === 'long') {
+ } else if (jobModalState.job?.interval === 'hours') {
jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`;
} else {
// jobs with interval: fixed should not be editable
@@ -286,7 +294,30 @@ const SettingsJobs = () => {
{intl.formatMessage(messages.editJobSchedulePrompt)}
- {jobModalState.job?.interval === 'short' ? (
+ {jobModalState.job?.interval === 'seconds' ? (
+
+ ) : jobModalState.job?.interval === 'minutes' ? (
- {currentStatus && (
+ {currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
Date: Thu, 23 Feb 2023 19:27:26 +0400
Subject: [PATCH 12/18] fix(ui): prevent title cards from flickering when
quickly hovering across them (#3349)
* fix(ui): remove opacity classes from transition enter and leave props
The flickering was caused by the opacity classes in the `leave` prop to take effect as the
transition ends; when the `leaveTo` prop classes are no longer applied, but the `leave` prop classes
are still applied.
* fix(ui): resolve transition issues for all components
1. Remove opacity classes from `enter` and `leave` props
2. Fix some class name typos
3. Remove transform classes since those are automatically applied as from TailwindCSS v3.0
4. Narrow down `transition` classes to only the properties being transitioned in Transition components
---
.../Common/ButtonWithDropdown/index.tsx | 12 ++++++------
src/components/Common/Modal/index.tsx | 14 +++++++-------
src/components/Common/SlideCheckbox/index.tsx | 2 +-
src/components/Common/SlideOver/index.tsx | 8 ++++----
src/components/Discover/index.tsx | 4 ++--
.../IssueDetails/IssueComment/index.tsx | 14 +++++++-------
.../IssueDetails/IssueDescription/index.tsx | 8 ++++----
src/components/IssueDetails/index.tsx | 4 ++--
src/components/IssueModal/index.tsx | 4 ++--
src/components/Layout/LanguagePicker/index.tsx | 12 ++++++------
src/components/Layout/MobileMenu/index.tsx | 6 +++---
src/components/Layout/Sidebar/index.tsx | 4 ++--
src/components/Layout/UserDropdown/index.tsx | 8 ++++----
src/components/Login/index.tsx | 4 ++--
src/components/RegionSelector/index.tsx | 2 +-
.../RequestModal/AdvancedRequester/index.tsx | 4 ++--
.../RequestModal/CollectionRequestModal.tsx | 4 ++--
src/components/RequestModal/TvRequestModal.tsx | 4 ++--
src/components/RequestModal/index.tsx | 4 ++--
src/components/Settings/LibraryItem.tsx | 2 +-
src/components/Settings/RadarrModal/index.tsx | 6 +++---
.../Settings/SettingsAbout/Releases/index.tsx | 4 ++--
.../Settings/SettingsJobsCache/index.tsx | 4 ++--
src/components/Settings/SettingsLogs/index.tsx | 4 ++--
src/components/Settings/SettingsServices.tsx | 6 +++---
src/components/Settings/SonarrModal/index.tsx | 6 +++---
src/components/StatusChecker/index.tsx | 4 ++--
src/components/TitleCard/index.tsx | 8 ++++----
src/components/TvDetails/index.tsx | 14 +++++++-------
src/components/UserList/PlexImportModal.tsx | 4 ++--
src/components/UserList/index.tsx | 16 ++++++++--------
31 files changed, 100 insertions(+), 100 deletions(-)
diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx
index b5bc0cb6..b0d314d1 100644
--- a/src/components/Common/ButtonWithDropdown/index.tsx
+++ b/src/components/Common/ButtonWithDropdown/index.tsx
@@ -101,12 +101,12 @@ const ButtonWithDropdown = ({
(
appear
as="div"
className="fixed top-0 bottom-0 left-0 right-0 z-50 flex h-full w-full items-center justify-center bg-gray-800 bg-opacity-70"
- enter="transition opacity-0 duration-300"
+ enter="transition-opacity duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
- leave="transition opacity-100 duration-300"
+ leave="transition-opacity duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
ref={parentRef}
@@ -89,10 +89,10 @@ const Modal = React.forwardRef(
(
(
}}
appear
as="div"
- enter="transition opacity-0 duration-300 transform scale-75"
+ enter="transition duration-300"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
- leave="transition opacity-100 duration-300"
+ leave="transition-opacity duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={!loading}
diff --git a/src/components/Common/SlideCheckbox/index.tsx b/src/components/Common/SlideCheckbox/index.tsx
index a514d6c0..320dd667 100644
--- a/src/components/Common/SlideCheckbox/index.tsx
+++ b/src/components/Common/SlideCheckbox/index.tsx
@@ -29,7 +29,7 @@ const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => {
aria-hidden="true"
className={`${
checked ? 'translate-x-5' : 'translate-x-0'
- } absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+ } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
>
);
diff --git a/src/components/Common/SlideOver/index.tsx b/src/components/Common/SlideOver/index.tsx
index 48c1f854..6da8972a 100644
--- a/src/components/Common/SlideOver/index.tsx
+++ b/src/components/Common/SlideOver/index.tsx
@@ -37,10 +37,10 @@ const SlideOver = ({
as={Fragment}
show={show}
appear
- enter="opacity-0 transition ease-in-out duration-300"
+ enter="transition-opacity ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
- leave="opacity-100 transition ease-in-out duration-300"
+ leave="transition-opacity ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
@@ -58,10 +58,10 @@ const SlideOver = ({
diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx
index b9071b42..74383f13 100644
--- a/src/components/Discover/index.tsx
+++ b/src/components/Discover/index.tsx
@@ -165,10 +165,10 @@ const Discover = () => {
)}
diff --git a/src/components/IssueDetails/IssueDescription/index.tsx b/src/components/IssueDetails/IssueDescription/index.tsx
index 7121f095..7dc8c8d3 100644
--- a/src/components/IssueDetails/IssueDescription/index.tsx
+++ b/src/components/IssueDetails/IssueDescription/index.tsx
@@ -57,11 +57,11 @@ const IssueDescription = ({
show={open}
as="div"
enter="transition ease-out duration-100"
- enterFrom="transform opacity-0 scale-95"
- enterTo="transform opacity-100 scale-100"
+ enterFrom="opacity-0 scale-95"
+ enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75"
- leaveFrom="transform opacity-100 scale-100"
- leaveTo="transform opacity-0 scale-95"
+ leaveFrom="opacity-100 scale-100"
+ leaveTo="opacity-0 scale-95"
>
{
(
{
{
show={isOpen}
as="div"
ref={ref}
- enter="transition transform duration-500"
+ enter="transition duration-500"
enterFrom="opacity-0 translate-y-0"
enterTo="opacity-100 -translate-y-full"
- leave="transition duration-500 transform"
+ leave="transition duration-500"
leaveFrom="opacity-100 -translate-y-full"
leaveTo="opacity-0 translate-y-0"
- className="absolute top-0 left-0 right-0 flex w-full -translate-y-full transform flex-col space-y-6 border-t border-gray-600 bg-gray-900 bg-opacity-90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
+ className="absolute top-0 left-0 right-0 flex w-full -translate-y-full flex-col space-y-6 border-t border-gray-600 bg-gray-900 bg-opacity-90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
>
{filteredLinks.map((link) => {
const isActive = router.pathname.match(link.activeRegExp);
diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx
index 4baf56a6..d9f8ffd5 100644
--- a/src/components/Layout/Sidebar/index.tsx
+++ b/src/components/Layout/Sidebar/index.tsx
@@ -127,10 +127,10 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
diff --git a/src/components/Layout/UserDropdown/index.tsx b/src/components/Layout/UserDropdown/index.tsx
index c21a9c50..6d3fe7b9 100644
--- a/src/components/Layout/UserDropdown/index.tsx
+++ b/src/components/Layout/UserDropdown/index.tsx
@@ -63,11 +63,11 @@ const UserDropdown = () => {
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx
index 5ad862b1..fe92629a 100644
--- a/src/components/Login/index.tsx
+++ b/src/components/Login/index.tsx
@@ -95,10 +95,10 @@ const Login = () => {
diff --git a/src/components/RegionSelector/index.tsx b/src/components/RegionSelector/index.tsx
index d0a0113e..38febf9a 100644
--- a/src/components/RegionSelector/index.tsx
+++ b/src/components/RegionSelector/index.tsx
@@ -122,7 +122,7 @@ const RegionSelector = ({
@@ -389,7 +389,7 @@ const CollectionRequestModal = ({
isSelectedPart(part.id)
? 'translate-x-5'
: 'translate-x-0'
- } absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+ } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
>
diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx
index 4a6b25b8..25c8fd3c 100644
--- a/src/components/RequestModal/TvRequestModal.tsx
+++ b/src/components/RequestModal/TvRequestModal.tsx
@@ -540,7 +540,7 @@ const TvRequestModal = ({
aria-hidden="true"
className={`${
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
- } absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+ } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
>
@@ -631,7 +631,7 @@ const TvRequestModal = ({
isSelectedSeason(season.seasonNumber)
? 'translate-x-5'
: 'translate-x-0'
- } absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+ } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
>
diff --git a/src/components/RequestModal/index.tsx b/src/components/RequestModal/index.tsx
index e5421fb5..9ef6b405 100644
--- a/src/components/RequestModal/index.tsx
+++ b/src/components/RequestModal/index.tsx
@@ -29,10 +29,10 @@ const RequestModal = ({
return (
{
aria-hidden="true"
className={`${
isEnabled ? 'translate-x-5' : 'translate-x-0'
- } relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out`}
+ } relative inline-block h-5 w-5 rounded-full bg-white shadow transition duration-200 ease-in-out`}
>
{
as="div"
appear
show
- enter="transition ease-in-out duration-300 transform opacity-0"
+ enter="transition-opacity ease-in-out duration-300"
enterFrom="opacity-0"
- enterTo="opacuty-100"
- leave="transition ease-in-out duration-300 transform opacity-100"
+ enterTo="opacity-100"
+ leave="transition-opacity ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
diff --git a/src/components/Settings/SettingsAbout/Releases/index.tsx b/src/components/Settings/SettingsAbout/Releases/index.tsx
index 93ffd6de..c697dc6b 100644
--- a/src/components/Settings/SettingsAbout/Releases/index.tsx
+++ b/src/components/Settings/SettingsAbout/Releases/index.tsx
@@ -63,10 +63,10 @@ const Release = ({ currentVersion, release, isLatest }: ReleaseProps) => {
{
/>
{
/>
{
diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx
index ab5f9758..d9ff0c17 100644
--- a/src/components/Settings/SonarrModal/index.tsx
+++ b/src/components/Settings/SonarrModal/index.tsx
@@ -223,10 +223,10 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
as="div"
appear
show
- enter="transition ease-in-out duration-300 transform opacity-0"
+ enter="transition-opacity ease-in-out duration-300"
enterFrom="opacity-0"
- enterTo="opacuty-100"
- leave="transition ease-in-out duration-300 transform opacity-100"
+ enterTo="opacity-100"
+ leave="transition-opacity ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
diff --git a/src/components/StatusChecker/index.tsx b/src/components/StatusChecker/index.tsx
index 2a3977fd..41377393 100644
--- a/src/components/StatusChecker/index.tsx
+++ b/src/components/StatusChecker/index.tsx
@@ -44,10 +44,10 @@ const StatusChecker = () => {
return (
@@ -169,10 +169,10 @@ const TitleCard = ({
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx
index 5330141a..c450ef4a 100644
--- a/src/components/TvDetails/index.tsx
+++ b/src/components/TvDetails/index.tsx
@@ -735,18 +735,18 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
)}
diff --git a/src/components/UserList/PlexImportModal.tsx b/src/components/UserList/PlexImportModal.tsx
index 98a24829..11f64398 100644
--- a/src/components/UserList/PlexImportModal.tsx
+++ b/src/components/UserList/PlexImportModal.tsx
@@ -155,7 +155,7 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
aria-hidden="true"
className={`${
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
- } absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+ } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
>
@@ -194,7 +194,7 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
isSelectedUser(user.id)
? 'translate-x-5'
: 'translate-x-0'
- } absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+ } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
>
diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx
index fcab9127..b37751f4 100644
--- a/src/components/UserList/index.tsx
+++ b/src/components/UserList/index.tsx
@@ -228,10 +228,10 @@ const UserList = () => {
{
{
{
Date: Fri, 24 Feb 2023 04:40:01 +0400
Subject: [PATCH 13/18] fix(watchlist): correctly load more than 20 watchlist
items (#3351)
* fix(discover): correctly load additional watchlist items pages
* chore(discover): remove unused params types
---
server/routes/discover.ts | 8 ++++----
server/routes/user/index.ts | 8 ++++----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/server/routes/discover.ts b/server/routes/discover.ts
index 2c3c665f..f032fa66 100644
--- a/server/routes/discover.ts
+++ b/server/routes/discover.ts
@@ -800,12 +800,12 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
}
);
-discoverRoutes.get<{ page?: number }, WatchlistResponse>(
+discoverRoutes.get, WatchlistResponse>(
'/watchlist',
async (req, res) => {
const userRepository = getRepository(User);
const itemsPerPage = 20;
- const page = req.params.page ?? 1;
+ const page = Number(req.query.page) ?? 1;
const offset = (page - 1) * itemsPerPage;
const activeUser = await userRepository.findOne({
@@ -829,8 +829,8 @@ discoverRoutes.get<{ page?: number }, WatchlistResponse>(
return res.json({
page,
- totalPages: Math.ceil(watchlist.size / itemsPerPage),
- totalResults: watchlist.size,
+ totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
+ totalResults: watchlist.totalSize,
results: watchlist.items.map((item) => ({
ratingKey: item.ratingKey,
title: item.title,
diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts
index f77b7e51..ea709caf 100644
--- a/server/routes/user/index.ts
+++ b/server/routes/user/index.ts
@@ -607,7 +607,7 @@ router.get<{ id: string }, UserWatchDataResponse>(
}
);
-router.get<{ id: string; page?: number }, WatchlistResponse>(
+router.get<{ id: string }, WatchlistResponse>(
'/:id/watchlist',
async (req, res, next) => {
if (
@@ -627,7 +627,7 @@ router.get<{ id: string; page?: number }, WatchlistResponse>(
}
const itemsPerPage = 20;
- const page = req.params.page ?? 1;
+ const page = Number(req.query.page) ?? 1;
const offset = (page - 1) * itemsPerPage;
const user = await getRepository(User).findOneOrFail({
@@ -651,8 +651,8 @@ router.get<{ id: string; page?: number }, WatchlistResponse>(
return res.json({
page,
- totalPages: Math.ceil(watchlist.size / itemsPerPage),
- totalResults: watchlist.size,
+ totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
+ totalResults: watchlist.totalSize,
results: watchlist.items.map((item) => ({
ratingKey: item.ratingKey,
title: item.title,
From b3882de8930a70adb2f93a27be6370bfa1826587 Mon Sep 17 00:00:00 2001
From: Danshil Kokil Mungur
Date: Fri, 24 Feb 2023 06:03:01 +0400
Subject: [PATCH 14/18] fix(ui): hide search bar behind slideover when opened
(#3348)
---
.vscode/settings.json | 5 ++++-
src/components/Common/SlideOver/index.tsx | 2 +-
src/styles/globals.css | 4 ++--
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 45da7ba6..1a237571 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -16,5 +16,8 @@
}
],
"editor.formatOnSave": true,
- "typescript.preferences.importModuleSpecifier": "non-relative"
+ "typescript.preferences.importModuleSpecifier": "non-relative",
+ "files.associations": {
+ "globals.css": "tailwindcss"
+ }
}
diff --git a/src/components/Common/SlideOver/index.tsx b/src/components/Common/SlideOver/index.tsx
index 6da8972a..ec2ea263 100644
--- a/src/components/Common/SlideOver/index.tsx
+++ b/src/components/Common/SlideOver/index.tsx
@@ -67,7 +67,7 @@ const SlideOver = ({
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
e.stopPropagation()}
>
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 8d040a00..97b2a7b8 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -43,8 +43,8 @@
}
.slideover {
- padding-top: calc(1rem + env(safe-area-inset-top)) !important;
- padding-bottom: calc(1rem + env(safe-area-inset-top)) !important;
+ padding-top: calc(0.75rem + env(safe-area-inset-top)) !important;
+ padding-bottom: calc(0.75rem + env(safe-area-inset-top)) !important;
}
.sidebar-close-button {
From ae3818304b2f75222d1bd223ece94f829a3b42d0 Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Fri, 24 Feb 2023 00:28:22 -0500
Subject: [PATCH 15/18] feat: availability sync rework (#3219)
* feat: add availability synchronization job
fix #377
* fix: feedback on PR
* perf: use pagination for Media Availability Synchronization job
The original approach loaded all media items from the database at once. With large libraries, this
could lead to performance issues. We're now using a paginated approach with a page size of 50.
* feat: updated the availability sync to work with 4k
* fix: corrected detection of media in plex
* refactor: code cleanup and minimized unnecessary calls
* fix: if media is not found, media check will continue
* fix: if non-4k or 4k show media is updated, seasons and request is now properly updated
* refactor: consolidated media updater into one function
* fix: season requests are now removed if season has been deleted
* refactor: removed joincolumn
* fix: makes sure we will always check radarr/sonarr to see if media exists
* fix: media will now only be updated to unavailable and deletion will be prevented
* fix: changed types in Media entity
* fix: prevent season deletion in preference of setting season to unknown
---------
Co-authored-by: Jari Zwarts
Co-authored-by: Sebastian Kappen
---
server/api/servarr/sonarr.ts | 2 +-
server/entity/Media.ts | 48 +-
server/entity/MediaRequest.ts | 2 +
server/entity/SeasonRequest.ts | 14 +
server/job/schedule.ts | 18 +
server/lib/availabilitySync.ts | 718 ++++++++++++++++++
server/lib/settings.ts | 6 +-
.../Settings/SettingsJobsCache/index.tsx | 1 +
src/i18n/locale/en.json | 1 +
9 files changed, 788 insertions(+), 22 deletions(-)
create mode 100644 server/lib/availabilitySync.ts
diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts
index eca0208c..2e423ef3 100644
--- a/server/api/servarr/sonarr.ts
+++ b/server/api/servarr/sonarr.ts
@@ -1,7 +1,7 @@
import logger from '@server/logger';
import ServarrBase from './base';
-interface SonarrSeason {
+export interface SonarrSeason {
seasonNumber: number;
monitored: boolean;
statistics?: {
diff --git a/server/entity/Media.ts b/server/entity/Media.ts
index 6a681c47..2d169172 100644
--- a/server/entity/Media.ts
+++ b/server/entity/Media.ts
@@ -114,29 +114,29 @@ class Media {
@Column({ type: 'datetime', nullable: true })
public mediaAddedAt: Date;
- @Column({ nullable: true })
- public serviceId?: number;
+ @Column({ nullable: true, type: 'int' })
+ public serviceId?: number | null;
- @Column({ nullable: true })
- public serviceId4k?: number;
+ @Column({ nullable: true, type: 'int' })
+ public serviceId4k?: number | null;
- @Column({ nullable: true })
- public externalServiceId?: number;
+ @Column({ nullable: true, type: 'int' })
+ public externalServiceId?: number | null;
- @Column({ nullable: true })
- public externalServiceId4k?: number;
+ @Column({ nullable: true, type: 'int' })
+ public externalServiceId4k?: number | null;
- @Column({ nullable: true })
- public externalServiceSlug?: string;
+ @Column({ nullable: true, type: 'varchar' })
+ public externalServiceSlug?: string | null;
- @Column({ nullable: true })
- public externalServiceSlug4k?: string;
+ @Column({ nullable: true, type: 'varchar' })
+ public externalServiceSlug4k?: string | null;
- @Column({ nullable: true })
- public ratingKey?: string;
+ @Column({ nullable: true, type: 'varchar' })
+ public ratingKey?: string | null;
- @Column({ nullable: true })
- public ratingKey4k?: string;
+ @Column({ nullable: true, type: 'varchar' })
+ public ratingKey4k?: string | null;
public serviceUrl?: string;
public serviceUrl4k?: string;
@@ -260,7 +260,9 @@ class Media {
if (this.mediaType === MediaType.MOVIE) {
if (
this.externalServiceId !== undefined &&
- this.serviceId !== undefined
+ this.externalServiceId !== null &&
+ this.serviceId !== undefined &&
+ this.serviceId !== null
) {
this.downloadStatus = downloadTracker.getMovieProgress(
this.serviceId,
@@ -270,7 +272,9 @@ class Media {
if (
this.externalServiceId4k !== undefined &&
- this.serviceId4k !== undefined
+ this.externalServiceId4k !== null &&
+ this.serviceId4k !== undefined &&
+ this.serviceId4k !== null
) {
this.downloadStatus4k = downloadTracker.getMovieProgress(
this.serviceId4k,
@@ -282,7 +286,9 @@ class Media {
if (this.mediaType === MediaType.TV) {
if (
this.externalServiceId !== undefined &&
- this.serviceId !== undefined
+ this.externalServiceId !== null &&
+ this.serviceId !== undefined &&
+ this.serviceId !== null
) {
this.downloadStatus = downloadTracker.getSeriesProgress(
this.serviceId,
@@ -292,7 +298,9 @@ class Media {
if (
this.externalServiceId4k !== undefined &&
- this.serviceId4k !== undefined
+ this.externalServiceId4k !== null &&
+ this.serviceId4k !== undefined &&
+ this.serviceId4k !== null
) {
this.downloadStatus4k = downloadTracker.getSeriesProgress(
this.serviceId4k,
diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index fad97ef6..61122afc 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -1187,3 +1187,5 @@ export class MediaRequest {
}
}
}
+
+export default MediaRequest;
diff --git a/server/entity/SeasonRequest.ts b/server/entity/SeasonRequest.ts
index f9eeef50..c55906eb 100644
--- a/server/entity/SeasonRequest.ts
+++ b/server/entity/SeasonRequest.ts
@@ -1,5 +1,7 @@
import { MediaRequestStatus } from '@server/constants/media';
+import { getRepository } from '@server/datasource';
import {
+ AfterRemove,
Column,
CreateDateColumn,
Entity,
@@ -34,6 +36,18 @@ class SeasonRequest {
constructor(init?: Partial) {
Object.assign(this, init);
}
+
+ @AfterRemove()
+ public async handleRemoveParent(): Promise {
+ const mediaRequestRepository = getRepository(MediaRequest);
+ const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({
+ where: { id: this.request.id },
+ });
+
+ if (requestToBeDeleted.seasons.length === 0) {
+ await mediaRequestRepository.delete({ id: this.request.id });
+ }
+ }
}
export default SeasonRequest;
diff --git a/server/job/schedule.ts b/server/job/schedule.ts
index 7925dd21..998abf1f 100644
--- a/server/job/schedule.ts
+++ b/server/job/schedule.ts
@@ -1,3 +1,4 @@
+import availabilitySync from '@server/lib/availabilitySync';
import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy';
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
@@ -104,6 +105,23 @@ export const startJobs = (): void => {
cancelFn: () => sonarrScanner.cancel(),
});
+ // Checks if media is still available in plex/sonarr/radarr libs
+ scheduledJobs.push({
+ id: 'availability-sync',
+ name: 'Media Availability Sync',
+ type: 'process',
+ interval: 'hours',
+ cronSchedule: jobs['availability-sync'].schedule,
+ job: schedule.scheduleJob(jobs['availability-sync'].schedule, () => {
+ logger.info('Starting scheduled job: Media Availability Sync', {
+ label: 'Jobs',
+ });
+ availabilitySync.run();
+ }),
+ running: () => availabilitySync.running,
+ cancelFn: () => availabilitySync.cancel(),
+ });
+
// Run download sync every minute
scheduledJobs.push({
id: 'download-sync',
diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts
new file mode 100644
index 00000000..d192259f
--- /dev/null
+++ b/server/lib/availabilitySync.ts
@@ -0,0 +1,718 @@
+import type { PlexMetadata } from '@server/api/plexapi';
+import PlexAPI from '@server/api/plexapi';
+import RadarrAPI from '@server/api/servarr/radarr';
+import type { SonarrSeason } from '@server/api/servarr/sonarr';
+import SonarrAPI from '@server/api/servarr/sonarr';
+import { 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 SeasonRequest from '@server/entity/SeasonRequest';
+import { User } from '@server/entity/User';
+import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
+import { getSettings } from '@server/lib/settings';
+import logger from '@server/logger';
+
+class AvailabilitySync {
+ public running = false;
+ private plexClient: PlexAPI;
+ private plexSeasonsCache: Record = {};
+ private sonarrSeasonsCache: Record = {};
+ private radarrServers: RadarrSettings[];
+ private sonarrServers: SonarrSettings[];
+
+ async run() {
+ const settings = getSettings();
+ this.running = true;
+ this.plexSeasonsCache = {};
+ 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;
+ }
+
+ 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)) {
+ try {
+ if (!this.running) {
+ throw new Error('Job aborted');
+ }
+
+ 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 } },
+ });
+
+ 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' }
+ );
+
+ 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 (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 },
+ {
+ 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.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,
+ }
+ );
+ }
+
+ 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;
+ }
+ }
+
+ 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 (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,
+ });
+ }
+ }
+ }
+ }
+ }
+ } catch (ex) {
+ logger.error('Failure with media.', {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ });
+ }
+ }
+ } catch (ex) {
+ logger.error('Failed to complete availability sync.', {
+ errorMessage: ex.message,
+ label: 'AvailabilitySync',
+ });
+ } finally {
+ logger.info(`Availability sync complete.`, {
+ label: 'AvailabilitySync',
+ });
+ this.running = false;
+ }
+ }
+
+ public cancel() {
+ this.running = false;
+ }
+
+ private async *loadAvailableMediaPaginated(pageSize: number) {
+ let offset = 0;
+ const mediaRepository = getRepository(Media);
+ const whereOptions = [
+ { status: MediaStatus.AVAILABLE },
+ { status: MediaStatus.PARTIALLY_AVAILABLE },
+ { status4k: MediaStatus.AVAILABLE },
+ { status4k: MediaStatus.PARTIALLY_AVAILABLE },
+ ];
+
+ let mediaPage: Media[];
+
+ do {
+ yield* (mediaPage = await mediaRepository.find({
+ where: whereOptions,
+ skip: offset,
+ take: pageSize,
+ }));
+ offset += pageSize;
+ } while (mediaPage.length > 0);
+ }
+
+ private async mediaUpdater(media: Media, is4k: boolean): Promise {
+ const mediaRepository = getRepository(Media);
+ const requestRepository = getRepository(MediaRequest);
+
+ 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.tmdbId} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
+ isTVType ? 'sonarr' : 'radarr'
+ } and plex instance. We will change its status 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,
+ }
+ );
+
+ 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 });
+ }
+
+ private async mediaExistsInRadarr(
+ media: Media,
+ existsInPlex: boolean,
+ existsInPlex4k: boolean
+ ): Promise {
+ let existsInRadarr = true;
+ let existsInRadarr4k = true;
+
+ for (const server of this.radarrServers) {
+ const api = new RadarrAPI({
+ apiKey: server.apiKey,
+ url: RadarrAPI.buildUrl(server, '/api/v3'),
+ });
+ const meta = await api.getMovieByTmdbId(media.tmdbId);
+
+ //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;
+ }
+
+ if (server.is4k && !meta.id) {
+ existsInRadarr4k = false;
+ }
+ }
+
+ if (existsInRadarr && existsInRadarr4k) {
+ return true;
+ }
+
+ if (!existsInRadarr && existsInPlex) {
+ return true;
+ }
+
+ if (!existsInRadarr4k && existsInPlex4k) {
+ return true;
+ }
+
+ //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 (media.status !== MediaStatus.UNKNOWN) {
+ this.mediaUpdater(media, false);
+ }
+ }
+
+ if (existsInRadarr && !existsInRadarr4k && !existsInPlex4k) {
+ if (media.status4k !== MediaStatus.UNKNOWN) {
+ this.mediaUpdater(media, true);
+ }
+ }
+
+ if (existsInRadarr || existsInRadarr4k) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private async mediaExistsInSonarr(
+ media: Media,
+ existsInPlex: boolean,
+ existsInPlex4k: boolean
+ ): Promise {
+ if (!media.tvdbId) {
+ return false;
+ }
+
+ let existsInSonarr = true;
+ let existsInSonarr4k = true;
+
+ for (const server of this.sonarrServers) {
+ const api = new SonarrAPI({
+ apiKey: server.apiKey,
+ url: SonarrAPI.buildUrl(server, '/api/v3'),
+ });
+
+ const meta = await api.getSeriesByTvdbId(media.tvdbId);
+
+ this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = 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 && !meta.id) {
+ existsInSonarr4k = false;
+ }
+ }
+
+ if (existsInSonarr && existsInSonarr4k) {
+ return true;
+ }
+
+ if (!existsInSonarr && existsInPlex) {
+ return true;
+ }
+
+ if (!existsInSonarr4k && existsInPlex4k) {
+ return true;
+ }
+
+ //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 (media.status !== MediaStatus.UNKNOWN) {
+ this.mediaUpdater(media, false);
+ }
+ }
+
+ if (existsInSonarr && !existsInSonarr4k && !existsInPlex4k) {
+ if (media.status4k !== MediaStatus.UNKNOWN) {
+ this.mediaUpdater(media, true);
+ }
+ }
+
+ if (existsInSonarr || existsInSonarr4k) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private async seasonExistsInSonarr(
+ media: Media,
+ season: Season,
+ seasonExistsInPlex: boolean,
+ seasonExistsInPlex4k: boolean
+ ): Promise {
+ if (!media.tvdbId) {
+ return false;
+ }
+
+ let seasonExistsInSonarr = true;
+ let seasonExistsInSonarr4k = true;
+
+ const mediaRepository = getRepository(Media);
+ const seasonRepository = getRepository(Season);
+ const seasonRequestRepository = getRepository(SeasonRequest);
+
+ for (const server of this.sonarrServers) {
+ const api = new SonarrAPI({
+ apiKey: server.apiKey,
+ 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;
+
+ const hasMonitoredSeason = seasons.find(
+ ({ monitored, seasonNumber }) =>
+ monitored && season.seasonNumber === seasonNumber
+ );
+
+ if (!server.is4k && !hasMonitoredSeason) {
+ seasonExistsInSonarr = false;
+ }
+
+ if (server.is4k && !hasMonitoredSeason) {
+ seasonExistsInSonarr4k = false;
+ }
+ }
+
+ if (seasonExistsInSonarr && seasonExistsInSonarr4k) {
+ return true;
+ }
+
+ if (!seasonExistsInSonarr && seasonExistsInPlex) {
+ return true;
+ }
+
+ if (!seasonExistsInSonarr4k && seasonExistsInPlex4k) {
+ return true;
+ }
+
+ 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 &&
+ !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.`,
+ { 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.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
+ { label: 'AvailabilitySync' }
+ );
+ await mediaRepository.update(media.id, {
+ status: MediaStatus.PARTIALLY_AVAILABLE,
+ });
+ }
+ }
+ }
+
+ if (
+ seasonExistsInSonarr &&
+ !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.`,
+ { label: 'AvailabilitySync' }
+ );
+ 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.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
+ { label: 'AvailabilitySync' }
+ );
+ await mediaRepository.update(media.id, {
+ status4k: MediaStatus.PARTIALLY_AVAILABLE,
+ });
+ }
+ }
+ }
+
+ if (seasonExistsInSonarr || seasonExistsInSonarr4k) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private async mediaExists(media: Media): Promise {
+ const ratingKey = media.ratingKey;
+ const ratingKey4k = media.ratingKey4k;
+
+ let existsInPlex = false;
+ let existsInPlex4k = false;
+
+ //check each plex instance to see if media exists
+ try {
+ if (ratingKey) {
+ const meta = await this.plexClient?.getMetadata(ratingKey);
+ if (meta) {
+ existsInPlex = true;
+ }
+ }
+ if (ratingKey4k) {
+ const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
+ if (meta4k) {
+ existsInPlex4k = true;
+ }
+ }
+ } 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
+ 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.info(
+ `${media.tmdbId} exists in at least one radarr or plex instance. Media will be updated if set to available.`,
+ {
+ label: 'AvailabilitySync',
+ }
+ );
+
+ return true;
+ }
+ }
+
+ 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.info(
+ `${media.tvdbId} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
+ {
+ label: 'AvailabilitySync',
+ }
+ );
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private async seasonExists(media: Media, season: Season) {
+ const ratingKey = media.ratingKey;
+ const ratingKey4k = media.ratingKey4k;
+
+ 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
+ );
+
+ 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 (seasonMeta4k) {
+ seasonExistsInPlex4k = true;
+ }
+ }
+
+ //base case for if both season versions exist in plex
+ if (seasonExistsInPlex && seasonExistsInPlex4k) {
+ return true;
+ }
+
+ const existsInSonarr = await this.seasonExistsInSonarr(
+ media,
+ season,
+ seasonExistsInPlex,
+ seasonExistsInPlex4k
+ );
+
+ if (existsInSonarr) {
+ logger.info(
+ `${media.tvdbId}, season: ${season.seasonNumber} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
+ {
+ label: 'AvailabilitySync',
+ }
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private async initPlexClient() {
+ const userRepository = getRepository(User);
+ const admin = await userRepository.findOne({
+ select: { id: true, plexToken: true },
+ where: { id: 1 },
+ });
+
+ if (!admin) {
+ logger.warning('No admin configured. Availability sync skipped.');
+ return;
+ }
+
+ this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
+ }
+}
+
+const availabilitySync = new AvailabilitySync();
+export default availabilitySync;
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index 86bcf144..8e66ebc5 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -248,7 +248,8 @@ export type JobId =
| 'sonarr-scan'
| 'download-sync'
| 'download-sync-reset'
- | 'image-cache-cleanup';
+ | 'image-cache-cleanup'
+ | 'availability-sync';
interface AllSettings {
clientId: string;
@@ -409,6 +410,9 @@ class Settings {
'sonarr-scan': {
schedule: '0 30 4 * * *',
},
+ 'availability-sync': {
+ schedule: '0 0 5 * * *',
+ },
'download-sync': {
schedule: '0 * * * * *',
},
diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx
index 8193a3bb..72bed7c4 100644
--- a/src/components/Settings/SettingsJobsCache/index.tsx
+++ b/src/components/Settings/SettingsJobsCache/index.tsx
@@ -53,6 +53,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'plex-recently-added-scan': 'Plex Recently Added Scan',
'plex-full-scan': 'Plex Full Library Scan',
'plex-watchlist-sync': 'Plex Watchlist Sync',
+ 'availability-sync': 'Media Availability Sync',
'radarr-scan': 'Radarr Scan',
'sonarr-scan': 'Sonarr Scan',
'download-sync': 'Download Sync',
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index 0315916d..a762f951 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -721,6 +721,7 @@
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
"components.Settings.SettingsAbout.uptodate": "Up to Date",
"components.Settings.SettingsAbout.version": "Version",
+ "components.Settings.SettingsJobsCache.availability-sync": "Media Availability Sync",
"components.Settings.SettingsJobsCache.cache": "Cache",
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} cache flushed.",
From c36a4ba2b8df05873f5dfd0946a9bc3dc4ecfd1d Mon Sep 17 00:00:00 2001
From: Brandon Cohen
Date: Fri, 24 Feb 2023 02:29:42 -0500
Subject: [PATCH 16/18] fix: logger was set to info for the wrong logs (#3354)
---
server/lib/availabilitySync.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts
index d192259f..93ccfe39 100644
--- a/server/lib/availabilitySync.ts
+++ b/server/lib/availabilitySync.ts
@@ -601,7 +601,7 @@ class AvailabilitySync {
//if true, media exists in at least one radarr or plex instance.
if (existsInRadarr) {
- logger.info(
+ logger.warn(
`${media.tmdbId} exists in at least one radarr or plex instance. Media will be updated if set to available.`,
{
label: 'AvailabilitySync',
@@ -621,7 +621,7 @@ class AvailabilitySync {
//if true, media exists in at least one sonarr or plex instance.
if (existsInSonarr) {
- logger.info(
+ logger.warn(
`${media.tvdbId} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
{
label: 'AvailabilitySync',
@@ -685,7 +685,7 @@ class AvailabilitySync {
);
if (existsInSonarr) {
- logger.info(
+ logger.warn(
`${media.tvdbId}, season: ${season.seasonNumber} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
{
label: 'AvailabilitySync',
From 2a3213d706bca34dceccd9c3edeb0081ca4e9344 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 24 Feb 2023 19:46:46 +0900
Subject: [PATCH 17/18] docs: add Nimelrian as a contributor for code (#3356)
[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 3202486c..f8c3c197 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -791,6 +791,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "Nimelrian",
+ "name": "Sebastian K",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8960836?v=4",
+ "profile": "https://github.com/Nimelrian",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "
-orange.svg\"/>",
diff --git a/README.md b/README.md
index a17dad13..b24b2cd5 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -185,6 +185,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Pedro Nascimento 💻 |
 Owen Voke 💻 |
+  Sebastian K 💻 |
From 04980f93ab9b126fff2ee21321a5244bcb9c9d79 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 24 Feb 2023 19:47:36 +0900
Subject: [PATCH 18/18] docs: add jariz as a contributor for code (#3357) [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 f8c3c197..113d0af4 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -800,6 +800,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "jariz",
+ "name": "jariz",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1415847?v=4",
+ "profile": "https://github.com/jariz",
+ "contributions": [
+ "code"
+ ]
}
],
"badgeTemplate": "
-orange.svg\"/>",
diff --git a/README.md b/README.md
index b24b2cd5..83fff5ea 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -186,6 +186,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Pedro Nascimento 💻 |
 Owen Voke 💻 |
 Sebastian K 💻 |
+  jariz 💻 |