mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-07 15:18:08 -05:00
Merge branch 'develop' into custom-jellyfin-password-reset
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import downloadTracker from '@server/lib/downloadtracker';
|
import downloadTracker from '@server/lib/downloadtracker';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
|
import {
|
||||||
|
jellyfinFullScanner,
|
||||||
|
jellyfinRecentScanner,
|
||||||
|
} from '@server/lib/scanners/jellyfin';
|
||||||
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
||||||
import { radarrScanner } from '@server/lib/scanners/radarr';
|
import { radarrScanner } from '@server/lib/scanners/radarr';
|
||||||
import { sonarrScanner } from '@server/lib/scanners/sonarr';
|
import { sonarrScanner } from '@server/lib/scanners/sonarr';
|
||||||
@@ -10,7 +14,6 @@ import watchlistSync from '@server/lib/watchlistsync';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import random from 'lodash/random';
|
import random from 'lodash/random';
|
||||||
import schedule from 'node-schedule';
|
import schedule from 'node-schedule';
|
||||||
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
|
|
||||||
|
|
||||||
interface ScheduledJob {
|
interface ScheduledJob {
|
||||||
id: JobId;
|
id: JobId;
|
||||||
@@ -73,38 +76,38 @@ export const startJobs = (): void => {
|
|||||||
// Run recently added jellyfin sync every 5 minutes
|
// Run recently added jellyfin sync every 5 minutes
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'jellyfin-recently-added-scan',
|
id: 'jellyfin-recently-added-scan',
|
||||||
name: 'Jellyfin Recently Added Sync',
|
name: 'Jellyfin Recently Added Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'minutes',
|
interval: 'minutes',
|
||||||
cronSchedule: jobs['jellyfin-recently-added-scan'].schedule,
|
cronSchedule: jobs['jellyfin-recently-added-scan'].schedule,
|
||||||
job: schedule.scheduleJob(
|
job: schedule.scheduleJob(
|
||||||
jobs['jellyfin-recently-added-scan'].schedule,
|
jobs['jellyfin-recently-added-scan'].schedule,
|
||||||
() => {
|
() => {
|
||||||
logger.info('Starting scheduled job: Jellyfin Recently Added Sync', {
|
logger.info('Starting scheduled job: Jellyfin Recently Added Scan', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
jobJellyfinRecentSync.run();
|
jellyfinRecentScanner.run();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
running: () => jobJellyfinRecentSync.status().running,
|
running: () => jellyfinRecentScanner.status().running,
|
||||||
cancelFn: () => jobJellyfinRecentSync.cancel(),
|
cancelFn: () => jellyfinRecentScanner.cancel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run full jellyfin sync every 24 hours
|
// Run full jellyfin sync every 24 hours
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'jellyfin-full-scan',
|
id: 'jellyfin-full-scan',
|
||||||
name: 'Jellyfin Full Library Sync',
|
name: 'Jellyfin Full Library Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'hours',
|
interval: 'hours',
|
||||||
cronSchedule: jobs['jellyfin-full-scan'].schedule,
|
cronSchedule: jobs['jellyfin-full-scan'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['jellyfin-full-scan'].schedule, () => {
|
job: schedule.scheduleJob(jobs['jellyfin-full-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Jellyfin Full Sync', {
|
logger.info('Starting scheduled job: Jellyfin Full Scan', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
jobJellyfinFullSync.run();
|
jellyfinFullScanner.run();
|
||||||
}),
|
}),
|
||||||
running: () => jobJellyfinFullSync.status().running,
|
running: () => jellyfinFullScanner.status().running,
|
||||||
cancelFn: () => jobJellyfinFullSync.cancel(),
|
cancelFn: () => jellyfinFullScanner.cancel(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface SyncStatus {
|
|||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class JobJellyfinSync {
|
class JellyfinScanner {
|
||||||
private sessionId: string;
|
private sessionId: string;
|
||||||
private tmdb: TheMovieDb;
|
private tmdb: TheMovieDb;
|
||||||
private jfClient: JellyfinAPI;
|
private jfClient: JellyfinAPI;
|
||||||
@@ -675,7 +675,7 @@ class JobJellyfinSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobJellyfinFullSync = new JobJellyfinSync();
|
export const jellyfinFullScanner = new JellyfinScanner();
|
||||||
export const jobJellyfinRecentSync = new JobJellyfinSync({
|
export const jellyfinRecentScanner = new JellyfinScanner({
|
||||||
isRecentOnly: true,
|
isRecentOnly: true,
|
||||||
});
|
});
|
||||||
@@ -12,12 +12,12 @@ import type {
|
|||||||
LogsResultsResponse,
|
LogsResultsResponse,
|
||||||
SettingsAboutResponse,
|
SettingsAboutResponse,
|
||||||
} from '@server/interfaces/api/settingsInterfaces';
|
} from '@server/interfaces/api/settingsInterfaces';
|
||||||
import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
|
|
||||||
import { scheduledJobs } from '@server/job/schedule';
|
import { scheduledJobs } from '@server/job/schedule';
|
||||||
import type { AvailableCacheIds } from '@server/lib/cache';
|
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
|
import { jellyfinFullScanner } from '@server/lib/scanners/jellyfin';
|
||||||
import { plexFullScanner } from '@server/lib/scanners/plex';
|
import { plexFullScanner } from '@server/lib/scanners/plex';
|
||||||
import type { JobId, Library, MainSettings } from '@server/lib/settings';
|
import type { JobId, Library, MainSettings } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
@@ -345,16 +345,16 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
|
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
|
||||||
return res.status(200).json(jobJellyfinFullSync.status());
|
return res.status(200).json(jellyfinFullScanner.status());
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/jellyfin/sync', (req, res) => {
|
settingsRoutes.post('/jellyfin/sync', (req, res) => {
|
||||||
if (req.body.cancel) {
|
if (req.body.cancel) {
|
||||||
jobJellyfinFullSync.cancel();
|
jellyfinFullScanner.cancel();
|
||||||
} else if (req.body.start) {
|
} else if (req.body.start) {
|
||||||
jobJellyfinFullSync.run();
|
jellyfinFullScanner.run();
|
||||||
}
|
}
|
||||||
return res.status(200).json(jobJellyfinFullSync.status());
|
return res.status(200).json(jellyfinFullScanner.status());
|
||||||
});
|
});
|
||||||
settingsRoutes.get('/tautulli', (_req, res) => {
|
settingsRoutes.get('/tautulli', (_req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type ListViewProps = {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isReachingEnd?: boolean;
|
isReachingEnd?: boolean;
|
||||||
onScrollBottom: () => void;
|
onScrollBottom: () => void;
|
||||||
|
mutateParent?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListView = ({
|
const ListView = ({
|
||||||
@@ -28,6 +29,7 @@ const ListView = ({
|
|||||||
onScrollBottom,
|
onScrollBottom,
|
||||||
isReachingEnd,
|
isReachingEnd,
|
||||||
plexItems,
|
plexItems,
|
||||||
|
mutateParent,
|
||||||
}: ListViewProps) => {
|
}: ListViewProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
|
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
|
||||||
@@ -46,7 +48,9 @@ const ListView = ({
|
|||||||
id={title.tmdbId}
|
id={title.tmdbId}
|
||||||
tmdbId={title.tmdbId}
|
tmdbId={title.tmdbId}
|
||||||
type={title.mediaType}
|
type={title.mediaType}
|
||||||
|
isAddedToWatchlist={true}
|
||||||
canExpand
|
canExpand
|
||||||
|
mutateParent={mutateParent}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const DiscoverWatchlist = () => {
|
|||||||
titles,
|
titles,
|
||||||
fetchMore,
|
fetchMore,
|
||||||
error,
|
error,
|
||||||
|
mutate,
|
||||||
} = useDiscover<WatchlistItem>(
|
} = useDiscover<WatchlistItem>(
|
||||||
`/api/v1/${
|
`/api/v1/${
|
||||||
router.pathname.startsWith('/profile')
|
router.pathname.startsWith('/profile')
|
||||||
@@ -76,6 +77,7 @@ const DiscoverWatchlist = () => {
|
|||||||
}
|
}
|
||||||
isReachingEnd={isReachingEnd}
|
isReachingEnd={isReachingEnd}
|
||||||
onScrollBottom={fetchMore}
|
onScrollBottom={fetchMore}
|
||||||
|
mutateParent={mutate}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import Modal from '@app/components/Common/Modal';
|
import Modal from '@app/components/Common/Modal';
|
||||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import type { SonarrSettings } from '@server/lib/settings';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { type SonarrSettings } from '@server/lib/settings';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@@ -109,6 +111,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const settings = useSettings();
|
||||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||||
profiles: [],
|
profiles: [],
|
||||||
rootFolders: [],
|
rootFolders: [],
|
||||||
@@ -255,7 +258,9 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
animeTags: sonarr?.animeTags ?? [],
|
animeTags: sonarr?.animeTags ?? [],
|
||||||
isDefault: sonarr?.isDefault ?? false,
|
isDefault: sonarr?.isDefault ?? false,
|
||||||
is4k: sonarr?.is4k ?? false,
|
is4k: sonarr?.is4k ?? false,
|
||||||
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
enableSeasonFolders:
|
||||||
|
sonarr?.enableSeasonFolders ??
|
||||||
|
settings.currentSettings.mediaServerType !== MediaServerType.PLEX,
|
||||||
externalUrl: sonarr?.externalUrl,
|
externalUrl: sonarr?.externalUrl,
|
||||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||||
enableSearch: !sonarr?.preventSearch,
|
enableSearch: !sonarr?.preventSearch,
|
||||||
@@ -961,11 +966,24 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.seasonfolders)}
|
{intl.formatMessage(messages.seasonfolders)}
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div
|
||||||
|
className={`form-input-area ${
|
||||||
|
settings.currentSettings.mediaServerType ===
|
||||||
|
MediaServerType.JELLYFIN ||
|
||||||
|
settings.currentSettings.mediaServerType ===
|
||||||
|
MediaServerType.EMBY
|
||||||
|
? 'opacity-50'
|
||||||
|
: 'opacity-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Field
|
<Field
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="enableSeasonFolders"
|
id="enableSeasonFolders"
|
||||||
name="enableSeasonFolders"
|
name="enableSeasonFolders"
|
||||||
|
disabled={
|
||||||
|
settings.currentSettings.mediaServerType !==
|
||||||
|
MediaServerType.PLEX
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface TmdbTitleCardProps {
|
|||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
isAddedToWatchlist?: boolean;
|
isAddedToWatchlist?: boolean;
|
||||||
|
mutateParent?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||||
@@ -25,6 +26,7 @@ const TmdbTitleCard = ({
|
|||||||
type,
|
type,
|
||||||
canExpand,
|
canExpand,
|
||||||
isAddedToWatchlist = false,
|
isAddedToWatchlist = false,
|
||||||
|
mutateParent,
|
||||||
}: TmdbTitleCardProps) => {
|
}: TmdbTitleCardProps) => {
|
||||||
const { hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
|
|
||||||
@@ -71,6 +73,7 @@ const TmdbTitleCard = ({
|
|||||||
year={title.releaseDate}
|
year={title.releaseDate}
|
||||||
mediaType={'movie'}
|
mediaType={'movie'}
|
||||||
canExpand={canExpand}
|
canExpand={canExpand}
|
||||||
|
mutateParent={mutateParent}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TitleCard
|
<TitleCard
|
||||||
@@ -87,6 +90,7 @@ const TmdbTitleCard = ({
|
|||||||
year={title.firstAirDate}
|
year={title.firstAirDate}
|
||||||
mediaType={'tv'}
|
mediaType={'tv'}
|
||||||
canExpand={canExpand}
|
canExpand={canExpand}
|
||||||
|
mutateParent={mutateParent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface TitleCardProps {
|
|||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
inProgress?: boolean;
|
inProgress?: boolean;
|
||||||
isAddedToWatchlist?: number | boolean;
|
isAddedToWatchlist?: number | boolean;
|
||||||
|
mutateParent?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -61,6 +62,7 @@ const TitleCard = ({
|
|||||||
isAddedToWatchlist = false,
|
isAddedToWatchlist = false,
|
||||||
inProgress = false,
|
inProgress = false,
|
||||||
canExpand = false,
|
canExpand = false,
|
||||||
|
mutateParent,
|
||||||
}: TitleCardProps) => {
|
}: TitleCardProps) => {
|
||||||
const isTouch = useIsTouch();
|
const isTouch = useIsTouch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -148,6 +150,9 @@ const TitleCard = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
mutate('/api/v1/discover/watchlist');
|
mutate('/api/v1/discover/watchlist');
|
||||||
|
if (mutateParent) {
|
||||||
|
mutateParent();
|
||||||
|
}
|
||||||
setToggleWatchlist((prevState) => !prevState);
|
setToggleWatchlist((prevState) => !prevState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface DiscoverResult<T, S> {
|
|||||||
error: unknown;
|
error: unknown;
|
||||||
titles: T[];
|
titles: T[];
|
||||||
firstResultData?: BaseSearchResult<T> & S;
|
firstResultData?: BaseSearchResult<T> & S;
|
||||||
|
mutate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraEncodes: [RegExp, string][] = [
|
const extraEncodes: [RegExp, string][] = [
|
||||||
@@ -54,7 +55,7 @@ const useDiscover = <
|
|||||||
{ hideAvailable = true } = {}
|
{ hideAvailable = true } = {}
|
||||||
): DiscoverResult<T, S> => {
|
): DiscoverResult<T, S> => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { data, error, size, setSize, isValidating } = useSWRInfinite<
|
const { data, error, size, setSize, isValidating, mutate } = useSWRInfinite<
|
||||||
BaseSearchResult<T> & S
|
BaseSearchResult<T> & S
|
||||||
>(
|
>(
|
||||||
(pageIndex: number, previousPageData) => {
|
(pageIndex: number, previousPageData) => {
|
||||||
@@ -119,6 +120,7 @@ const useDiscover = <
|
|||||||
error,
|
error,
|
||||||
titles,
|
titles,
|
||||||
firstResultData: data?.[0],
|
firstResultData: data?.[0],
|
||||||
|
mutate,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const defaultTheme = require('tailwindcss/defaultTheme');
|
|||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
important: true,
|
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
content: [
|
content: [
|
||||||
'./node_modules/react-tailwindcss-datepicker-sct/dist/index.esm.js',
|
'./node_modules/react-tailwindcss-datepicker-sct/dist/index.esm.js',
|
||||||
|
|||||||
Reference in New Issue
Block a user