mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 18:59:28 -05:00
Compare commits
25 Commits
preview-pr
...
feat-serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2c752cf7 | ||
|
|
dc34d6c1f6 | ||
|
|
d868082b56 | ||
|
|
e531765dac | ||
|
|
58eb91530b | ||
|
|
2d802a5eff | ||
|
|
e03edd2d1f | ||
|
|
2d9530c0ed | ||
|
|
1cc43cee93 | ||
|
|
5bcbc810d6 | ||
|
|
b8042ab700 | ||
|
|
c3a6c7d4b2 | ||
|
|
6636aeef45 | ||
|
|
6207a6a26d | ||
|
|
5875b2b5c2 | ||
|
|
635a5f019c | ||
|
|
900e6110ad | ||
|
|
19e20749c1 | ||
|
|
1aac3c5f09 | ||
|
|
1bc4bd3d69 | ||
|
|
7a505813d5 | ||
|
|
45dbf84d7e | ||
|
|
a4f1f1203a | ||
|
|
504d8bd5fe | ||
|
|
395a91c2f0 |
@@ -8,7 +8,7 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/discord/952656177924300932" alt="Discord"></a>
|
||||
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
||||
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-47-orange.svg"/></a>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const VersionMismatchWarning = () => {
|
||||
<>
|
||||
{!isUpToDate ? (
|
||||
<Admonition type="warning">
|
||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package-derivation">override the package derivation</a>.
|
||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package">override the package derivation</a>.
|
||||
</Admonition>
|
||||
) : (
|
||||
<Admonition type="success">
|
||||
@@ -95,12 +95,12 @@ export const VersionMatch = () => {
|
||||
};
|
||||
|
||||
offlineCache = pkgs.fetchYarnDeps {
|
||||
sha256 = pkgs.lib.fakeSha256;
|
||||
sha256 = pkgs.lib.fakeSha256;
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
}`;
|
||||
|
||||
|
||||
const module = `{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
@@ -12,8 +12,6 @@ This is your Jellyseerr API key, which can be used to integrate Jellyseerr with
|
||||
|
||||
If you need to generate a new API key for any reason, simply click the button to the right of the text box.
|
||||
|
||||
If you want to set the API key, rather than letting it be randomly generated, you can use the API_KEY environment variable. Whatever that variable is set to will be your API key.
|
||||
|
||||
## Application Title
|
||||
|
||||
If you aren't a huge fan of the name "Jellyseerr" and would like to display something different to your users, you can customize the application title!
|
||||
|
||||
@@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene
|
||||
|
||||
### Discover Region & Discover Language
|
||||
|
||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region--discover-language) to suit their own preferences.
|
||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-and-discover-language) to suit their own preferences.
|
||||
|
||||
### Movie Request Limit & Series Request Limit
|
||||
|
||||
|
||||
@@ -4864,11 +4864,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
example: 8|9
|
||||
- in: query
|
||||
name: status
|
||||
schema:
|
||||
type: string
|
||||
example: 3|4
|
||||
responses:
|
||||
'200':
|
||||
description: Results
|
||||
|
||||
@@ -303,10 +303,10 @@ class SonarrAPI extends ServarrBase<{
|
||||
});
|
||||
|
||||
try {
|
||||
await this.runCommand('MissingEpisodeSearch', { seriesId });
|
||||
await this.runCommand('SeriesSearch', { seriesId });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while executing Sonarr missing episode search.',
|
||||
'Something went wrong while executing Sonarr series search.',
|
||||
{
|
||||
label: 'Sonarr API',
|
||||
errorMessage: e.message,
|
||||
|
||||
@@ -95,7 +95,6 @@ interface DiscoverTvOptions {
|
||||
sortBy?: SortOptions;
|
||||
watchRegion?: string;
|
||||
watchProviders?: string;
|
||||
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
@@ -524,7 +523,6 @@ class TheMovieDb extends ExternalAPI {
|
||||
voteCountLte,
|
||||
watchProviders,
|
||||
watchRegion,
|
||||
withStatus,
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const defaultFutureDate = new Date(
|
||||
@@ -572,7 +570,6 @@ class TheMovieDb extends ExternalAPI {
|
||||
'vote_count.lte': voteCountLte || '',
|
||||
with_watch_providers: watchProviders || '',
|
||||
watch_region: watchRegion || '',
|
||||
with_status: withStatus || '',
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
@@ -2,7 +2,6 @@ export enum ApiErrorCode {
|
||||
InvalidUrl = 'INVALID_URL',
|
||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||
InvalidEmail = 'INVALID_EMAIL',
|
||||
NotAdmin = 'NOT_ADMIN',
|
||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||
|
||||
@@ -611,11 +611,7 @@ class Settings {
|
||||
}
|
||||
|
||||
private generateApiKey(): string {
|
||||
if (process.env.API_KEY) {
|
||||
return process.env.API_KEY;
|
||||
} else {
|
||||
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
|
||||
}
|
||||
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
|
||||
}
|
||||
|
||||
private generateVapidKeys(force = false): void {
|
||||
@@ -652,12 +648,6 @@ class Settings {
|
||||
|
||||
this.data = merge(this.data, parsedJson);
|
||||
|
||||
if (process.env.API_KEY) {
|
||||
if (this.main.apiKey != process.env.API_KEY) {
|
||||
this.main.apiKey = process.env.API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
}
|
||||
return this;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateHostname = (settings: any): AllSettings => {
|
||||
const oldMediaServerType = settings.main.mediaServerType;
|
||||
console.log('Migrating media server type', oldMediaServerType);
|
||||
if (
|
||||
oldMediaServerType === MediaServerType.JELLYFIN &&
|
||||
process.env.JELLYFIN_TYPE === 'emby'
|
||||
@@ -71,7 +71,6 @@ const QueryFilterOptions = z.object({
|
||||
network: z.coerce.string().optional(),
|
||||
watchProviders: z.coerce.string().optional(),
|
||||
watchRegion: z.coerce.string().optional(),
|
||||
status: z.coerce.string().optional(),
|
||||
});
|
||||
|
||||
export type FilterOptions = z.infer<typeof QueryFilterOptions>;
|
||||
@@ -386,7 +385,6 @@ discoverRoutes.get('/tv', async (req, res, next) => {
|
||||
voteCountLte: query.voteCountLte,
|
||||
watchProviders: query.watchProviders,
|
||||
watchRegion: query.watchRegion,
|
||||
withStatus: query.status,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
|
||||
@@ -2,7 +2,6 @@ import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
@@ -551,10 +550,7 @@ router.post(
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType:
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
await userRepository.save(newUser);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import { UserSettings } from '@server/entity/UserSettings';
|
||||
@@ -10,7 +9,6 @@ import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { Router } from 'express';
|
||||
import { canMakePermissionsChange } from '.';
|
||||
|
||||
@@ -100,18 +98,10 @@ userSettingsRoutes.post<
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
const oldEmail = user.email;
|
||||
if (user.jellyfinUsername) {
|
||||
user.email = req.body.email || user.jellyfinUsername || user.email;
|
||||
}
|
||||
|
||||
const existingUser = await userRepository.findOne({
|
||||
where: { email: user.email },
|
||||
});
|
||||
if (oldEmail !== user.email && existingUser) {
|
||||
throw new ApiError(400, ApiErrorCode.InvalidEmail);
|
||||
}
|
||||
|
||||
// Update quota values only if the user has the correct permissions
|
||||
if (
|
||||
!user.hasPermission(Permission.MANAGE_USERS) &&
|
||||
@@ -155,14 +145,7 @@ userSettingsRoutes.post<
|
||||
email: savedUser.email,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errorCode) {
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
} else {
|
||||
return next({ status: 500, message: e.message });
|
||||
}
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
CompanySelector,
|
||||
GenreSelector,
|
||||
KeywordSelector,
|
||||
StatusSelector,
|
||||
WatchProviderSelector,
|
||||
} from '@app/components/Selector';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
@@ -41,7 +40,6 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
|
||||
runtime: 'Runtime',
|
||||
streamingservices: 'Streaming Services',
|
||||
voteCount: 'Number of votes between {minValue} and {maxValue}',
|
||||
status: 'Status',
|
||||
});
|
||||
|
||||
type FilterSlideoverProps = {
|
||||
@@ -152,23 +150,6 @@ const FilterSlideover = ({
|
||||
updateQueryParams('genre', value?.map((v) => v.value).join(','));
|
||||
}}
|
||||
/>
|
||||
{type === 'tv' && (
|
||||
<>
|
||||
<span className="text-lg font-semibold">
|
||||
{intl.formatMessage(messages.status)}
|
||||
</span>
|
||||
<StatusSelector
|
||||
defaultValue={currentFilters.status}
|
||||
isMulti
|
||||
onChange={(value) => {
|
||||
updateQueryParams(
|
||||
'status',
|
||||
value?.map((v) => v.value).join('|')
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<span className="text-lg font-semibold">
|
||||
{intl.formatMessage(messages.keywords)}
|
||||
</span>
|
||||
|
||||
@@ -108,7 +108,6 @@ export const QueryFilterOptions = z.object({
|
||||
voteCountGte: z.string().optional(),
|
||||
watchRegion: z.string().optional(),
|
||||
watchProviders: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FilterOptions = z.infer<typeof QueryFilterOptions>;
|
||||
@@ -148,10 +147,6 @@ export const prepareFilterValues = (
|
||||
filterValues.genre = values.genre;
|
||||
}
|
||||
|
||||
if (values.status) {
|
||||
filterValues.status = values.status;
|
||||
}
|
||||
|
||||
if (values.keywords) {
|
||||
filterValues.keywords = values.keywords;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,6 @@ const messages = defineMessages('components.Selector', {
|
||||
nooptions: 'No results.',
|
||||
showmore: 'Show More',
|
||||
showless: 'Show Less',
|
||||
searchStatus: 'Select status...',
|
||||
returningSeries: 'Returning Series',
|
||||
planned: 'Planned',
|
||||
inProduction: 'In Production',
|
||||
ended: 'Ended',
|
||||
canceled: 'Canceled',
|
||||
pilot: 'Pilot',
|
||||
});
|
||||
|
||||
type SingleVal = {
|
||||
@@ -211,75 +204,6 @@ export const GenreSelector = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusSelector = ({
|
||||
isMulti,
|
||||
defaultValue,
|
||||
onChange,
|
||||
}: BaseSelectorMultiProps | BaseSelectorSingleProps) => {
|
||||
const intl = useIntl();
|
||||
const [defaultDataValue, setDefaultDataValue] = useState<
|
||||
{ label: string; value: number }[] | null
|
||||
>(null);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{ name: intl.formatMessage(messages.returningSeries), id: 0 },
|
||||
{ name: intl.formatMessage(messages.planned), id: 1 },
|
||||
{ name: intl.formatMessage(messages.inProduction), id: 2 },
|
||||
{ name: intl.formatMessage(messages.ended), id: 3 },
|
||||
{ name: intl.formatMessage(messages.canceled), id: 4 },
|
||||
{ name: intl.formatMessage(messages.pilot), id: 5 },
|
||||
],
|
||||
[intl]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefaultStatus = async (): Promise<void> => {
|
||||
if (!defaultValue) {
|
||||
return;
|
||||
}
|
||||
const statuses = defaultValue.split('|');
|
||||
|
||||
const statusData = options
|
||||
.filter((opt) => statuses.find((s) => Number(s) === opt.id))
|
||||
.map((o) => ({
|
||||
label: o.name,
|
||||
value: o.id,
|
||||
}));
|
||||
|
||||
setDefaultDataValue(statusData);
|
||||
};
|
||||
|
||||
loadDefaultStatus();
|
||||
}, [defaultValue, options]);
|
||||
|
||||
const loadStatusOptions = async () => {
|
||||
return options
|
||||
.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}))
|
||||
.filter(({ label }) => label.toLowerCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
key={`status-select-${defaultDataValue}`}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
defaultValue={isMulti ? defaultDataValue : defaultDataValue?.[0]}
|
||||
defaultOptions
|
||||
isMulti={isMulti}
|
||||
loadOptions={loadStatusOptions}
|
||||
placeholder={intl.formatMessage(messages.searchStatus)}
|
||||
onChange={(value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onChange(value as any);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeywordSelector = ({
|
||||
isMulti,
|
||||
defaultValue,
|
||||
|
||||
@@ -100,8 +100,6 @@ const Setup = () => {
|
||||
router,
|
||||
]);
|
||||
|
||||
if (settings.currentSettings.initialized) return <></>;
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col justify-center bg-gray-900 py-12">
|
||||
<PageTitle title={intl.formatMessage(messages.setup)} />
|
||||
|
||||
@@ -14,7 +14,6 @@ import globalMessages from '@app/i18n/globalMessages';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -43,7 +42,6 @@ const messages = defineMessages(
|
||||
user: 'User',
|
||||
toastSettingsSuccess: 'Settings saved successfully!',
|
||||
toastSettingsFailure: 'Something went wrong while saving settings.',
|
||||
toastSettingsFailureEmail: 'This email is already taken!',
|
||||
region: 'Discover Region',
|
||||
regionTip: 'Filter content by regional availability',
|
||||
originallanguage: 'Discover Language',
|
||||
@@ -180,7 +178,7 @@ const UserGeneralSettings = () => {
|
||||
watchlistSyncTv: values.watchlistSyncTv,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (currentUser?.id === user?.id && setLocale) {
|
||||
setLocale(
|
||||
@@ -195,24 +193,10 @@ const UserGeneralSettings = () => {
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
if (errorData?.message === ApiErrorCode.InvalidEmail) {
|
||||
addToast(intl.formatMessage(messages.toastSettingsFailureEmail), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} else {
|
||||
addToast(intl.formatMessage(messages.toastSettingsFailure), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
}
|
||||
addToast(intl.formatMessage(messages.toastSettingsFailure), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
revalidateUser();
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
"components.Discover.FilterSlideover.releaseDate": "Release Date",
|
||||
"components.Discover.FilterSlideover.runtime": "Runtime",
|
||||
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime",
|
||||
"components.Discover.FilterSlideover.status": "Status",
|
||||
"components.Discover.FilterSlideover.streamingservices": "Streaming Services",
|
||||
"components.Discover.FilterSlideover.studio": "Studio",
|
||||
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
|
||||
@@ -559,16 +558,9 @@
|
||||
"components.ResetPassword.validationpasswordrequired": "You must provide a password",
|
||||
"components.Search.search": "Search",
|
||||
"components.Search.searchresults": "Search Results",
|
||||
"components.Selector.canceled": "Canceled",
|
||||
"components.Selector.ended": "Ended",
|
||||
"components.Selector.inProduction": "In Production",
|
||||
"components.Selector.nooptions": "No results.",
|
||||
"components.Selector.pilot": "Pilot",
|
||||
"components.Selector.planned": "Planned",
|
||||
"components.Selector.returningSeries": "Returning Series",
|
||||
"components.Selector.searchGenres": "Select genres…",
|
||||
"components.Selector.searchKeywords": "Search keywords…",
|
||||
"components.Selector.searchStatus": "Select status...",
|
||||
"components.Selector.searchStudios": "Search studios…",
|
||||
"components.Selector.showless": "Show Less",
|
||||
"components.Selector.showmore": "Show More",
|
||||
|
||||
Reference in New Issue
Block a user