Merge branch 'develop' into custom-jellyfin-password-reset

This commit is contained in:
fallenbagel
2023-11-30 09:26:14 +05:00
10 changed files with 61 additions and 24 deletions

View File

@@ -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(),
}); });
} }

View File

@@ -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,
}); });

View File

@@ -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();

View File

@@ -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>
); );

View File

@@ -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}
/> />
</> </>
); );

View File

@@ -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>

View File

@@ -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}
/> />
); );
}; };

View File

@@ -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);
} }
}; };

View File

@@ -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,
}; };
}; };

View File

@@ -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',