mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 10:49:30 -05:00
Compare commits
21 Commits
v1.1.0
...
forgotPass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0259975402 | ||
|
|
71d33e47a9 | ||
|
|
93a9cff2ef | ||
|
|
e9fa94097a | ||
|
|
046ae932ec | ||
|
|
1570a8715a | ||
|
|
90095bb185 | ||
|
|
4f972be858 | ||
|
|
b07f7032ad | ||
|
|
af23a257d5 | ||
|
|
dec4062cdc | ||
|
|
e42153c599 | ||
|
|
d22bc09652 | ||
|
|
9ded45fef8 | ||
|
|
14519ef555 | ||
|
|
1054b4e2d7 | ||
|
|
e4039d09c0 | ||
|
|
29be659512 | ||
|
|
475314c87b | ||
|
|
1d00229a48 | ||
|
|
b2878390b4 |
@@ -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":"<HOST>"
|
||||
failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -5815,6 +5815,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
|
||||
|
||||
@@ -129,7 +129,13 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
||||
params: { query, page, include_adult: includeAdult, language, year },
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
primary_release_year: year,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -266,8 +266,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
as="a"
|
||||
buttonType="ghost"
|
||||
href={
|
||||
settings.currentSettings.jellyfinHost +
|
||||
'/web/#!/forgotpassword.html'
|
||||
process.env.JELLYFIN_TYPE == 'emby'
|
||||
? settings.currentSettings.jellyfinHost +
|
||||
'/web/index.html#!/startup/forgotpassword.html'
|
||||
: settings.currentSettings.jellyfinHost +
|
||||
'/web/index.html#!/forgotpassword.html'
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(messages.forgotpassword)}
|
||||
|
||||
@@ -210,7 +210,7 @@ const ManageSlideOver: React.FC<
|
||||
{hasPermission(Permission.ADMIN) &&
|
||||
(data.mediaInfo?.serviceUrl ||
|
||||
data.mediaInfo?.tautulliUrl ||
|
||||
watchData?.data?.playCount) && (
|
||||
!!watchData?.data?.playCount) && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-xl font-bold">
|
||||
{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) && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-xl font-bold">
|
||||
{intl.formatMessage(messages.manageModalMedia4k)}
|
||||
|
||||
@@ -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<RequestBlockProps> = ({ 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<void> => {
|
||||
setIsUpdating(true);
|
||||
@@ -209,7 +211,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(server || profile !== null || rootFolder) && (
|
||||
{(server || profile || rootFolder || languageProfile) && (
|
||||
<>
|
||||
<div className="mt-4 mb-1 text-sm">
|
||||
{intl.formatMessage(messages.requestoverrides)}
|
||||
@@ -223,12 +225,12 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
<span>{server}</span>
|
||||
</li>
|
||||
)}
|
||||
{profile !== null && (
|
||||
{profile && (
|
||||
<li className="flex justify-between px-1 py-2">
|
||||
<span className="font-bold">
|
||||
{intl.formatMessage(messages.profilechanged)}
|
||||
</span>
|
||||
<span>ID {profile}</span>
|
||||
<span>{profile}</span>
|
||||
</li>
|
||||
)}
|
||||
{rootFolder && (
|
||||
@@ -239,6 +241,14 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
||||
<span>{rootFolder}</span>
|
||||
</li>
|
||||
)}
|
||||
{languageProfile && (
|
||||
<li className="flex justify-between px-1 py-2">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.languageprofile)}
|
||||
</span>
|
||||
<span>{languageProfile}</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -82,7 +82,9 @@ const useDiscover = <T extends BaseMedia, S = Record<string, never>>(
|
||||
|
||||
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,
|
||||
|
||||
@@ -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<ServiceCommonServer[]>(
|
||||
const { data: allServers } = useSWR<ServiceCommonServer[]>(
|
||||
`/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}`
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
server: null,
|
||||
profile: null,
|
||||
rootFolder: null,
|
||||
};
|
||||
const { data } = useSWR<ServiceCommonServerWithDetails>(
|
||||
`/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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user