diff --git a/docs/using-seerr/settings/general.md b/docs/using-seerr/settings/general.md index 65b337495f..24bf19daa9 100644 --- a/docs/using-seerr/settings/general.md +++ b/docs/using-seerr/settings/general.md @@ -71,6 +71,14 @@ When enabled, media that has been blocklisted will not appear on the "Discover" This setting is **disabled** by default. +## Hide Requested Media + +When enabled, media that has been requested (but not yet available) will not appear on the "Discover" home page, or in the "Recommended" or "Similar" categories or other links on media detail pages. + +Requested media will still appear in search results, however, so it is possible to locate and view hidden items by searching for them by title. + +This setting is **disabled** by default. + ## Allow Partial Series Requests When enabled, users will be able to submit requests for specific seasons of TV series. If disabled, users will only be able to submit requests for all unavailable seasons. diff --git a/seerr-api.yml b/seerr-api.yml index cd0012b679..89539bf5fa 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -232,6 +232,9 @@ components: hideAvailable: type: boolean example: false + hideRequested: + type: boolean + example: false partialRequestsEnabled: type: boolean example: false diff --git a/server/entity/Media.ts b/server/entity/Media.ts index a63003df66..2ddf104f5d 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -1,6 +1,10 @@ import RadarrAPI from '@server/api/servarr/radarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { Blocklist } from '@server/entity/Blocklist'; @@ -49,13 +53,36 @@ class Media { 'watchlist', 'media.id= watchlist.media and watchlist.requestedBy = :userId', { userId: user?.id } - ) //, + ) .where(' media.tmdbId in (:...finalIds)', { finalIds }) .getMany(); - return media.filter((m) => + const relatedMedia = media.filter((m) => items.some((i) => i.tmdbId === m.tmdbId && i.mediaType === m.mediaType) ); + + if (getSettings().main.hideRequested && relatedMedia.length > 0) { + const activeRequestMediaIds = await mediaRepository + .createQueryBuilder('media') + .select('media.id', 'id') + .distinct(true) + .innerJoin('media.requests', 'request') + .where('media.id IN (:...mediaIds)', { + mediaIds: relatedMedia.map((m) => m.id), + }) + .andWhere('request.status IN (:...statuses)', { + statuses: [MediaRequestStatus.PENDING, MediaRequestStatus.APPROVED], + }) + .getRawMany<{ id: number }>(); + + const activeIds = new Set(activeRequestMediaIds.map((row) => row.id)); + + relatedMedia.forEach((m) => { + m.hasActiveRequest = activeIds.has(m.id); + }); + } + + return relatedMedia; } catch (e) { logger.error(e.message); return []; @@ -187,6 +214,7 @@ class Media { public serviceUrl?: string; public serviceUrl4k?: string; + public hasActiveRequest?: boolean; public downloadStatus?: DownloadingItem[] = []; public downloadStatus4k?: DownloadingItem[] = []; diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 3dfa619205..c044683b7f 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -31,6 +31,7 @@ export interface PublicSettingsResponse { applicationUrl: string; hideAvailable: boolean; hideBlocklisted: boolean; + hideRequested: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 010c43d81d..b85571d651 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -141,6 +141,7 @@ export interface MainSettings { }; hideAvailable: boolean; hideBlocklisted: boolean; + hideRequested: boolean; localLogin: boolean; mediaServerLogin: boolean; newPlexLogin: boolean; @@ -193,6 +194,7 @@ interface FullPublicSettings extends PublicSettings { applicationUrl: string; hideAvailable: boolean; hideBlocklisted: boolean; + hideRequested: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; @@ -414,6 +416,7 @@ class Settings { }, hideAvailable: false, hideBlocklisted: false, + hideRequested: false, localLogin: true, mediaServerLogin: true, newPlexLogin: true, @@ -709,6 +712,7 @@ class Settings { applicationUrl: this.data.main.applicationUrl, hideAvailable: this.data.main.hideAvailable, hideBlocklisted: this.data.main.hideBlocklisted, + hideRequested: this.data.main.hideRequested, localLogin: this.data.main.localLogin, mediaServerLogin: this.data.main.mediaServerLogin, jellyfinExternalHost: this.data.jellyfin.externalHostname, diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx index 4418c12314..46eae4236d 100644 --- a/src/components/MediaSlider/index.tsx +++ b/src/components/MediaSlider/index.tsx @@ -82,6 +82,16 @@ const MediaSlider = ({ ); } + if (settings.currentSettings.hideRequested) { + titles = titles.filter((i) => { + if (i.mediaType !== 'movie' && i.mediaType !== 'tv') { + return true; + } + + return !i.mediaInfo?.hasActiveRequest; + }); + } + useEffect(() => { if ( titles.length < 24 && diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index e5a54180bb..bf0a83317d 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -34,7 +34,7 @@ const Search = () => { { query: router.query.query, }, - { hideAvailable: false, hideBlocklisted: false } + { hideAvailable: false, hideBlocklisted: false, hideRequested: false } ); if (error) { diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 00703ddd37..2535433815 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -62,6 +62,9 @@ const messages = defineMessages('components.Settings.SettingsMain', { hideAvailable: 'Hide Available Media', hideAvailableTip: 'Hide available media from the discover pages but not search results', + hideRequested: 'Hide Requested Media', + hideRequestedTip: + 'Hide media that has been requested from the discover pages but not search results', cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', @@ -171,6 +174,7 @@ const SettingsMain = () => { applicationUrl: data?.applicationUrl, hideAvailable: data?.hideAvailable, hideBlocklisted: data?.hideBlocklisted, + hideRequested: data?.hideRequested, locale: data?.locale ?? 'en', discoverRegion: data?.discoverRegion, originalLanguage: data?.originalLanguage, @@ -193,6 +197,7 @@ const SettingsMain = () => { applicationUrl: values.applicationUrl, hideAvailable: values.hideAvailable, hideBlocklisted: values.hideBlocklisted, + hideRequested: values.hideRequested, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, @@ -538,6 +543,26 @@ const SettingsMain = () => { /> +