From b2878390b486e338151f26a2354711147012f88e Mon Sep 17 00:00:00 2001 From: Brandon Cohen Date: Mon, 25 Apr 2022 06:47:38 -0400 Subject: [PATCH 1/8] fix: manual browser refresh would redirect to home on search page (#2692) If you used the search feature and tried to manual refresh or share the link, it would reset the query. Taking you back to the home page. fix #2683 --- src/hooks/useSearchInput.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useSearchInput.ts b/src/hooks/useSearchInput.ts index 8e879279a..fd4d20885 100644 --- a/src/hooks/useSearchInput.ts +++ b/src/hooks/useSearchInput.ts @@ -1,9 +1,9 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import type { UrlObject } from 'url'; -import { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import useDebouncedState from './useDebouncedState'; import { useRouter } from 'next/router'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import type { UrlObject } from 'url'; import type { Nullable } from '../utils/typeHelpers'; +import useDebouncedState from './useDebouncedState'; type Url = string | UrlObject; @@ -48,7 +48,7 @@ const useSearchInput = (): SearchObject => { * in a new route. If we are, then we only replace the history. */ useEffect(() => { - if (debouncedValue !== '') { + if (debouncedValue !== '' && searchOpen) { if (router.pathname.startsWith('/search')) { router.replace({ pathname: router.pathname, From 1d00229a485bb2b376e9f63b52c70c7719f5f023 Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Mon, 25 Apr 2022 09:09:22 -0400 Subject: [PATCH 2/8] feat(discover): add Paramount+ to network slider (#2608) --- src/components/Discover/NetworkSlider/index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Discover/NetworkSlider/index.tsx b/src/components/Discover/NetworkSlider/index.tsx index 44c776cf0..61468a6fb 100644 --- a/src/components/Discover/NetworkSlider/index.tsx +++ b/src/components/Discover/NetworkSlider/index.tsx @@ -110,6 +110,12 @@ const networks: Network[] = [ 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/nm8d7P7MJNiBLdgIzUK0gkuEA4r.png', url: '/discover/tv/network/16', }, + { + name: 'Paramount+', + image: + 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/fi83B1oztoS47xxcemFdPMhIzK.png', + url: '/discover/tv/network/4330', + }, { name: 'BBC One', image: From 475314c87bd0a79419702802a4d04787ef764f65 Mon Sep 17 00:00:00 2001 From: Gian Marco Cinalli Date: Mon, 25 Apr 2022 18:13:41 +0200 Subject: [PATCH 3/8] docs: update fail2ban documentation to handle logging changes (#2707) [skip ci] --- docs/extending-overseerr/fail2ban.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extending-overseerr/fail2ban.md b/docs/extending-overseerr/fail2ban.md index fbfe6fa82..1cf9131f0 100644 --- a/docs/extending-overseerr/fail2ban.md +++ b/docs/extending-overseerr/fail2ban.md @@ -8,7 +8,7 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you ``` [Definition] -failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":"" +failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"" ``` You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail. From 29be6595125017700eccb34d33a0e852f23c97ba Mon Sep 17 00:00:00 2001 From: Danshil Kokil Mungur Date: Tue, 26 Apr 2022 02:53:33 +0400 Subject: [PATCH 4/8] fix(ui): don't show 0 playcount in slideover (#2714) --- src/components/ManageSlideOver/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index 8c6f27919..3e360d0a1 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -210,7 +210,7 @@ const ManageSlideOver: React.FC< {hasPermission(Permission.ADMIN) && (data.mediaInfo?.serviceUrl || data.mediaInfo?.tautulliUrl || - watchData?.data?.playCount) && ( + !!watchData?.data?.playCount) && (

{intl.formatMessage(messages.manageModalMedia)} @@ -325,7 +325,7 @@ const ManageSlideOver: React.FC< {hasPermission(Permission.ADMIN) && (data.mediaInfo?.serviceUrl4k || data.mediaInfo?.tautulliUrl4k || - watchData?.data4k?.playCount) && ( + !!watchData?.data4k?.playCount) && (

{intl.formatMessage(messages.manageModalMedia4k)} From e4039d09c0380d80f03c7a00b51a150f88c02cca Mon Sep 17 00:00:00 2001 From: Danshil Kokil Mungur Date: Tue, 26 Apr 2022 03:06:36 +0400 Subject: [PATCH 5/8] feat(api): add issue counts endpoint (#2713) --- overseerr-api.yml | 30 ++++++++++++++++++++ server/routes/issue.ts | 64 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/overseerr-api.yml b/overseerr-api.yml index 77282ea17..be9ee0c02 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -5562,6 +5562,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Issue' + + /issue/count: + get: + summary: Gets issue counts + description: | + Returns the number of open and closed issues, as well as the number of issues of each type. + tags: + - issue + responses: + '200': + description: Issue counts returned + content: + application/json: + schema: + type: object + properties: + total: + type: number + video: + type: number + audio: + type: number + subtitles: + type: number + others: + type: number + open: + type: number + closed: + type: number /issue/{issueId}: get: summary: Get issue diff --git a/server/routes/issue.ts b/server/routes/issue.ts index c7db5232c..07cf3277d 100644 --- a/server/routes/issue.ts +++ b/server/routes/issue.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import { getRepository } from 'typeorm'; -import { IssueStatus } from '../constants/issue'; +import { IssueStatus, IssueType } from '../constants/issue'; import Issue from '../entity/Issue'; import IssueComment from '../entity/IssueComment'; import Media from '../entity/Media'; @@ -146,6 +146,68 @@ issueRoutes.post< } ); +issueRoutes.get('/count', async (req, res, next) => { + const issueRepository = getRepository(Issue); + + try { + const query = issueRepository.createQueryBuilder('issue'); + + const totalCount = await query.getCount(); + + const videoCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.VIDEO, + }) + .getCount(); + + const audioCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.AUDIO, + }) + .getCount(); + + const subtitlesCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.SUBTITLES, + }) + .getCount(); + + const othersCount = await query + .where('issue.issueType = :issueType', { + issueType: IssueType.OTHER, + }) + .getCount(); + + const openCount = await query + .where('issue.status = :issueStatus', { + issueStatus: IssueStatus.OPEN, + }) + .getCount(); + + const closedCount = await query + .where('issue.status = :issueStatus', { + issueStatus: IssueStatus.RESOLVED, + }) + .getCount(); + + return res.status(200).json({ + total: totalCount, + video: videoCount, + audio: audioCount, + subtitles: subtitlesCount, + others: othersCount, + open: openCount, + closed: closedCount, + }); + } catch (e) { + logger.debug('Something went wrong retrieving issue counts.', { + label: 'API', + errorMessage: e.message, + }); + next({ status: 500, message: 'Unable to retrieve issue counts.' }); + } +}); + issueRoutes.get<{ issueId: string }>( '/:issueId', isAuthenticated( From 1054b4e2d7262d841fa83cde624f1138ad7bd23a Mon Sep 17 00:00:00 2001 From: Danshil Kokil Mungur Date: Sat, 30 Apr 2022 18:31:02 +0400 Subject: [PATCH 6/8] fix(search): use correct param to filter movies by year (#2727) --- server/api/themoviedb/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index cf5e280c2..b5060c030 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -129,7 +129,13 @@ class TheMovieDb extends ExternalAPI { }: SingleSearchOptions): Promise => { try { const data = await this.get('/search/movie', { - params: { query, page, include_adult: includeAdult, language, year }, + params: { + query, + page, + include_adult: includeAdult, + language, + primary_release_year: year, + }, }); return data; From 14519ef5559038b0d9d037a2bdc5d98e63c9db6f Mon Sep 17 00:00:00 2001 From: Brandon Cohen Date: Fri, 13 May 2022 15:15:53 -0400 Subject: [PATCH 7/8] fix(recommendations): only load more titles if there can be more than 40 (#2749) * fix: fixed recommendations page causing infinite network requests to tmdb api TMDB API would only return 40 results and the recommendations page expected more. This would cause an infinite amount of network requests. I set a limit specifically for this solving the problem. fix #2710 --- src/hooks/useDiscover.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/useDiscover.ts b/src/hooks/useDiscover.ts index 24ba47c1e..3bc016eb2 100644 --- a/src/hooks/useDiscover.ts +++ b/src/hooks/useDiscover.ts @@ -82,7 +82,9 @@ const useDiscover = >( const isEmpty = !isLoadingInitialData && titles?.length === 0; const isReachingEnd = - isEmpty || (!!data && (data[data?.length - 1]?.results.length ?? 0) < 20); + isEmpty || + (!!data && (data[data?.length - 1]?.results.length ?? 0) < 20) || + (!!data && (data[data?.length - 1]?.totalResults ?? 0) < 41); return { isLoadingInitialData, From 90095bb18548dfd663a78df1908c40dbf2f99faf Mon Sep 17 00:00:00 2001 From: Danshil Kokil Mungur Date: Mon, 23 May 2022 04:49:35 +0400 Subject: [PATCH 8/8] feat(manage slideover): show more request override details (#2772) * feat(manage slideover): show the language profile if request is for a show * feat(manage slideover): show name of profiles instead of id --- src/components/RequestBlock/index.tsx | 18 +++++++--- src/hooks/useRequestOverride.ts | 50 ++++++++++++++++++--------- src/i18n/locale/en.json | 1 + 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index 073ffaebb..d1d4ae8a6 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -26,6 +26,7 @@ const messages = defineMessages({ server: 'Destination Server', profilechanged: 'Quality Profile', rootfolder: 'Root Folder', + languageprofile: 'Language Profile', }); interface RequestBlockProps { @@ -38,7 +39,8 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { const intl = useIntl(); const [isUpdating, setIsUpdating] = useState(false); const [showEditModal, setShowEditModal] = useState(false); - const { profile, rootFolder, server } = useRequestOverride(request); + const { profile, rootFolder, server, languageProfile } = + useRequestOverride(request); const updateRequest = async (type: 'approve' | 'decline'): Promise => { setIsUpdating(true); @@ -209,7 +211,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => {

)} - {(server || profile !== null || rootFolder) && ( + {(server || profile || rootFolder || languageProfile) && ( <>
{intl.formatMessage(messages.requestoverrides)} @@ -223,12 +225,12 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { {server} )} - {profile !== null && ( + {profile && (
  • {intl.formatMessage(messages.profilechanged)} - ID {profile} + {profile}
  • )} {rootFolder && ( @@ -239,6 +241,14 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { {rootFolder} )} + {languageProfile && ( +
  • + + {intl.formatMessage(messages.languageprofile)} + + {languageProfile} +
  • + )} )} diff --git a/src/hooks/useRequestOverride.ts b/src/hooks/useRequestOverride.ts index ba791f990..be992e8b2 100644 --- a/src/hooks/useRequestOverride.ts +++ b/src/hooks/useRequestOverride.ts @@ -1,45 +1,61 @@ import useSWR from 'swr'; import { MediaRequest } from '../../server/entity/MediaRequest'; -import { ServiceCommonServer } from '../../server/interfaces/api/serviceInterfaces'; +import { + ServiceCommonServer, + ServiceCommonServerWithDetails, +} from '../../server/interfaces/api/serviceInterfaces'; interface OverrideStatus { - server: string | null; - profile: number | null; - rootFolder: string | null; + server?: string; + profile?: string; + rootFolder?: string; + languageProfile?: string; } const useRequestOverride = (request: MediaRequest): OverrideStatus => { - const { data } = useSWR( + const { data: allServers } = useSWR( `/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}` ); - if (!data) { - return { - server: null, - profile: null, - rootFolder: null, - }; + const { data } = useSWR( + `/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}/${ + request.serverId + }` + ); + + if (!data || !allServers) { + return {}; } - const defaultServer = data.find( + const defaultServer = allServers.find( (server) => server.is4k === request.is4k && server.isDefault ); - const activeServer = data.find((server) => server.id === request.serverId); + const activeServer = allServers.find( + (server) => server.id === request.serverId + ); return { server: activeServer && request.serverId !== defaultServer?.id ? activeServer.name - : null, + : undefined, profile: defaultServer?.activeProfileId !== request.profileId - ? request.profileId - : null, + ? data.profiles.find((profile) => profile.id === request.profileId) + ?.name + : undefined, rootFolder: defaultServer?.activeDirectory !== request.rootFolder ? request.rootFolder - : null, + : undefined, + languageProfile: + request.type === 'tv' && + defaultServer?.activeLanguageProfileId !== request.languageProfileId + ? data.languageProfiles?.find( + (profile) => profile.id === request.languageProfileId + )?.name + : undefined, }; }; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 6bcb39323..84b77f79d 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -269,6 +269,7 @@ "components.QuotaSelector.unlimited": "Unlimited", "components.RegionSelector.regionDefault": "All Regions", "components.RegionSelector.regionServerDefault": "Default ({region})", + "components.RequestBlock.languageprofile": "Language Profile", "components.RequestBlock.profilechanged": "Quality Profile", "components.RequestBlock.requestoverrides": "Request Overrides", "components.RequestBlock.rootfolder": "Root Folder",