From b6eac0f364680675aa6ad3aaf5d858c27873dcdf Mon Sep 17 00:00:00 2001 From: Ryan Cohen Date: Sat, 11 Feb 2023 17:18:30 +0900 Subject: [PATCH 01/44] 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 02/44] 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 03/44] 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 04/44] 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 05/44] 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": "\"All-orange.svg\"/>", diff --git a/README.md b/README.md index ae82f548..af828033 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Translation status GitHub -All Contributors +All Contributors

@@ -182,6 +182,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d ceptonit
ceptonit

📖 aedelbro
aedelbro

💻 + + Pedro Nascimento
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 06/44] 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 07/44] 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": "\"All-orange.svg\"/>", diff --git a/README.md b/README.md index af828033..a17dad13 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Translation status GitHub -All Contributors +All Contributors

@@ -184,6 +184,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Pedro Nascimento
Pedro Nascimento

💻 + Owen Voke
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 08/44] 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 09/44] 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 | 30 ++++++++++---- 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, 119 insertions(+), 24 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,6 +40,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { const [requestModal, setRequestModal] = useState(false); const [is4k, setIs4k] = useState(false); + const returnCollectionDownloadItems = (data: Collection | undefined) => { + const [downloadStatus, downloadStatus4k] = [ + data?.parts.flatMap((item) => + item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] + ), + data?.parts.flatMap((item) => + item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : [] + ), + ]; + + return { downloadStatus, downloadStatus4k }; + }; + const { data, error, @@ -46,21 +60,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { } = 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(() => { - return [ - data?.parts.flatMap((item) => - item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] - ), - data?.parts.flatMap((item) => - item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : [] - ), - ]; - }, [data?.parts]); + 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' ? (